/* 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; printf("Got body of %lu bytes\n", (unsigned long)content_len); if (!StrMatch(filename, "/") && (file = open(filename, O_WRONLY|O_CREAT|O_EXCL)) != -1 && write(file, contents, content_len) == content_len) { GenerateStatus(args->fd, 200, "OK", protocol); } else { 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 { 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 */