smol_http

A simple http server supporting GET requests. Written in less than 400 lines of C.
Log | Files | Refs | README | LICENSE

smol_http.c (12166B)


      1 /*
      2 Copyright (C) 2022 by Anton Kling <anton@kling.gg>
      3 
      4 Permission to use, copy, modify, and/or distribute this software for any
      5 purpose with or without fee is hereby granted.
      6 
      7 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      8 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      9 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     10 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     11 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     12 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     13 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     14 */
     15 #include "config.h"
     16 #include <arpa/inet.h>
     17 #include <assert.h>
     18 #include <dirent.h>
     19 #include <errno.h>
     20 #include <fcntl.h>
     21 #include <getopt.h>
     22 #include <netdb.h>
     23 #include <pwd.h>
     24 #include <signal.h>
     25 #include <stdio.h>
     26 #include <stdlib.h>
     27 #include <string.h>
     28 #include <sys/socket.h>
     29 #include <sys/stat.h>
     30 #include <sys/time.h>
     31 #include <unistd.h>
     32 
     33 #define MAX_BUFFER 4096 // Size of the read buffer
     34 
     35 #ifndef PATH_MAX
     36 #define PATH_MAX 4096
     37 #endif
     38 
     39 #define COND_PERROR_EXP(condition, function_name, expression)                  \
     40     if (condition) {                                                           \
     41         perror(function_name);                                                 \
     42         expression;                                                            \
     43     }
     44 
     45 #define HAS_PLEDGE (__OpenBSD__ | __serenity__)
     46 #define HAS_UNVEIL (__OpenBSD__ | __serenity__)
     47 #define HAS_SENDFILE (__linux__ | __FreeBSD__)
     48 
     49 #if HAS_SENDFILE
     50 #include <sys/sendfile.h>
     51 #endif
     52 
     53 #if HAS_PLEDGE
     54 #define PLEDGE(promise, exec)                                                  \
     55     COND_PERROR_EXP(-1 == pledge(promise, exec), "pledge", exit(1))
     56 #else
     57 #define PLEDGE(promise, exec) ;
     58 #endif
     59 
     60 #if HAS_UNVEIL
     61 #define UNVEIL(path, permissions)                                              \
     62     COND_PERROR_EXP(-1 == unveil(path, permissions), "unveil", exit(1))
     63 #else
     64 #define UNVEIL(path, permissions) ;
     65 #endif
     66 
     67 #define ASSERT_NOT_REACHED assert(0)
     68 
     69 static const struct {
     70     char *ext;
     71     char *type;
     72 } mimes[] = {
     73     {"xml", "application/xml; charset=utf-8"},
     74     {"xhtml", "application/xhtml+xml; charset=utf-8"},
     75     {"html", "text/html; charset=utf-8"},
     76     {"htm", "text/html; charset=utf-8"},
     77     {"css", "text/css; charset=utf-8"},
     78     {"txt", "text/plain; charset=utf-8"},
     79     {"md", "text/plain; charset=utf-8"},
     80     {"c", "text/plain; charset=utf-8"},
     81     {"h", "text/plain; charset=utf-8"},
     82     {"gz", "application/x-gtar"},
     83     {"tar", "application/tar"},
     84     {"pdf", "application/x-pdf"},
     85     {"png", "image/png"},
     86     {"gif", "image/gif"},
     87     {"jpeg", "image/jpg"},
     88     {"jpg", "image/jpg"},
     89     {"iso", "application/x-iso9660-image"},
     90     {"webp", "image/webp"},
     91     {"svg", "image/svg+xml; charset=utf-8"},
     92     {"flac", "audio/flac"},
     93     {"mp3", "audio/mpeg"},
     94     {"ogg", "audio/ogg"},
     95     {"mp4", "video/mp4"},
     96     {"ogv", "video/ogg"},
     97     {"webm", "video/webm"},
     98 };
     99 
    100 const char *const get_mime(const char *file) {
    101     const char *ext = file;
    102     for (; *ext++;) // Move ext to end of string
    103         ;
    104     for (; ext != file && *(ext - 1) != '.';
    105          ext--) // Move ext back until we find a dot
    106         ;
    107     if (file == ext)
    108         goto ret_default; // If there is no dot then there is no file
    109                           // extension.
    110 
    111     for (size_t i = 0; i < sizeof(mimes) / sizeof(mimes[0]) - 1; i++)
    112         if (0 == strcmp(mimes[i].ext, ext))
    113             return mimes[i].type;
    114 
    115 ret_default:
    116     return "application/octet-stream";
    117 }
    118 
    119 const char *const status_code_to_error_message(uint16_t status_code) {
    120     switch (status_code) {
    121     case 400:
    122         return "400 Bad Request";
    123     case 404:
    124         return "404 File Not Found";
    125     case 200:
    126     default:
    127         return "200 OK";
    128     }
    129 }
    130 
    131 void connection_handler(int socket_desc) {
    132     PLEDGE("stdio rpath", "");
    133     char recv_buffer[MAX_BUFFER];
    134     int status_code = 200;
    135 
    136     // We can ignore SIGPIPE as we already have checks
    137     // that would deal with this.
    138     COND_PERROR_EXP(SIG_ERR == signal(SIGPIPE, SIG_IGN), "signal",
    139                     goto cleanup);
    140 
    141     // Ensure that we timeout should the send/recv take too long.
    142     struct timeval timeout;
    143     timeout.tv_sec = TIMEOUT_SECOND;
    144     timeout.tv_usec = TIMEOUT_USECOND;
    145     COND_PERROR_EXP(-1 == setsockopt(socket_desc, SOL_SOCKET, SO_RCVTIMEO,
    146                                      &timeout, sizeof(timeout)),
    147                     "setsockopt", goto cleanup);
    148     COND_PERROR_EXP(-1 == setsockopt(socket_desc, SOL_SOCKET, SO_SNDTIMEO,
    149                                      &timeout, sizeof(timeout)),
    150                     "setsockopt", goto cleanup);
    151 
    152     ssize_t recv_size;
    153     COND_PERROR_EXP(
    154         -1 == (recv_size = recv(socket_desc, recv_buffer, MAX_BUFFER - 1, 0)),
    155         "recv", goto cleanup);
    156     // Null terminate the request.
    157     recv_buffer[recv_size] = 0;
    158 
    159     char *filename = recv_buffer;
    160     // Get to the second argument in the buffer.
    161     for (; *filename && *filename++ != ' ';)
    162         ;
    163 
    164     // If we had only one argument then just provide 400.html.
    165     if (!(*filename)) {
    166         filename = "/400.html";
    167         status_code = 400;
    168         goto skip_filename_parse;
    169     }
    170 
    171     int enter_directory = 0;
    172     uint16_t i;
    173     for (i = 0; filename[i] && ' ' != filename[i] && '\n' != filename[i] &&
    174                 '\r' != filename[i];
    175          i++)
    176         ;
    177 
    178     filename[i] = 0;
    179 
    180     struct stat statbuf;
    181     if (-1 == stat(filename, &statbuf)) {
    182         if (ENOENT == errno)
    183             goto not_found;
    184         goto cleanup;
    185     }
    186     if (S_ISDIR(statbuf.st_mode)) {
    187         enter_directory = 1;
    188         chdir(filename);
    189         filename = "index.html";
    190     }
    191 
    192     int fd;
    193     char *const_site_content;
    194 skip_filename_parse:
    195 redo:
    196     const_site_content = NULL;
    197     if (-1 == (fd = open(filename, O_RDONLY))) {
    198         if (1 == enter_directory) {
    199             enter_directory = 2;
    200             goto write;
    201         }
    202 
    203         if (400 == status_code) {
    204             const_site_content = DEFAULT_400_SITE;
    205             goto write;
    206         }
    207 
    208         if (0 == strcmp(filename, "/404.html") && 404 == status_code) {
    209             const_site_content = DEFAULT_404_SITE;
    210             goto write;
    211         }
    212 
    213     not_found:
    214         filename = "/404.html";
    215         status_code = 404;
    216         goto redo;
    217     }
    218 
    219 write:
    220     if (0 >
    221         dprintf(socket_desc,
    222                 "HTTP/1.0 %s\r\nContent-Type: %s\r\nServer: smol_http\r\n\r\n",
    223                 status_code_to_error_message(status_code),
    224                 get_mime(filename))) {
    225         puts("dprintf error");
    226         goto cleanup;
    227     }
    228 
    229     if (const_site_content) {
    230         PLEDGE("stdio", NULL);
    231         COND_PERROR_EXP(-1 == write(socket_desc, const_site_content,
    232                                     strlen(const_site_content)),
    233                         "write",
    234                         /*NOP*/);
    235         goto cleanup;
    236     }
    237 
    238     // Should ./index.html be unable to be read we create a
    239     // directory listing.
    240     if (2 == enter_directory) {
    241         // Get the directory contents and provide that to the client.
    242         DIR *d;
    243         COND_PERROR_EXP(NULL == (d = opendir(".")), "opendir", goto cleanup);
    244 
    245         char current_path[PATH_MAX];
    246         char back_path[PATH_MAX];
    247         COND_PERROR_EXP(!realpath(".", current_path), "realpath",
    248                         goto directory_cleanup)
    249         COND_PERROR_EXP(!realpath("..", back_path), "realpath",
    250                         goto directory_cleanup)
    251         if (0 > dprintf(socket_desc,
    252                         "Index of %s/<br><a href='%s'>./</a><br><a "
    253                         "href='%s'>../</a><br>",
    254                         current_path, current_path, back_path)) {
    255             puts("dprintf error");
    256             goto directory_cleanup;
    257         }
    258         for (struct dirent *dir; (dir = readdir(d));) {
    259             if (0 == strcmp(dir->d_name, ".") || 0 == strcmp(dir->d_name, ".."))
    260                 continue;
    261 
    262             char tmp_path[PATH_MAX];
    263             COND_PERROR_EXP(!realpath(dir->d_name, tmp_path), "realpath",
    264                             break);
    265             if (0 > dprintf(socket_desc, "<a href='%s'>%s%s</a><br>", tmp_path,
    266                             dir->d_name, (DT_DIR == dir->d_type) ? "/" : "")) {
    267                 puts("dprintf error");
    268                 break;
    269             }
    270         }
    271     directory_cleanup:
    272         closedir(d);
    273         goto cleanup;
    274     }
    275     PLEDGE("stdio", NULL);
    276 
    277 #if HAS_SENDFILE
    278     struct stat buf;
    279     COND_PERROR_EXP(-1 == fstat(fd, &buf), "fstat", goto fd_end);
    280     COND_PERROR_EXP(-1 == sendfile(socket_desc, fd, 0, buf.st_size), "sendfile",
    281                     /*NOP*/);
    282 fd_end:
    283 #else
    284     char rwbuf[4096];
    285     for (int l; 0 != (l = read(fd, rwbuf, sizeof(rwbuf)));) {
    286         COND_PERROR_EXP(-1 == l, "read", break);
    287         COND_PERROR_EXP(-1 == write(socket_desc, rwbuf, l), "write", break);
    288     }
    289 #endif
    290     close(fd);
    291 cleanup:
    292     PLEDGE("", NULL);
    293     close(socket_desc);
    294 }
    295 
    296 int drop_root_privleges(void) {
    297     COND_PERROR_EXP(0 != seteuid(getuid()), "seteuid", return 0);
    298     COND_PERROR_EXP(0 != setegid(getgid()), "setegid", return 0);
    299     if (0 == geteuid()) {
    300         fprintf(stderr, "Error: Program can not be ran by a root user.\n");
    301         return 0;
    302     }
    303     return 1;
    304 }
    305 
    306 int init_server(short port, const char *website_root) {
    307     int socket_desc, new_socket;
    308     struct sockaddr_in server;
    309     struct sockaddr client;
    310     socklen_t c;
    311 
    312     UNVEIL(website_root, "r");
    313     // Disable usage of unveil()(this will also be
    314     // done by our pledge() call)
    315     UNVEIL(NULL, NULL);
    316 
    317     // Reap all child processes that exit
    318     signal(SIGCHLD, SIG_IGN);
    319 
    320     COND_PERROR_EXP(0 != chroot(website_root), "chroot", return 1);
    321     PLEDGE("stdio inet rpath exec id proc", "");
    322 
    323     // I am unsure if chdir("/") even can fail.
    324     // But I will keep this check here just in case.
    325     COND_PERROR_EXP(0 != chdir("/"), "chdir", return 1);
    326 
    327     COND_PERROR_EXP(-1 == (socket_desc = socket(AF_INET, SOCK_STREAM, 0)),
    328                     "socket", return 1);
    329 
    330     server.sin_family = AF_INET;
    331     server.sin_addr.s_addr = INADDR_ANY;
    332     server.sin_port = htons(port);
    333 
    334     COND_PERROR_EXP(
    335         0 > bind(socket_desc, (struct sockaddr *)&server, sizeof(server)),
    336         "bind", return 1);
    337 
    338     // Everything that requires root privleges is done,
    339     // we can now drop privleges.
    340     if (!drop_root_privleges()) {
    341         fprintf(stderr, "Unable to drop privleges.\n");
    342         return 1;
    343     }
    344 
    345     PLEDGE("stdio inet rpath exec proc", NULL);
    346 
    347     COND_PERROR_EXP(0 != listen(socket_desc, 3), "listen", return 1);
    348 
    349     c = sizeof(struct sockaddr_in);
    350     for (; (new_socket = accept(socket_desc, &client, &c));) {
    351         COND_PERROR_EXP(-1 == new_socket, "accept", continue);
    352 
    353         // Create a child and handle the connection
    354         pid_t pid;
    355         COND_PERROR_EXP(-1 == (pid = fork()), "fork", continue);
    356         if (0 != pid) // We are the parent.
    357         {
    358             close(new_socket);
    359             continue;
    360         }
    361         // We are the child.
    362         connection_handler(new_socket);
    363         close(socket_desc);
    364         _exit(0);
    365     }
    366     close(socket_desc);
    367     return 0;
    368 }
    369 
    370 void usage(const char *const str) {
    371     fprintf(stderr,
    372             "Usage: %s [-p PORT] [-d Website root directory] -h(Print this "
    373             "message)\n",
    374             str);
    375 }
    376 
    377 int main(int argc, char **argv) {
    378     if (0 != geteuid()) {
    379         fprintf(stderr, "Error: Program does not have root privleges.");
    380         return 1;
    381     }
    382 
    383     short port = DEFAULT_PORT;
    384     char *website_root = WEBSITE_ROOT;
    385     for (int ch; - 1 != (ch = getopt(argc, argv, "p:d:h"));)
    386         switch ((char)ch) {
    387         case 'p':
    388             if (0 == (port = atoi(optarg))) {
    389                 usage(argv[0]);
    390                 return 0;
    391             }
    392             break;
    393         case 'd':
    394             website_root = optarg;
    395             break;
    396         case '?':
    397         case ':':
    398         case 'h':
    399             usage(argv[0]);
    400             return 0;
    401         }
    402 
    403     return init_server(port, website_root);
    404 }