/*
httpserve - Serve a directory over HTTP
Copyright (C) 2024 Ian Cowburn (ianc@noddybox.co.uk)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* ---------------------------------------------------------------------------
TYPES AND GLOBALS
---------------------------------------------------------------------------
*/
typedef struct
{
int fd;
magic_t magic;
int allow_post;
} thread_arg_t;
/* ---------------------------------------------------------------------------
BASE ROUTINES
---------------------------------------------------------------------------
*/
static char *Strdup(const char *p)
{
char *ret = strdup(p);
if (!ret)
{
perror("strdup");
exit(EXIT_FAILURE);
}
return ret;
}
static void Chomp(char *p)
{
int len = strlen(p);
len--;
while(len >= 0 && (p[len] == '\r' || p[len] == '\n'))
{
p[len--] = 0;
}
}
static const char *GetFilename(const char *path)
{
const char *p = strrchr(path, '/');
if (p)
{
if (*(p + 1))
{
return p + 1;
}
else
{
return p;
}
}
return path;
}
static int StrMatch(const char *a, const char *b)
{
return strcasecmp(a,b) == 0;
}
static void *Malloc(size_t s)
{
void *ret = malloc(s);
if (!ret)
{
perror("malloc");
exit(EXIT_FAILURE);
}
return ret;
}
static void *Realloc(void *p, size_t s)
{
void *ret = realloc(p, s);
if (!ret)
{
perror("realloc");
exit(EXIT_FAILURE);
}
return ret;
}
static char *PrintStream(char *stream, const char *format, ...)
{
char buff[4096];
va_list va;
size_t stream_len;
size_t buff_len;
va_start(va, format);
vsnprintf(buff, sizeof buff, format, va);
va_end(va);
buff_len = strlen(buff);
if (stream)
{
stream_len = strlen(stream);
}
else
{
stream_len = 0;
}
stream = Realloc(stream, stream_len + buff_len + 1);
strcpy(stream + stream_len, buff);
return stream;
}
static int SplitHeader(char *buff, char **setting, char **value)
{
char *p = buff;
*setting = NULL;
*value = NULL;
while(*p && !isspace((unsigned char)*p) && *p != ':')
{
p++;
}
if (*p)
{
*setting = buff;
}
while (*p && (isspace((unsigned char)*p) || *p == ':'))
{
*p++ = 0;
}
if (*p)
{
*value = p;
}
return *setting && *value;
}
/* ---------------------------------------------------------------------------
SOCKET ROUTINES
---------------------------------------------------------------------------
*/
static int Listen(int port)
{
struct sockaddr_in addr;
int sockopt = 1;
int fd;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
&sockopt, sizeof sockopt) == -1)
{
perror("setsockopt");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(fd, (void*)&addr, sizeof(addr)) == -1)
{
perror("bind");
return -1;
}
if (listen(fd, 5) == -1)
{
perror("listen");
return -1;
}
return fd;
}
static int Read(int fd, char *buff, size_t len)
{
int rd;
size_t tot = 0;
while(tot < len)
{
rd = read(fd, buff + tot, len - tot);
if (rd < 1)
{
return tot > 0 ? tot : 0;
}
tot += rd;
}
return tot;
}
static int ReadByte(int fd, unsigned char *byte)
{
if (read(fd, byte, 1) == 1)
{
return 1;
}
else
{
return 0;
}
}
static int ReadLine(int fd, char *line, size_t max_size)
{
unsigned char c;
unsigned char last = 0;
size_t len = 0;
while(ReadByte(fd, &c) && len < max_size - 1)
{
line[len] = c;
line[len + 1] = 0;
if (c == '\n' && last == '\r')
{
Chomp(line);
return 1;
}
len++;
last = c;
}
return 0;
}
/* ---------------------------------------------------------------------------
HTML ROUTINES
---------------------------------------------------------------------------
*/
static void GenerateListing(int fd, const char *protocol)
{
glob_t gl;
char *stream = NULL;
int f;
glob("*", 0, NULL, &gl);
stream = PrintStream(stream, "\n");
for(f = 0; f < gl.gl_pathc; f++)
{
stream = PrintStream(stream, "%s
",
gl.gl_pathv[f], gl.gl_pathv[f]);
}
stream = PrintStream(stream, "");
dprintf(fd, "%s 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %lu\r\n"
"\r\n"
"%s", protocol, strlen(stream), stream);
globfree(&gl);
free(stream);
}
static void GenerateStatus(int fd,
int status,
const char *status_text,
const char *protocol)
{
dprintf(fd, "%s %d %s\r\nContent-Length: 0\r\n\r\n",
protocol, status, status_text);
}
static void GenerateFile(int fd,
magic_t magic,
const char *path,
const char *protocol)
{
char buff[4096];
int file;
off_t size;
ssize_t len;
file = open(path, O_RDONLY);
if (file == -1)
{
perror("open");
GenerateStatus(fd, 404, "Not found", protocol);
return;
}
size = lseek(file, 0, SEEK_END);
lseek(file, 0, SEEK_SET);
dprintf(fd, "%s 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %lu\r\n"
"\r\n", protocol, magic_file(magic, path), (unsigned long)size);
while((len = read(file, buff, sizeof buff)) > 0)
{
write(fd, buff, len);
}
close(file);
}
/* ---------------------------------------------------------------------------
HTTP ROUTINES
---------------------------------------------------------------------------
*/
static void DecodeURL(char *dest, const char *url)
{
while(*url)
{
char num[3]={0};
switch(*url)
{
case '%':
num[0] = url[1];
num[1] = url[2];
*dest++ = (char)strtol(num, NULL, 16);
url += 3;
break;
case '+':
*dest++ = ' ';
url++;
break;
default:
*dest++ = *url++;
break;
}
}
*dest = 0;
}
static int GetRequest(int fd,
char **method,
char **path,
char **protocol,
size_t *content_length,
int *close)
{
char buff[0x4000];
char *tok[3] = {0};
char *last;
int f;
*method = NULL;
*path = NULL;
*protocol = NULL;
*content_length = 0;
*close = 0;
if (ReadLine(fd, buff, sizeof buff))
{
if ((tok[0] = strtok_r(buff, " ", &last)) &&
(tok[1] = strtok_r(NULL, " ", &last)) &&
(tok[2] = strtok_r(NULL, " \r\n", &last)))
{
char *tmppath;
*method = Strdup(tok[0]);
*path = Strdup(tok[1]);
*protocol = Strdup(tok[2]);
tmppath = Strdup(*path);
DecodeURL(*path, tmppath);
free(tmppath);
if(strcmp(*protocol, "HTTP/1.0") == 0)
{
*close = 1;
}
}
while(ReadLine(fd, buff, sizeof buff) && buff[0])
{
if (SplitHeader(buff, &tok[0], &tok[1]))
{
if (StrMatch(tok[0], "connection") && StrMatch(tok[1], "close"))
{
*close = 1;
}
else if (StrMatch(tok[0], "content-length"))
{
*content_length = (size_t)atoll(tok[1]);
}
}
}
return 1;
}
*close = 1;
return 0;
}
static char *ReadContents(int fd, size_t len)
{
char *contents = Malloc(len);
if (contents)
{
if (Read(fd, contents, len) != len)
{
free(contents);
contents = NULL;
}
}
return contents;
}
/* ---------------------------------------------------------------------------
THREAD CODE
---------------------------------------------------------------------------
*/
static void *ThreadCode(void *p)
{
char *method = NULL;
char *path = NULL;
char *protocol = NULL;
size_t content_len = 0;
int close_connection = 0;
thread_arg_t *args = p;
while (!close_connection)
{
if (GetRequest(args->fd, &method, &path,
&protocol, &content_len, &close_connection))
{
const char *filename;
printf("%s %s %s\n", method, path, protocol);
filename = GetFilename(path);
if (strcmp(method, "GET") == 0)
{
if (strcmp(filename, "/") == 0)
{
printf("Generate listing for /\n");
GenerateListing(args->fd, protocol);
}
else if (access(filename, R_OK) == 0)
{
printf("Generating download for %s\n", filename);
GenerateFile(args->fd, args->magic, filename, protocol);
}
else
{
printf("File not found - returning 404\n");
GenerateStatus(args->fd, 404, "Not found", protocol);
}
}
else if (strcmp(method, "POST") == 0 && args->allow_post)
{
char *contents;
if ((contents = ReadContents(args->fd, content_len)))
{
int file = -1;
if (!StrMatch(filename, "/") &&
(file = open(filename,
O_WRONLY|O_CREAT|O_EXCL,
0664)) != -1 &&
write(file, contents, content_len) ==
content_len)
{
printf("Wrote %lu bytes to '%s'\n",
(unsigned long)content_len, filename);
GenerateStatus(args->fd, 200, "OK", protocol);
}
else
{
if (file == -1)
{
perror("open");
}
GenerateStatus(args->fd, 500,
"Failed to write contents", protocol);
}
if (file != -1)
{
close(file);
}
free(contents);
}
else
{
GenerateStatus(args->fd, 500,
"Failed to read contents from request",
protocol);
}
}
else
{
printf("Method %s not supported\n", method);
GenerateStatus(args->fd, 501, "Not implemented", protocol);
}
free(method);
free(path);
free(protocol);
}
else
{
if (!close_connection)
{
printf("Bad request\n");
}
}
}
printf("Connection closed on %d\n", args->fd);
close(args->fd);
return NULL;
}
/* ---------------------------------------------------------------------------
MAIN
---------------------------------------------------------------------------
*/
int main(int argc, char *argv[])
{
magic_t magic;
int port = 80;
int opt;
int sock;
const char *dir=".";
int allow_post = 0;
while((opt = getopt(argc, argv, "p:d:a")) != -1)
{
switch(opt)
{
case 'p':
port = atoi(optarg);
break;
case 'd':
dir = optarg;
break;
case 'a':
allow_post = 1;
break;
default:
fprintf(stderr, "%s: usage %s [-p port] [-d dir]\n",
argv[0], argv[0]);
return EXIT_FAILURE;
}
}
magic = magic_open(MAGIC_MIME_TYPE);
if (!magic)
{
fprintf(stderr, "%s: Failed to open magic library\n", argv[0]);
}
magic_load(magic, NULL);
if ((sock = Listen(port)) == -1)
{
return EXIT_FAILURE;
}
chdir(dir);
printf("Serving up %s on %d\n", dir, port);
while(1)
{
pthread_t thread;
thread_arg_t *thread_args;
int fd;
if ((fd = accept(sock, NULL, 0)) == -1)
{
perror("accept");
close(sock);
return EXIT_FAILURE;
}
printf("Accepted connection on %d\n", fd);
thread_args = Malloc(sizeof *thread_args);
thread_args->fd = fd;
thread_args->magic = magic;
thread_args->allow_post = allow_post;
pthread_create(&thread, NULL, ThreadCode, thread_args);
pthread_detach(thread);
}
return EXIT_SUCCESS;
}
/*
vim: ai sw=4 ts=8 expandtab
*/