Rename to eris, maybe fix CGI \r\n\r\n bug

This commit is contained in:
Neale Pickett 2012-02-16 20:18:17 -07:00
parent ab3a9fb31b
commit 300840c311
5 changed files with 141 additions and 71 deletions

16
CHANGES
View File

@ -1,17 +1,17 @@
2.0: 2:
Major modifications Rename to "eris httpd" to acknowledge fork
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
Add regression test suite Add regression test suite
Replace compile-time options with command-line ones
1.11: 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 Fix if-modified-since date parsing
Make text content-types use charset=UTF-8 Make text content-types use charset=UTF-8
Change default content-type to application/octet-stream Change default content-type to application/octet-stream
Makefile no longer overrides CC and CPP from parent makes Makefile no longer overrides CC and CPP from parent makes
Don't send Content-type if there's no content Don't send Content-type if there's no content
New maintainer: Neale Pickett <neale@woozle.org>
1.10: 1.10:
have fallback in case sendfile fails have fallback in case sendfile fails

View File

@ -1,11 +1,11 @@
VERSION := $(shell head -n 1 CHANGES | tr -d :) 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 cd tests && python3 ./test.py
clean: clean:
rm -f *.[oa] fnord rm -f *.[oa] eris

47
README
View File

@ -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: Usage:
tcpserver -v -RHl localhost -u 1234 -g 1234 0 80 ./httpd 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 the Referer HTTP header or "none" if none was given, and the rest of
each line is the decoded requested URL. each line is the decoded requested URL.
fnord-httpd does simple virtual hosting. If the Host: HTTP header is eris 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 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", fnord will look for client asks for "/" on host "www.fefe.de:80", eris will look for
"www.fefe.de:80/index.html". Fnord will also try the directory "www.fefe.de:80/index.html". Eris will also try the directory
"default" if no specific directory for the virtual host was there. If "default" if no specific directory for the virtual host was there. If
the directory is a dangling symlink and fnord was compiled with the directory is a dangling symlink and eris was compiled with
-DREDIRECT (default), fnord will redirect the whole site. Examples: -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 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/ 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.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/. 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). 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 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 the source code. Shouldn't be a problem because you _have_ the source
code ;) code ;)
fnord implements HTTP redirection. If a file is not found, but a eris implements HTTP redirection. If a file is not found, but a
dangling symlink is there under the same name, fnord will issue 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 redirection to the contents of that symlink. To be RFC compliant, the
symlink must point to a full URL, i.e. 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 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 if the file is available and the client supports the mime-type and
content-encoding. That means you can save substantial bandwidth by content-encoding. That means you can save substantial bandwidth by
having an index.html.gz for each index.html, as most clients can having an index.html.gz for each index.html, as most clients can
transparently decode gzipped files. 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. 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. 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 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 CGIs. Please see http://hoohoo.ncsa.uiuc.edu/cgi/interface.html for the

View File

