diff --git a/CHANGES b/CHANGES index 5e091fd..8234630 100644 --- a/CHANGES +++ b/CHANGES @@ -1,17 +1,17 @@ -2.0: - Major modifications - Replace libowfat with libc - Replace buffer_1 and buffer_2 with stdio - Fix directory listing of / SEGV - Replace compile-time options with command-line ones +2: + Rename to "eris httpd" to acknowledge fork Add regression test suite - -1.11: + Replace compile-time options with command-line ones + Fix segfault with directory listing of / + Replace buffer_1 and buffer_2 with stdio + Replace libowfat with libc + Add all patches from (defunct) Debian package Fix if-modified-since date parsing Make text content-types use charset=UTF-8 Change default content-type to application/octet-stream Makefile no longer overrides CC and CPP from parent makes Don't send Content-type if there's no content + New maintainer: Neale Pickett 1.10: have fallback in case sendfile fails diff --git a/Makefile b/Makefile index 126306c..2a910dd 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ VERSION := $(shell head -n 1 CHANGES | tr -d :) -CFLAGS = -DFNORD='"fnord/$(VERSION)"' -Wall -Werror +CFLAGS = -DFNORD='"eris/$(VERSION)"' -Wall -Werror -all: fnord +all: eris -test: fnord +test: eris cd tests && python3 ./test.py clean: - rm -f *.[oa] fnord + rm -f *.[oa] eris diff --git a/README b/README index 9673185..707d161 100644 --- a/README +++ b/README @@ -1,3 +1,20 @@ +Eris HTTPD is a part of Dirtbags Capture The Flag +(http://dirtbags.net/ctf/). As I was adding more and more patches +against fnord 1.10 (http://www.fefe.de), I decided to fork fnord into +a new project. Fnord's author, Fefe, approved of the fork. + +The differences between eris and fnord are: + +* command-line arguments instead of compile-time defines +* eliminated use of libowfat +* no build dependency of dietlibc +* elimination of "old style symlink handling" +* elimination of user switching (use tcpserver) +* elimination of chroot code (use chroot) +* several bugfixes (which have been sent to the fnord mail list) + +---- + Usage: tcpserver -v -RHl localhost -u 1234 -g 1234 0 80 ./httpd @@ -13,13 +30,13 @@ user agent with spaces replaced by underscores, the next token (none) is the Referer HTTP header or "none" if none was given, and the rest of each line is the decoded requested URL. -fnord-httpd does simple virtual hosting. If the Host: HTTP header is -there, fnord will try to chdir to a directory of that name, i.e. if the -client asks for "/" on host "www.fefe.de:80", fnord will look for -"www.fefe.de:80/index.html". Fnord will also try the directory +eris httpd does simple virtual hosting. If the Host: HTTP header is +there, eris will try to chdir to a directory of that name, i.e. if the +client asks for "/" on host "www.fefe.de:80", eris will look for +"www.fefe.de:80/index.html". Eris will also try the directory "default" if no specific directory for the virtual host was there. If -the directory is a dangling symlink and fnord was compiled with --DREDIRECT (default), fnord will redirect the whole site. Examples: +the directory is a dangling symlink and eris was compiled with +-DREDIRECT (default), eris will redirect the whole site. Examples: lrwxrwxrwx 1 leitner users 19 May 5 01:09 www.foo.de:80 -> http://www.baz.de/ lrwxrwxrwx 1 leitner users 20 May 5 01:12 www.bar.de:80 -> =http://www.baz.de/ @@ -27,35 +44,35 @@ the directory is a dangling symlink and fnord was compiled with http://www.foo.de/blub.html will be redirected to http://www.baz.de/blub.html. http://www.bar.de/blub.html will be redirected to http://www.baz.de/. -fnord implements el-cheapo HTTP ranges (only byte ranges and only of the +eris implements el-cheapo HTTP ranges (only byte ranges and only of the form x-y, not multiple ranges). -fnord implements content type matching and Accepts: parsing, but the +eris implements content type matching and Accepts: parsing, but the content type table is compiled in, i.e. to change it, you have to change the source code. Shouldn't be a problem because you _have_ the source code ;) -fnord implements HTTP redirection. If a file is not found, but a -dangling symlink is there under the same name, fnord will issue a +eris implements HTTP redirection. If a file is not found, but a +dangling symlink is there under the same name, eris will issue a redirection to the contents of that symlink. To be RFC compliant, the symlink must point to a full URL, i.e. ln -s ftp://foobar.math.fu-berlin.de/pub/dietlibc/dietlibc-0.11.tar.bz2 dietlibc-0.11.tar.bz2 -fnord implements in-place substitution of * to *.gz +eris implements in-place substitution of * to *.gz if the file is available and the client supports the mime-type and content-encoding. That means you can save substantial bandwidth by having an index.html.gz for each index.html, as most clients can transparently decode gzipped files. -fnord will change dots at the start of file or directory names to colons +eris will change dots at the start of file or directory names to colons in the query before trying to answer them. -fnord understands and implements keep-alive connections. +eris understands and implements keep-alive connections. -fnord can use sendfile on Linux to enable zero-copy TCP. +eris can use sendfile on Linux to enable zero-copy TCP. -If fnord is given the -c option, it will regard files +If eris is given the -c option, it will regard files whose names end with ".cgi" as CGI programs and try to execute them. CGI programs starting with "nph-" will be handled as no-parse-header CGIs. Please see http://hoohoo.ncsa.uiuc.edu/cgi/interface.html for the diff --git a/fnord.c b/eris.c similarity index 95% rename from fnord.c rename to eris.c index 0cf8fff..507d181 100644 --- a/fnord.c +++ b/eris.c @@ -38,6 +38,7 @@ #define DUMP_s(v) DUMPf("%s = %s", #v, v) #define DUMP_c(v) DUMPf("%s = %c", #v, v) #define DUMP_p(v) DUMPf("%s = %p", #v, v) +#define DUMP_buf(v, l) DUMPf("%s = %.*s", #v, l, v) /* * the following is the time in seconds that fnord should wait for a valid @@ -70,10 +71,6 @@ #include #endif -#ifndef O_NDELAY -#define O_NDELAY O_NONBLOCK -#endif - #define USE_MMAP #ifndef _POSIX_MAPPED_FILES #undef USE_MMAP @@ -219,7 +216,38 @@ elen(register const char *const *e) return i; } -char * +static ssize_t +read_header(int fd, char *buf, size_t buflen) +{ + size_t len = 0; + int found = 0; + size_t p = 0; + + while (found < 2) { + int tmp; + + tmp = read(fd, buf + len, buflen - len); + if (tmp < 0) { + return -1; + } + if (tmp == 0) { + break; + } + len += tmp; + + for (; p < len; p += 1) { + if (buf[p] == '\n') { + if (++found == 2) { + break; + } + } + } + } + + return len; +} + +char * env_append(const char *key, const char *val) { static char buf[MAXHEADERLEN * 2 + PATH_MAX + 200]; @@ -370,30 +398,37 @@ cgi_child(int sig) signal(SIGCHLD, cgi_child); } -static void +/* Convert bare \n to \r\n in header. Return 0 if + * header is over. */ +static int cgi_send_correct_http(const char *s, unsigned int sl) { unsigned int i; - char ch = 0; - for (i = 0; i < sl; ++i) { - if ((s[i] == '\r') && (s[i + 1] == '\n')) { - ++i; - goto out_nl; - } else { - if (s[i] != '\n') { - putchar(s[i]); - } else { - out_nl: - printf("\r\n"); - if (ch == '\n') { - ++i; + int newline = 0; + + for (i = 0; i < sl; i += 1) { + switch (s[i]) { + case '\r': + if (s[i + 1] == '\n') { + i += 1; + case '\n': + printf("\r\n"); + if (newline) { + fwrite(s + i + 1, sl - i - 1, 1, stdout); + return 0; + } else { + newline = 1; + } break; + } else { + default: + newline = 0; + putchar(s[i]); } - } + break; } - ch = s[i]; } - printf("%.*s", sl - i, s + i); + return 1; } static void @@ -442,17 +477,29 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp) * read from cgi */ if (pfd[0].revents & POLLIN) { - if (!(n = read(fd[0], ibuf, sizeof(ibuf)))) + size_t len; + + if (startup) { + /* XXX: could block :< */ + len = read_header(fd[0], ibuf, sizeof ibuf); + } else { + len = read(fd[0], ibuf, sizeof ibuf); + } + + if (0 == len) { break; - if (n < 0) + } + if (len == -1) { goto cgi_500; + } + /* * startup */ if (startup) { - startup = 0; if (nph) { /* NPH-CGI */ - printf("%.*s", n, ibuf); + startup = 0; + printf("%.*s", len, ibuf); /* * skip HTTP/x.x */ @@ -473,7 +520,8 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp) FNORD "\r\nPragma: no-cache\r\nConnection: close\r\n"); signal(SIGCHLD, SIG_IGN); - cgi_send_correct_http(ibuf, n); + cgi_send_correct_http(ibuf, len); + startup = 0; } } } @@ -481,9 +529,9 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp) * non startup */ else { - printf("%.*s", n, ibuf); + fwrite(ibuf, len, 1, stdout); } - size += n; + size += len; if (pfd[0].revents & POLLHUP) break; } @@ -1398,6 +1446,7 @@ serve_static_data(int fd) #endif } + int main(int argc, char *argv[], const char *const *envp) { @@ -1797,14 +1846,17 @@ main(int argc, char *argv[], const char *const *envp) } printf("Content-Length: %llu\r\n", (unsigned long long) (rangeend - rangestart)); - printf("Last-Modified: "); { - /* XXX: This parses tzinfo. It shouldn't have to. */ + /* + * glibc's gmtime parses tzinfo, resulting in 9 + * additional syscalls. uclibc doesn't do this. + * I presume dietlibc doesn't either. + */ struct tm *x = gmtime(&st.st_mtime); /* * "Sun, 06 Nov 1994 08:49:37 GMT" */ - printf("%.3s, %02d %.3s %d %02d:%02d:%02d GMT\r\n", + printf("Last-Modified: %.3s, %02d %.3s %d %02d:%02d:%02d GMT\r\n", days + (3 * x->tm_wday), x->tm_mday, months + (3 * x->tm_mon), diff --git a/tests/test.py b/tests/test.py index 5cd196d..537344f 100755 --- a/tests/test.py +++ b/tests/test.py @@ -4,8 +4,8 @@ import unittest from subprocess import * import os -def fnord(*args): - return Popen(('../fnord',) + args, +def eris(*args): + return Popen(('../eris',) + args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env={'PROTO': 'TCP', 'TCPREMOTEPORT': '5858', @@ -13,9 +13,9 @@ def fnord(*args): class ArgTests(unittest.TestCase): def check_index(self, *args): - p = fnord(*args) + p = eris(*args) so, se = p.communicate(b'GET / HTTP/1.0\r\n\r\n') - self.assertRegex(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 6\r\nLast-Modified: (Mon|Tue|Wed|Thu|Fri|Sat|Sun), .. (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) 2... ..:..:.. GMT\r\n\r\njames\n') + self.assertRegex(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 6\r\nLast-Modified: (Mon|Tue|Wed|Thu|Fri|Sat|Sun), .. (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) 2... ..:..:.. GMT\r\n\r\njames\n') self.assertEqual(se, b'10.1.2.3 200 6 127.0.0.1:80 (null) (null) /index.html\n') def testArgs(self): @@ -36,7 +36,7 @@ class BasicTests(unittest.TestCase): args = [] def setUp(self): - self.p = fnord(*self.args) + self.p = eris(*self.args) def tearDown(self): del self.p @@ -56,7 +56,7 @@ class DirTests(BasicTests): def testRootDir(self): so, se = self.get('/', 'empty') - self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Directory Listing: /

\n
\n
\n') + self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Directory Listing: /

\n
\n
\n') self.assertEqual(se, b'10.1.2.3 200 32 empty:80 (null) (null) /\n') def testNoTrailingSlash(self): @@ -67,7 +67,7 @@ class DirTests(BasicTests): def testFiles(self): so, se = self.get('/files/', 'default') - self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Directory Listing: /files/

\n
\nParent directory\n[TXT] 1.txt\n
\n') + self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Directory Listing: /files/

\n
\nParent directory\n[TXT] 1.txt\n
\n') self.assertEqual(se, b'10.1.2.3 200 110 default:80 (null) (null) /files/\n') @@ -76,17 +76,18 @@ class CGITests(BasicTests): def testSet(self): so, se = self.get('/cgi/set.cgi', 'default') - self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:fnord/2.0\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:GET\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\n') - self.assertEqual(se, b'10.1.2.3 200 248 default:80 (null) (null) /cgi/set.cgi\n') + self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:eris/2\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:GET\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\n') + self.assertEqual(se, b'10.1.2.3 200 245 default:80 (null) (null) /cgi/set.cgi\n') def testSetArgs(self): so, se = self.get('/cgi/set.cgi?a=1&b=2&c=3', 'default') - self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:fnord/2.0\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:GET\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\nQUERY_STRING:a=1&b=2&c=3\n') - self.assertEqual(se, b'10.1.2.3 200 273 default:80 (null) (null) /cgi/set.cgi\n') + self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:eris/2\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:GET\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\nQUERY_STRING:a=1&b=2&c=3\n') + self.assertEqual(se, b'10.1.2.3 200 270 default:80 (null) (null) /cgi/set.cgi\n') def testPost(self): so, se = self.post('/cgi/set.cgi', 'default', 'a=1&b=2&c=3') - self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: fnord/2.0\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:fnord/2.0\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:POST\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\nCONTENT_TYPE:application/x-www-form-urlencoded\nCONTENT_LENGTH:11\nForm data: a=1&b=2&c=3') + self.assertEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2\r\nPragma: no-cache\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nGATEWAY_INTERFACE:CGI/1.1\nSERVER_PROTOCOL:HTTP/1.0\nSERVER_SOFTWARE:eris/2\nSERVER_NAME:default:80\nSERVER_PORT:80\nREQUEST_METHOD:POST\nREQUEST_URI:/cgi/set.cgi\nSCRIPT_NAME:/cgi/set.cgi\nREMOTE_ADDR:10.1.2.3\nREMOTE_PORT:5858\nCONTENT_TYPE:application/x-www-form-urlencoded\nCONTENT_LENGTH:11\nForm data: a=1&b=2&c=3') + self.assertEqual(se, b'10.1.2.3 200 333 default:80 (null) (null) /cgi/set.cgi\n') unittest.main()