diff --git a/Makefile b/Makefile index b1ffb72..e648f37 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ CFLAGS = -DFNORD='"eris/$(VERSION)"' -Wall -Werror all: eris -eris: eris.c strings.c mime.c time.c +eris: eris.c strings.c mime.c time.c cgi.c $(CC) $(CFLAGS) -o $@ $< test: eris diff --git a/cgi.c b/cgi.c new file mode 100644 index 0000000..e66dc9d --- /dev/null +++ b/cgi.c @@ -0,0 +1,179 @@ +void +sigchld(int sig) +{ + while (waitpid(0, NULL, WNOHANG) > 0); +} + +static void +cgi_child(const char *relpath) +{ + setenv("GATEWAY_INTERFACE", "CGI/1.1", 1); + setenv("SERVER_SOFTWARE", FNORD, 1); + setenv("REQUEST_URI", path, 1); + setenv("SERVER_NAME", host, 1); + setenv("SCRIPT_NAME", relpath, 1); + setenv("REMOTE_ADDR", remote_ip, 1); + setenv("REMOTE_PORT", remote_port, 1); + setenv("REMOTE_IDENT", remote_ident, 1); + setenv("CONTENT_TYPE", content_type, 1); + { + char cl[20]; + + snprintf(cl, sizeof cl, "%llu", (unsigned long long) content_length); + setenv("CONTENT_LENGTH", cl, 1); + } + + execl(relpath, relpath, NULL); + exit(1); +} + +void +cgi_parent(int cin, int cout) +{ + FILE *cinf = fdopen(cin, "rb"); + int passthru = nph; + + fcntl(child_in, F_SETFL, O_NONBLOCK); + signal(SIGCHLD, sigchld); + signal(SIGPIPE, SIG_IGN); /* NO! no signal! */ + + /* Eris is not this smart yet */ + keepalive = 0; + + while (1) { + int nfds; + fd_set rfds, wfds; + + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_SET(cin[0], &rfds); + nfds = cin[0]; + + if (post_len) { + /* have post data */ + FD_SET(cout[1], &wfds); + if (cout[1] > nfds) { + nfds = cout[1]; + } + } else if (cout[1] >= 0) { + close(cout[1]); /* no post data */ + cout[1] = -1; + } + + if (-1 == select(nfds+1, &rfds, &wfds, NULL, NULL)) { + break; + } + + if (FD_ISSET(cin[0], &rfds)) { + if (passthru) { + size_t len; + + /* Re-use this big buffer */ + len = fread(cgiheader, 1, sizeof cgiheader, cinf); + if (0 == len) { + /* CGI is done */ + break; + } + fwrite(cgiheader, 1, len, stdout); + size += len; + } else { + int ret; + + ret = read_header(cinf, cgiheader, &cgiheaderlen); + if (0 == ret) { + /* Call read_header again */ + } else if (-1 == ret) { + /* EOF or error */ + badrequest(500, "CGI Error", + "CGI output too weird"); + } else { + /* Entire header is in memory now */ + passthru = 1; + + /* XXX: I think we need to look for Location: + * anywhere, but fnord got away with checking + * only the first header field, so I will too. + */ + if (memcmp(cgiheader, "Location: ", 10) == 0) { + retcode = 302; + printf + ("HTTP/1.0 302 CGI-Redirect\r\nConnection: close\r\n"); + fwrite(cgiheader, 1, cgiheaderlen, stdout); + dolog(0); + exit(0); + } + + retcode = 200; + printf("HTTP/1.0 200 OK\r\nServer: " + FNORD + "\r\nPragma: no-cache\r\nConnection: close\r\n"); + signal(SIGCHLD, SIG_IGN); + fwrite(cgiheader, 1, cgiheaderlen, stdout); + } + } + } else if (FD_ISSET(cout[1], &wfds)) { + /* + * write to cgi the post data + */ + if (post_len) { + size_t len; + char buf[BUFFER_SIZE]; + size_t nmemb = min(BUFFER_SIZE, post_len); + + len = fread(buf, 1, nmemb, stdin); + if (len < 1) { + break; + } + post_len -= len; + write(cout[1], buf, len); + } else { + close(cout[1]); + } + } + } + alarm(0); + + fflush(stdout); + dolog(size); + cork(0); +} +void +serve_cgi(char *relpath) +{ + size_t size = 0; + int pid; + char buf[BUFFER_SIZE]; + int cin[2]; + int cout[2]; + + if (pipe(cin) || pipe(cout)) { + badrequest(500, "Internal Server Error", "Server Resource problem."); + } + + pid = fork(); + if (-1 == pid) { + badrequest(500, "Internal Server Error", "Unable to fork."); + } + if (pid) { + close(cin[1]); + close(cout[0]); + + alarm(CHILD_TIMEOUT); + cgi_parent(cin[0], cout[1]); + alarm(0); + } else { + close(cwd); + close(cout[1]); + close(cin[0]); + + dup2(cout[0], 0); + dup2(cin[1], 1); + + close(cout[0]); + close(cin[1]); + + cgi_child(relpath); + } +} + + diff --git a/eris.c b/eris.c index e2896e2..ec312f5 100644 --- a/eris.c +++ b/eris.c @@ -45,10 +45,6 @@ #define DUMP_p(v) DUMPf("%s = %p", #v, v) #define DUMP_buf(v, l) DUMPf("%s = %.*s", #v, (int)(l), v) -#include "strings.c" -#include "mime.c" -#include "time.c" - /* Wait this long (seconds) for a valid HTTP request */ #define READTIMEOUT 2 @@ -77,6 +73,7 @@ int redirect = 0; int portappend = 0; /* Variables that persist between requests */ +int cwd; int keepalive = 0; char *remote_ip = NULL; char *remote_port = NULL; @@ -93,8 +90,8 @@ char *user_agent; char *refer; char *path; int http_version; +char *content_type; size_t content_length; -char *query_string = NULL; off_t range_start, range_end; time_t ims = 0; @@ -102,6 +99,11 @@ time_t ims = 0; #define BUFFER_SIZE 8192 char stdout_buf[BUFFER_SIZE]; +#include "strings.c" +#include "mime.c" +#include "time.c" +#include "cgi.c" + /* * TCP_CORK is a Linux extension to work around a TCP problem. * http://www.baus.net/on-tcp_cork has a good description. @@ -496,6 +498,7 @@ handle_request() path = NULL; range_start = 0; range_end = 0; + content_type = NULL; content_length = 0; alarm(READTIMEOUT); @@ -520,10 +523,16 @@ handle_request() badrequest(405, "Method Not Allowed", "Unsupported HTTP method."); } + if (docgi) { + p[-2] = 0; + setenv("REQUEST_METHOD", p, 1); + } + /* Interpret path into fspath. */ path = p - 1; { FILE *f = fmemopen(fspath, sizeof fspath, "w"); + char *query_string = NULL; fprintf(f, "./"); for (; *p != ' '; p += 1) { @@ -561,8 +570,13 @@ handle_request() } fputc(0, f); fclose(f); + + *(p++) = 0; /* NULL-terminate path */ + + if (docgi && query_string) { + setenv("QUERY_STRING", query_string, 1); + } } - *(p++) = 0; /* NULL-terminate path */ http_version = -1; if (! strncmp(p, "HTTP/1.", 7) && p[8] && ((p[8] == '\r') || (p[8] == '\n'))) { @@ -577,6 +591,9 @@ handle_request() } else { keepalive = 0; } + if (docgi) { + setenv("SERVER_PROTOCOL", p, 1); + } /* Read header fields */ { @@ -617,7 +634,9 @@ handle_request() } /* Set up CGI environment variables */ - setenv(cgi_name, val, 1); + if (docgi) { + setenv(cgi_name, val, 1); + } /* By default, re-use buffer space */ base = cgi_name; @@ -632,6 +651,11 @@ handle_request() } else if (! strcmp(name, "REFERER")) { refer = val; base = name + len + 1; + } else if (! strcmp(name, "CONTENT_TYPE")) { + content_type = val; + base = name + len + 1; + } else if (! strcmp(name, "CONTENT_LENGTH")) { + content_length = (size_t) strtoull(val, NULL, 10); } else if (! strcmp(name, "CONNECTION")) { if (! strcasecmp(val, "keep-alive")) { keepalive = 1; @@ -640,8 +664,6 @@ handle_request() } } else if (! strcmp(name, "IF_MODIFIED_SINCE")) { ims = timerfc(val); - } else if (! strcmp(name, "CONTENT_LENGTH")) { - content_length = (size_t) strtoull(val, NULL, 10); } else if (! strcmp(name, "RANGE")) { /* Range: bytes=17-23 */ /* Range: bytes=23- */ @@ -700,9 +722,10 @@ handle_request() int main(int argc, char *argv[], const char *const *envp) { - int cwd = open(".", O_RDONLY); parse_options(argc, argv); + cwd = open(".", O_RDONLY); + setbuffer(stdout, stdout_buf, sizeof stdout_buf); signal(SIGPIPE, SIG_IGN); diff --git a/mime.c b/mime.c new file mode 100644 index 0000000..869ca82 --- /dev/null +++ b/mime.c @@ -0,0 +1,65 @@ +static struct mimeentry { + const char *name, + *type; +} mimetab[] = { + { + "html", "text/html; charset=UTF-8"}, { + "htm", "text/html; charset=UTF-8"}, { + "txt", "text/plain; charset=UTF-8"}, { + "css", "text/css"}, { + "ps", "application/postscript"}, { + "pdf", "application/pdf"}, { + "js", "application/javascript"}, { + "gif", "image/gif"}, { + "png", "image/png"}, { + "jpeg", "image/jpeg"}, { + "jpg", "image/jpeg"}, { + "svg", "image/svg+xml"}, { + "mpeg", "video/mpeg"}, { + "mpg", "video/mpeg"}, { + "avi", "video/x-msvideo"}, { + "mov", "video/quicktime"}, { + "qt", "video/quicktime"}, { + "mp3", "audio/mpeg"}, { + "ogg", "audio/ogg"}, { + "wav", "audio/x-wav"}, { + "epub", "application/epub+zip"}, { + "dvi", "application/x-dvi"}, { + "pac", "application/x-ns-proxy-autoconfig"}, { + "sig", "application/pgp-signature"}, { + "swf", "application/x-shockwave-flash"}, { + "torrent", "application/x-bittorrent"}, { + "tar", "application/x-tar"}, { + "zip", "application/zip"}, { + "dtd", "text/xml"}, { + "xml", "text/xml"}, { + "xbm", "image/x-xbitmap"}, { + "xpm", "image/x-xpixmap"}, { + "xwd", "image/x-xwindowdump"}, { + "ico", "image/x-icon"}, { + 0, 0}}; + +static const char *default_mimetype = "application/octet-stream"; + +/* + * Determine MIME type from file extension + */ +static const char * +getmimetype(char *url) +{ + char *ext = strrchr(url, '.'); + + + if (ext) { + int i; + + ext++; + for (i = 0; mimetab[i].name; ++i) { + if (!strcmp(mimetab[i].name, ext)) { + return mimetab[i].type; + } + } + } + return default_mimetype; +} + diff --git a/strings.c b/strings.c new file mode 100644 index 0000000..0f89a93 --- /dev/null +++ b/strings.c @@ -0,0 +1,130 @@ +int +endswith(char *haystack, char *needle) +{ + char *h, *n; + + for (h = haystack; *h; h++); + for (n = needle; *n; n++); + + if (h - haystack < n - needle) { + return 0; + } + + while (n >= needle) { + if (*(n--) != *(h--)) { + return 0; + } + } + + return 1; +} + +/** Replace whitespace with underscores for logging */ +static void +sanitize(char *s) +{ + if (!s) { + return; + } + for (; *s; s += 1) { + if (isspace(*s)) { + *s = '_'; + } + } +} + +/** Parse a header out of a line. + * + * This capitalizes the header name, and strips trailing [\r\n] + * Returns the length of the line (after stripping) + */ +size_t +extract_header_field(char *buf, char **val, int cgi) +{ + size_t len; + + *val = NULL; + + for (len = 0; buf[len]; len += 1) { + if (! *val) { + if (buf[len] == '\n') { + /* Blank line or incorrectly-formatted header */ + return 0; + } else if (buf[len] == ':') { + buf[len] = 0; + for (*val = &(buf[len+1]); **val == ' '; *val += 1); + } else if (cgi) { + switch (buf[len]) { + case 'a'...'z': + buf[len] ^= ' '; + break; + case 'A'...'Z': + case '0'...'9': + case '\r': + case '\n': + break; + default: + buf[len] = '_'; + break; + } + } + } + } + + for (; (buf[len-1] == '\n') || (buf[len-1] == '\r'); len -= 1); + buf[len] = 0; + + return len; +} + + +int +fromhex(int c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + else { + c |= ' '; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + } + return -1; +} + +void +html_esc(FILE *f, char *s) +{ + for (; *s; s += 1) { + switch (*s) { + case '<': + fprintf(f, "<"); + break; + case '>': + fprintf(f, ">"); + break; + case '&': + fprintf(f, "&"); + break; + default: + fputc(*s, f); + break; + } + } +} + +void +url_esc(FILE *f, char *s) +{ + for (; *s; s += 1) { + switch (*s) { + case '%': + case 127: + case -127 ... 31: + fprintf(f, "%%%02x", *s); + break; + default: + fputc(*s, f); + break; + } + } +} diff --git a/time.c b/time.c new file mode 100644 index 0000000..2b7053a --- /dev/null +++ b/time.c @@ -0,0 +1,202 @@ +/* + * Code in this file is from mathopd + * + * Copyright 1996, Michiel Boland. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +static const char days[] = "SunMonTueWedThuFriSat"; +static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + +static time_t +timerfc(const char *s) +{ + static const int daytab[2][12] = { + {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}, + {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335} + }; + unsigned sec, + min, + hour, + day, + mon, + year; + char month[3]; + int c; + unsigned n; + char flag; + char state; + char isctime; + enum { D_START, D_END, D_MON, D_DAY, D_YEAR, D_HOUR, D_MIN, D_SEC }; + + sec = 60; + min = 60; + hour = 24; + day = 32; + year = 1969; + isctime = 0; + month[0] = 0; + state = D_START; + n = 0; + flag = 1; + do { + c = *s++; + switch (state) { + case D_START: + if (c == ' ') { + state = D_MON; + isctime = 1; + } else if (c == ',') + state = D_DAY; + break; + case D_MON: + if (isalpha(c)) { + if (n < 3) + month[n++] = c; + } else { + if (n < 3) + return -1; + n = 0; + state = isctime ? D_DAY : D_YEAR; + } + break; + case D_DAY: + if (c == ' ' && flag); + else if (isdigit(c)) { + flag = 0; + n = 10 * n + (c - '0'); + } else { + day = n; + n = 0; + state = isctime ? D_HOUR : D_MON; + } + break; + case D_YEAR: + if (isdigit(c)) + n = 10 * n + (c - '0'); + else { + year = n; + n = 0; + state = isctime ? D_END : D_HOUR; + } + break; + case D_HOUR: + if (isdigit(c)) + n = 10 * n + (c - '0'); + else { + hour = n; + n = 0; + state = D_MIN; + } + break; + case D_MIN: + if (isdigit(c)) + n = 10 * n + (c - '0'); + else { + min = n; + n = 0; + state = D_SEC; + } + break; + case D_SEC: + if (isdigit(c)) + n = 10 * n + (c - '0'); + else { + sec = n; + n = 0; + state = isctime ? D_YEAR : D_END; + } + break; + } + } while (state != D_END && c); + switch (month[0]) { + case 'A': + mon = (month[1] == 'p') ? 4 : 8; + break; + case 'D': + mon = 12; + break; + case 'F': + mon = 2; + break; + case 'J': + mon = (month[1] == 'a') ? 1 : ((month[2] == 'l') ? 7 : 6); + break; + case 'M': + mon = (month[2] == 'r') ? 3 : 5; + break; + case 'N': + mon = 11; + break; + case 'O': + mon = 10; + break; + case 'S': + mon = 9; + break; + default: + return -1; + } + if (year <= 100) + year += (year < 70) ? 2000 : 1900; + --mon; + --day; + if (sec >= 60 || min >= 60 || hour >= 60 || day >= 31) + return -1; + if (year < 1970) + return 0; + return sec + 60L * (min + 60L * (hour + 24L * (day + + daytab[year % 4 == 0 + && (year % 100 + || year % + 400 == + 0)][mon] + + 365L * (year - 1970L) + + ((year - + 1969L) >> 2)))); +} + +char * +rfctime(time_t t, char *buf) +{ + struct tm *tp; + + /* ntp: in glibc, this triggers a bunch of needless I/O. */ + tp = gmtime(&t); + if (tp == 0) { + strcpy(buf, "?"); + return buf; + } + strftime(buf, 31, "%a, %d %b %Y %H:%M:%S GMT", tp); + return buf; +}