@ -38,6 +38,7 @@
#define DUMP_s(v) DUMPf("%s = %s", #v, v) #define DUMP_s(v) DUMPf("%s = %s", #v, v)
#define DUMP_c(v) DUMPf("%s = %c", #v, v) #define DUMP_c(v) DUMPf("%s = %c", #v, v)
#define DUMP_p(v) DUMPf("%s = %p", #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 * the following is the time in seconds that fnord should wait for a valid
@ -70,10 +71,6 @@
#include <sys/sendfile.h> #include <sys/sendfile.h>
#endif #endif
#ifndef O_NDELAY
#define O_NDELAY O_NONBLOCK
#endif
#define USE_MMAP #define USE_MMAP
#ifndef _POSIX_MAPPED_FILES #ifndef _POSIX_MAPPED_FILES
#undef USE_MMAP #undef USE_MMAP
@ -219,6 +216,37 @@ elen(register const char *const *e)
return i; return i;
} }
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 * char *
env_append(const char *key, const char *val) env_append(const char *key, const char *val)
{ {
@ -370,30 +398,37 @@ cgi_child(int sig)
signal(SIGCHLD, cgi_child); 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) cgi_send_correct_http(const char *s, unsigned int sl)
{ {
unsigned int i; unsigned int i;
char ch = 0; int newline = 0;
for (i = 0; i < sl; ++i) {
if ((s[i] == '\r') && (s[i + 1] == '\n')) { for (i = 0; i < sl; i += 1) {
++i; switch (s[i]) {
goto out_nl; case '\r':
} else { if (s[i + 1] == '\n') {
if (s[i] != '\n') { i += 1;
putchar(s[i]); case '\n':
} else {
out_nl:
printf("\r\n"); printf("\r\n");
if (ch == '\n') { if (newline) {
++i; fwrite(s + i + 1, sl - i - 1, 1, stdout);
return 0;
} else {
newline = 1;
}
break;
} else {
default:
newline = 0;
putchar(s[i]);
}
break; break;
} }
} }
} return 1;
ch = s[i];
}
printf("%.*s", sl - i, s + i);
} }
static void static void
@ -442,17 +477,29 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp)
* read from cgi * read from cgi
*/ */
if (pfd[0].revents & POLLIN) { 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; break;
if (n < 0) }
if (len == -1) {
goto cgi_500; goto cgi_500;
}
/* /*
* startup * startup
*/ */
if (startup) { if (startup) {
startup = 0;
if (nph) { /* NPH-CGI */ if (nph) { /* NPH-CGI */
printf("%.*s", n, ibuf); startup = 0;
printf("%.*s", len, ibuf);
/* /*
* skip HTTP/x.x * skip HTTP/x.x
*/ */
@ -473,7 +520,8 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp)
FNORD FNORD
"\r\nPragma: no-cache\r\nConnection: close\r\n"); "\r\nPragma: no-cache\r\nConnection: close\r\n");
signal(SIGCHLD, SIG_IGN); 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 * non startup
*/ */
else { else {
printf("%.*s", n, ibuf); fwrite(ibuf, len, 1, stdout);
} }
size += n; size += len;
if (pfd[0].revents & POLLHUP) if (pfd[0].revents & POLLHUP)
break; break;
} }
@ -1398,6 +1446,7 @@ serve_static_data(int fd)
#endif #endif
} }
int int
main(int argc, char *argv[], const char *const *envp) 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", printf("Content-Length: %llu\r\n",
(unsigned long long) (rangeend - rangestart)); (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); struct tm *x = gmtime(&st.st_mtime);
/* /*
* "Sun, 06 Nov 1994 08:49:37 GMT" * "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), days + (3 * x->tm_wday),
x->tm_mday, x->tm_mday,
months + (3 * x->tm_mon), months + (3 * x->tm_mon),

View File

@ -4,8 +4,8 @@ import unittest
from subprocess import * from subprocess import *
import os import os
def fnord(*args): def eris(*args):
return Popen(('../fnord',) + args, return Popen(('../eris',) + args,
stdin=PIPE, stdout=PIPE, stderr=PIPE, stdin=PIPE, stdout=PIPE, stderr=PIPE,
env={'PROTO': 'TCP', env={'PROTO': 'TCP',
'TCPREMOTEPORT': '5858', 'TCPREMOTEPORT': '5858',
@ -13,9 +13,9 @@ def fnord(*args):
class ArgTests(unittest.TestCase): class ArgTests(unittest.TestCase):
def check_index(self, *args): def check_index(self, *args):
p = fnord(*args) p = eris(*args)
so, se = p.communicate(b'GET / HTTP/1.0\r\n\r\n') 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') self.assertEqual(se, b'10.1.2.3 200 6 127.0.0.1:80 (null) (null) /index.html\n')
def testArgs(self): def testArgs(self):
@ -36,7 +36,7 @@ class BasicTests(unittest.TestCase):
args = [] args = []
def setUp(self): def setUp(self):
self.p = fnord(*self.args) self.p = eris(*self.args)
def tearDown(self): def tearDown(self):
del self.p del self.p
@ -56,7 +56,7 @@ class DirTests(BasicTests):
def testRootDir(self): def testRootDir(self):
so, se = self.get('/', 'empty') 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<h3>Directory Listing: /</h3>\n<pre>\n</pre>\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<h3>Directory Listing: /</h3>\n<pre>\n</pre>\n')
self.assertEqual(se, b'10.1.2.3 200 32 empty:80 (null) (null) /\n') self.assertEqual(se, b'10.1.2.3 200 32 empty:80 (null) (null) /\n')
def testNoTrailingSlash(self): def testNoTrailingSlash(self):
@ -67,7 +67,7 @@ class DirTests(BasicTests):
def testFiles(self): def testFiles(self):
so, se = self.get('/files/', 'default') 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<h3>Directory Listing: /files/</h3>\n<pre>\n<a href="/">Parent directory</a>\n[TXT] <a href="1.txt">1.txt</a>\n</pre>\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<h3>Directory Listing: /files/</h3>\n<pre>\n<a href="/">Parent directory</a>\n[TXT] <a href="1.txt">1.txt</a>\n</pre>\n')
self.assertEqual(se, b'10.1.2.3 200 110 default:80 (null) (null) /files/\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): def testSet(self):
so, se = self.get('/cgi/set.cgi', 'default') 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(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 248 default:80 (null) (null) /cgi/set.cgi\n') self.assertEqual(se, b'10.1.2.3 200 245 default:80 (null) (null) /cgi/set.cgi\n')
def testSetArgs(self): def testSetArgs(self):
so, se = self.get('/cgi/set.cgi?a=1&b=2&c=3', 'default') 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(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 273 default:80 (null) (null) /cgi/set.cgi\n') self.assertEqual(se, b'10.1.2.3 200 270 default:80 (null) (null) /cgi/set.cgi\n')
def testPost(self): def testPost(self):
so, se = self.post('/cgi/set.cgi', 'default', 'a=1&b=2&c=3') 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() unittest.main()