Download
239 lines 8.5 KiBCModified 2020-09-18 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
// Minserv - A super minimal web server
// Minserv gets HTTP requests piped in stdin and prints HTTP response to stdout.
// It uses some simple lookup rules and populates environment variables to be
// used by other scripts.
// Filetypes can be custom handled by /.<extension>, which receives the filename
// as the first argument (e.g. /var/www/.md /var/www/foo.md)

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "utils.h"

#define VERSION "0.3.0"
#define NO_CONTENT -1
#define CHUNKED_CONTENT -2

static const char *usage =
    "minserv - A minimalist server\n"
    "Usage: minserv [-h|--help] [-v|--version] [-l|--log=<logfile>] <handler> [handler args...]\n";
#define LOGPREFIX "[mins] "
#define log(...) dprintf(STDERR_FILENO, LOGPREFIX __VA_ARGS__)

static int percent_decode(char* out, const char* in)
{
    // Values 1-16 (16 used as a sentinel for '0')
    static const char tbl[256] = {
        ['0']=0x10, ['1']=0x1, ['2']=0x2, ['3']=0x3, ['4']=0x4,
        ['5']=0x5, ['6']=0x6, ['7']=0x7, ['8']=0x8, ['9']=0x9,
        ['a']=0xa, ['A']=0xA, ['b']=0xb, ['B']=0xB,
        ['c']=0xc, ['C']=0xC, ['d']=0xd, ['D']=0xD,
        ['e']=0xe, ['E']=0xE, ['f']=0xf, ['F']=0xF,
    };
    char c, v1, v2, *p=out;
    if (in != NULL) {
        while ((c=*in++) != '\0') {
            if (c == '%') {
                if (!(v1=tbl[(unsigned char)*in++]) || 
                    !(v2=tbl[(unsigned char)*in++])) {
                    *out = '\0';
                    return -1;
                }
                c = (v1<<4)|(v2 & 0xF); // `v2 & 0xF` maps 1-16 -> 0-15
            }
            *p++ = c;
        }
    }
    *p = '\0';
    return 0;
}

// WARNING: This function leaks memory because setenv() requires values remain
// in memory.
static void set_params(const char *method, char *param_str)
{
    char *params = memcheck(strdup(param_str));
    char *cr = strrchr(params, '\r');
    if (cr) *cr = '\0';
    while (params && params[0]) {
        char *key = strsep(&params, "=");
        if (!params) break;
        char *value = strsep(&params, "&");
        assert(percent_decode(value, value) == 0,
               LOGPREFIX "Improperly encoded URL");
        char *method_key = memcheck(calloc(strlen(method)+1+strlen(key)+1, sizeof(char)));
        assert(sprintf(method_key, "%s_%s", method, key) > 0,
               LOGPREFIX "GET parameter allocation failure");
        log("[%-4s] %s = '%s'\n", method, key, value);
        assert(setenv(method_key, value, 1) == 0,
               LOGPREFIX "Failure to set env var");
    }
}

