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(¶ms, "=");
if (!params) break;
char *value = strsep(¶ms, "&");
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;
}
|