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:
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 <neale@woozle.org>
1.10:
have fallback in case sendfile fails

View File

@ -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

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:
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

View File

@ -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 <sys/sendfile.h>
#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),

View File

@ -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<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')
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<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')
@ -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()