int main(int argc, char *argv[])
{
    char **handler = NULL;
    char *logpath = NULL;
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--log") == 0) {
            logpath = argv[++i];
        } else if (startswith(argv[i], "--log=")) {
            logpath = &argv[i][strlen("--log=")];
        } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
            write(STDOUT_FILENO, usage, strlen(usage));
            return 0;
        } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
            printf("%s\n", VERSION);
            return 0;
        } else if (argv[i][0] == '-') {
            write(STDOUT_FILENO, usage, strlen(usage));
            return 1;
        } else if (handler == NULL) {
            handler = &argv[i];
            break;
        }
    }

    if (!handler) {
        write(STDOUT_FILENO, usage, strlen(usage));
        return 1;
    }

    if (logpath) {
        int logfd = open(logpath, O_WRONLY | O_APPEND | O_CREAT, 0644);
        assert(logfd >= 0, LOGPREFIX "Couldn't open log file");
        dup2(logfd, STDERR_FILENO);
    }

    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    assert(fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == 0,
           LOGPREFIX "Error setting stdin to nonblocking");

    log("=============================== BEGIN REQUEST ==================================\n");

    char *method, *url, *http;
    {
        char *linebuf = NULL;
        size_t linesize = 0;
        assert(getline(&linebuf, &linesize, stdin) != -1,
               LOGPREFIX "Improperly formatted header");

        method = strsep(&linebuf, " ");
        assert(linebuf != NULL, LOGPREFIX "Improperly formatted header");
        url = strsep(&linebuf, " ");
        assert(linebuf != NULL, LOGPREFIX "Improperly formatted header");
        http = strsep(&linebuf, "\r\n");
        assert(linebuf != NULL, LOGPREFIX "Improperly formatted header");
    }
    log("[head] %s %s %s\n", method, url, http);

    assert(setenv("METHOD", method, 1) == 0, LOGPREFIX "Error setting $METHOD");

    // Set URL GET parameter env vars:
    char *params = strchr(url, '?');
    if (params) {
        *(params++) = '\0';
        set_params("GET", params);
    }
    assert(setenv("URL", url, 1) == 0, LOGPREFIX "Error setting $URL");

    // Parse the rest of the header:
    char *linebuf = NULL;
    size_t linesize = 0;
    long int content_length = NO_CONTENT;
    for (;;) {
        if (getline(&linebuf, &linesize, stdin) == -1) break;
        memcheck(linebuf);
        char *cr = strrchr(linebuf, '\r');
        if (cr) *cr = '\0';
        if (linebuf[0] == '\0') break;
        log("[head] %s\n", linebuf);
        if (startswith(linebuf, "Content-Length: ")
            && content_length != CHUNKED_CONTENT) {
            content_length = strtol(&linebuf[strlen("Content-Length: ")+1], NULL, 10);
        } else if (startswith(linebuf, "Transfer-Encoding: ")
                   && strcmp(linebuf, "Transfer-Encoding: identity") != 0) {
            content_length = CHUNKED_CONTENT;
        } else if (startswith(linebuf, "Cookie: ")) {
            set_params("COOKIE", &linebuf[strlen("Cookie: ")+1]);
        }
    }

    log("Finished header.\n");

    // Set POST and PUT env vars:
    if (strcmp(method, "POST") == 0) {
        for (;;) {
            log("doop.\n");
            if (getline(&linebuf, &linesize, stdin) == -1) break;
            memcheck(linebuf);
            char *cr = strrchr(linebuf, '\r');
            if (cr) *cr = '\0';
            if (linebuf[0] == '\0') break;
            log("Setting params: %s\n", linebuf);
            set_params("POST", linebuf);
        }
    } else if (strcmp(method, "PUT") == 0) {
        if (content_length > 0) {
            char *put = memcheck(calloc((size_t)content_length + 1, sizeof(char)));
            assert(read(STDIN_FILENO, put, (size_t)(content_length)) == content_length,
                   LOGPREFIX "Mismatch in PUT content length");
            assert(setenv("PUT", put, 1) == 0, LOGPREFIX "Error setting $PUT");
        } else if (content_length == CHUNKED_CONTENT) {
            char *put = NULL;
            size_t put_size = 0;
            size_t chunk;
            while (scanf("%lu\r\n", &chunk) == 1) {
                put = memcheck(realloc(put, put_size + chunk + 2));
                assert(read(STDIN_FILENO, &put[put_size], chunk + 2) == (ssize_t)(chunk + 2),
                       LOGPREFIX "Failure reading chunked PUT data");
                put_size += chunk;
                put[put_size] = '\0';
            }
        }
    }

    // Run the handler:
	log("[handler] %s\n", *handler);
    log("--------------------------------------------------------------------------------\n");
    pid_t child;
    if ((child=fork()) == 0) {
        assert(execvp(*handler, handler) == 0,
               LOGPREFIX "Error spawning child");
    }
    int child_status;
    assert(waitpid(child, &child_status, 0) == child,
           LOGPREFIX "Error waiting for child");

    // Emergency fallback 500 response:
    if (child_status != 0) {
        log("Child process failed.\n");
        const char *err500 =
            "HTTP/1.1 500 Internal Server Error\r\n"
            "Content-Type: text/html; charset=utf-8\r\n"
            "Content-Length: 241\r\n"
            "\r\n"
            "<!DOCTYPE html>\r\n"
            "<html>\r\n"
            "	<head>\r\n"
            "		<title>500 - Internal Server Error</title>\r\n"
            "	</head>\r\n"
            "	<body>\r\n"
            "		<h1>500 - Internal Server Error</h1>\r\n"
            "		<p>\r\n"
            "		An internal server error occurred while processing your request.\r\n"
            "		</p>\r\n"
            "	</body>\r\n"
            "</html>\r\n";
        write(STDOUT_FILENO, err500, strlen(err500));
    }
    
    log("================================ END REQUEST ===================================\n");

    return 0;
}