mirror of https://github.com/nealey/eris.git
Rename to eris, maybe fix CGI \r\n\r\n bug
This commit is contained in:
parent
ab3a9fb31b
commit
300840c311
16
CHANGES
16
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 <neale@woozle.org>
|
||||
|
||||
1.10:
|
||||
have fallback in case sendfile fails
|
||||
|
|
8
Makefile
8
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
|
||||
|
|
47
README
47
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
|
||||
|
|
114
fnord.c → eris.c
114
fnord.c → 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 <sys/sendfile.h>
|
||||
#endif
|
||||
|
||||
#ifndef O_NDELAY
|
||||
#define O_NDELAY O_NONBLOCK
|
||||
#endif
|
||||
|
||||
#define USE_MMAP
|
||||
#ifndef _POSIX_MAPPED_FILES
|
||||
#undef USE_MMAP
|
||||
|
@ -219,6 +216,37 @@ elen(register const char *const *e)
|
|||
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 *
|
||||
env_append(const char *key, const char *val)
|
||||
{
|
||||
|
@ -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:
|
||||
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 (ch == '\n') {
|
||||
++i;
|
||||
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),
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue