found another fnord bug

This commit is contained in:
Neale Pickett 2012-02-24 17:03:09 -07:00
parent 4536afe4a6
commit 98f853c8ac
5 changed files with 229 additions and 87 deletions

2
README
View File

@ -12,7 +12,7 @@ Significant differences between eris and fnord are:
* elimination of user switching (you can use tcpserver -[ug])
* elimination of chroot code (you can use chroot)
* several bugfixes (sent to the fnord mail list)
* removal of (non-functional) content-negotiation
* ignores Accept header (fnord does too)
----

View File

@ -39,6 +39,10 @@ fail () {
failures=$(expr $failures + 1)
}
d () {
tr '\r\n' '#%'
}
if [ ! -f fnord-1.10.tar.bz2 ]; then
wget http://www.fefe.de/fnord/fnord-1.10.tar.bz2
@ -82,7 +86,7 @@ printf 'GET / HTTP/1.0\r\nHost: empty\r\n\r\n' | $HTTPD_IDX 2>/dev/null | grep -
# 2. Should output \r\n\r\n; instead outputs \r\n\n
title "CGI output bare newlines"
printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -qU '^\r$' && pass || fail
printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '#%#%' && pass || fail
# 3. Should process both requests; instead drops second
title "Multiple requests in one packet"
@ -102,7 +106,13 @@ title "Second MIME-Type"
title "POST to static HTML"
(printf 'POST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\n';
ls / > /dev/null
printf 'aPOST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\na') | $HTTPD 2>/dev/null | grep -c '^HTTP/1.' | grep -q 2 && pass || fail
printf 'aPOST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\na') | $HTTPD 2>/dev/null | grep -c '200 OK' | grep -q 2 && pass || fail
# 7. HTTP/1.1 should default to keepalive; instead connection is closed
title "HTTP/1.1 default keepalive"
(printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n'
ls / >/dev/null
printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n') | $HTTPD 2>/dev/null | grep -c '^HTTP/' | grep -q 2 && pass || fail
cat <<EOD

179
eris.c
View File

@ -73,25 +73,15 @@
#define USE_MMAP
#endif
/*
* TCP_CORK is a Linux extension to work around a TCP problem.
* http://www.baus.net/on-tcp_cork has a good description.
* XXX: Since we do our own buffering, TCP_CORK may not be helping
* with anything. This needs testing.
*/
#ifdef TCP_CORK
static int corked;
#endif
enum { UNKNOWN, GET, HEAD, POST } method;
static long retcode = 404; /* used for logging code */
char *host = "?"; /* Host: header */
char *port; /* also Host: header, :80 part */
char *args; /* URL behind ? (for CGIs) */
char *url; /* string between GET and HTTP/1.0, *
char *path; /* string between GET and HTTP/1.0, *
* demangled */
char rpath[PATH_MAX];
char *ua = "?"; /* user-agent */
char *refer; /* Referrer: header */
char *accept_enc; /* Accept-Encoding */
@ -128,6 +118,25 @@ char *remote_ident;
#define BUFFER_SIZE 8192
char stdout_buf[BUFFER_SIZE];
/*
* TCP_CORK is a Linux extension to work around a TCP problem.
* http://www.baus.net/on-tcp_cork has a good description.
* XXX: Since we do our own buffering, TCP_CORK may not be helping
* with anything. This needs testing.
*/
void
cork(int enable)
{
#ifdef TCP_CORK
static int corked = 0;
if (enable != corked) {
setsockopt(1, IPPROTO_TCP, TCP_CORK, &enable, sizeof(enable));
corked = enable;
}
#endif
}
static void
sanitize(char *ua)
{ /* replace strings with underscores for *
@ -149,7 +158,7 @@ dolog(off_t len)
fprintf(stderr, "%s %ld %lu %s %s %s %s\n",
remote_ip ? remote_ip : "0.0.0.0",
retcode, (unsigned long) len, host, ua, refer, url);
retcode, (unsigned long) len, host, ua, refer, path);
}
/*
@ -243,6 +252,11 @@ read_header(FILE *f, char *buf, size_t * buflen)
static int bare = 1; /* LF here would be bare */
static int bufsize = 0;
if (! buf) {
lastbuf = NULL;
return 0;
}
if (lastbuf != buf) {
lastbuf = buf;
bare = 1;
@ -330,7 +344,7 @@ do_cgi(const char *pathinfo, const char *const *envp)
cgi_env[i++] = env_append("SERVER_PORT", port);
cgi_env[i++] = env_append("REQUEST_METHOD", method_name[method]);
cgi_env[i++] = env_append("REQUEST_URI", uri);
cgi_env[i++] = env_append("SCRIPT_NAME", url);
cgi_env[i++] = env_append("SCRIPT_NAME", path);
if (remote_ip)
cgi_env[i++] = env_append("REMOTE_ADDR", remote_ip);
if (remote_port)
@ -396,9 +410,9 @@ do_cgi(const char *pathinfo, const char *const *envp)
{
char tmp[PATH_MAX];
i = strrchr(url, '/') - url;
i = strrchr(rpath, '/') - rpath;
if (i) {
strncpy(tmp, url + 1, i);
strncpy(tmp, rpath + 1, i);
tmp[i] = 0;
chdir(tmp);
}
@ -412,7 +426,7 @@ do_cgi(const char *pathinfo, const char *const *envp)
*/
cgi_arg[0] = tmp;
tmp[0] = '.';
strcpy(tmp + 1, url + i);
strcpy(tmp + 1, rpath + i);
/*
* start cgi
@ -570,12 +584,7 @@ start_cgi(int nph, const char *pathinfo, const char *const *envp)
fflush(stdout);
dolog(size);
#ifdef TCP_CORK
{
int zero = 0;
setsockopt(1, IPPROTO_TCP, TCP_CORK, &zero, sizeof(zero));
}
#endif
cork(0);
} else {
/* Child */
@ -1028,7 +1037,6 @@ handledirlist(const char *origurl)
unsigned int nl;
const char *nurl = origurl;
url = (char *) origurl;
while (nurl[0] == '/')
++nurl;
if (nurl <= origurl)
@ -1138,7 +1146,8 @@ handleindexcgi(const char *testurl, const char *origurl, char *space)
if (!(st.st_mode & ul))
return 0; /* should be executable */
*(--test) = '/';
url = test;
strncpy(rpath, test, sizeof rpath);
return 1; /* Wow... now start "index.cgi" */
}
@ -1273,9 +1282,7 @@ static int
serve_static_data(int fd)
{
off_t len = rangeend - rangestart;
#ifdef TCP_CORK
corked = 0;
#endif
if (len < 4096) { /* for small files, sendfile is actually
* slower */
char tmp[4096];
@ -1291,13 +1298,8 @@ serve_static_data(int fd)
#ifdef USE_SENDFILE
{
off_t offset = rangestart;
#ifdef TCP_CORK
{
int one = 1;
setsockopt(1, IPPROTO_TCP, TCP_CORK, &one, sizeof(one));
corked = 1;
}
#endif
cork(1);
fflush(stdout);
{
off_t l = rangeend - rangestart;
@ -1305,11 +1307,11 @@ serve_static_data(int fd)
off_t c;
c = (l > (1ul << 31)) ? 1ul << 31 : l;
if (sendfile(1, fd, &offset, c) == -1)
#ifdef USE_MMAP
# ifdef USE_MMAP
return serve_mmap(fd);
#else
# else
return serve_read_write(fd);
#endif
# endif
l -= c;
} while (l);
}
@ -1317,18 +1319,14 @@ serve_static_data(int fd)
}
#else
fflush(stdout);
#ifdef TCP_CORK
{
int one = 1;
setsockopt(1, IPPROTO_TCP, TCP_CORK, &one, sizeof(one));
corked = 1;
}
#endif
#ifdef USE_MMAP
/* XXX: Is this really the right place to uncork? */
cork(0);
# ifdef USE_MMAP
return serve_mmap(fd);
#else
# else
return serve_read_write(fd);
#endif
# endif
#endif
}
@ -1337,10 +1335,11 @@ int
main(int argc, char *argv[], const char *const *envp)
{
char headerbuf[MAXHEADERLEN];
char *nurl,
*origurl;
char *url,
*nurl;
int doauth = 0;
int docgi = 0;
int dokeepalive = 1;
int dirlist = 0;
int redirect = 0;
int portappend = 0;
@ -1349,7 +1348,7 @@ main(int argc, char *argv[], const char *const *envp)
{
int opt;
while (-1 != (opt = getopt(argc, argv, "acdrp"))) {
while (-1 != (opt = getopt(argc, argv, "acdhkprv"))) {
switch (opt) {
case 'a':
doauth = 1;
@ -1360,15 +1359,29 @@ main(int argc, char *argv[], const char *const *envp)
case 'd':
dirlist = 1;
break;
case 'r':
redirect = 1;
case 'k':
dokeepalive = 0;
break;
case 'p':
portappend = 1;
break;
case 'r':
redirect = 1;
break;
case 'v':
printf(FNORD "\n");
return 0;
default:
fprintf(stderr, "Usage: %s [-c] [-d] [-r] [-p]\n",
fprintf(stderr, "Usage: %s [OPTIONS]\n",
argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "-a Enable authentication\n");
fprintf(stderr, "-c Enable CGI\n");
fprintf(stderr, "-d Enable directory listing\n");
fprintf(stderr, "-k No default keepalive with HTTP/1.1\n");
fprintf(stderr, "-p Append port to hostname directory\n");
fprintf(stderr, "-r Enable symlink redirection\n");
fprintf(stderr, "-v Print version and exit\n");
return 69;
}
}
@ -1381,10 +1394,12 @@ main(int argc, char *argv[], const char *const *envp)
handlenext:
/* This is cancelled by later calls to alarm */
alarm(READTIMEOUT);
headerlen = sizeof headerbuf;
// XXX: I need a way to signal that this is a new read with the same buffer.
/* Clear static variables. This is such a kludge. I should fix it. */
read_header(NULL, NULL, NULL);
switch (read_header(stdin, headerbuf, &headerlen)) {
case -1:
return 0;
@ -1394,24 +1409,22 @@ main(int argc, char *argv[], const char *const *envp)
break;
}
alarm(0);
if (headerlen < 10)
badrequest(400, "Bad Request", "That does not look like HTTP");
if (!memcmp(headerbuf, "GET /", 5)) {
method = GET;
url = headerbuf + 4;
path = headerbuf + 4;
} else if (!memcmp(headerbuf, "POST /", 6)) {
method = POST;
url = headerbuf + 5;
path = headerbuf + 5;
} else if (!memcmp(headerbuf, "HEAD /", 6)) {
method = HEAD;
url = headerbuf + 5;
path = headerbuf + 5;
} else
badrequest(405, "Method Not Allowed", "Unsupported HTTP method.");
origurl = url;
url = path;
{
/*
@ -1431,7 +1444,11 @@ main(int argc, char *argv[], const char *const *envp)
if (httpversion > 1) {
badrequest(400, "Bad Request", "HTTP Version not supported");
}
keepalive = 0;
if (httpversion > 0) {
keepalive = dokeepalive;
} else {
keepalive = 0;
}
/*
* demangle path in-place
@ -1470,10 +1487,7 @@ main(int argc, char *argv[], const char *const *envp)
*d = 0;
}
/*
* XXX: check use of uri to see if it needs to be duplicated
*/
uri = strdup(url);
strncpy(rpath, url, sizeof rpath);
}
{
@ -1665,12 +1679,7 @@ main(int argc, char *argv[], const char *const *envp)
if ((method == HEAD))
badrequest(400, "Bad Request",
"Illegal HTTP method for Gateway call.");
#ifdef TCP_CORK
{
int one = 1;
setsockopt(1, IPPROTO_TCP, TCP_CORK, &one, sizeof(one));
}
#endif
cork(1);
for (i = nurl - url; i > -1; --i) {
if ((nurl[0] == '/') && (nurl[1] == 'n')
&& (nurl[2] == 'p') && (nurl[3] == 'h')
@ -1732,6 +1741,11 @@ main(int argc, char *argv[], const char *const *envp)
(unsigned long long) st.st_size);
}
fputs("\r\n", stdout);
for (; post_len; post_len -= 1) {
getchar();
}
if (method == GET || method == POST) {
int ret;
@ -1747,13 +1761,7 @@ main(int argc, char *argv[], const char *const *envp)
case 1:
return 1;
}
#ifdef TCP_CORK
if (corked) {
int zero = 0;
setsockopt(1, IPPROTO_TCP, TCP_CORK, &zero,
sizeof(zero));
}
#endif
cork(0);
if (keepalive > 0) {
close(fd);
fchdir(rootdir);
@ -1763,8 +1771,11 @@ main(int argc, char *argv[], const char *const *envp)
exit(0);
error500:
retcode = 500;
} else
} else {
fflush(stdout);
}
} else {
retcode = 404;
}
}
switch (retcode) {
@ -1772,11 +1783,11 @@ main(int argc, char *argv[], const char *const *envp)
{
char *space = alloca(strlen(url) + 2);
if (handleindexcgi(url, origurl, space))
if (handleindexcgi(url, path, space))
goto indexcgi;
handleredirect(url, origurl);
handleredirect(url, path);
if (dirlist) {
handledirlist(origurl);
handledirlist(path);
}
badrequest(404, "Not Found", "No such file or directory.");
}
@ -1789,5 +1800,5 @@ main(int argc, char *argv[], const char *const *envp)
case 500:
badrequest(500, "Internal Server Error", "");
}
return 1;
return 0;
}

116
tests.sh Executable file
View File

@ -0,0 +1,116 @@
#! /bin/sh
: ${HTTPD:=./eris}
: ${HTTPD_CGI:=./eris -c}
: ${HTTPD_IDX:=./eris -d}
H () {
echo
echo "$@"
echo "==================================="
echo
}
BR () {
echo
echo "-----------------------------------"
echo
}
title() {
printf "* %-50s: " "$1"
tests=$(expr $tests + 1)
}
successes=0
pass () {
echo 'ok'
successes=$(expr $successes + 1)
}
failures=0
fail () {
echo 'FAIL'
failures=$(expr $failures + 1)
}
d () {
tr '\r\n' '#%'
}
if [ ! -d default ]; then
mkdir default
echo james > default/index.html
touch default/a
cat <<EOD > default/a.cgi
#! /bin/sh
echo 'Content-type: text/plain'
ls / > /dev/null # delay a little
echo
echo james
EOD
chmod +x default/a.cgi
mkdir empty:80
fi
echo "HTTPD: $HTTPD "
echo "CGI: $HTTPD_CGI "
echo "IDX: $HTTPD_IDX "
H "Basic tests"
title "GET"
printf 'GET / HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.0 200 OK#%Server: [a-z]*/[0-9.]*#%Content-Type: text/html; charset=UTF-8#%Content-Length: 6#%Last-Modified: ..., .. ... 20.. ..:..:.. GMT#%#%james%' && pass || fail
title "POST"
printf 'POST / HTTP/1.0\r\nContent-Type: a\r\nContent-Length: 5\r\n\r\njames' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.0 200 OK#%Server: [a-z]*/[0-9.]*#%Content-Type: text/html; charset=UTF-8#%Content-Length: 6#%Last-Modified: ..., .. ... 20.. ..:..:.. GMT#%#%james%' && pass || fail
title "Logging /"
(printf 'GET / HTTP/1.1\r\nHost: host\r\n\r\n' |
PROTO=TCP TCPREMOTEPORT=1234 TCPREMOTEIP=10.0.0.2 $HTTPD >/dev/null) 2>&1 | grep -q '^10.0.0.2 200 6 host (null) (null) /$' && pass || fail
title "Logging /index.html"
(printf 'GET /index.html HTTP/1.1\r\nHost: host\r\n\r\n' |
PROTO=TCP TCPREMOTEPORT=1234 TCPREMOTEIP=10.0.0.2 $HTTPD >/dev/null) 2>&1 | grep -q '^10.0.0.2 200 6 host (null) (null) /index.html$' && pass || fail
H "fnord bugs"
# 1. Should return directory listing of /; instead segfaults
title "Directory indexing of /"
printf 'GET / HTTP/1.0\r\nHost: empty\r\n\r\n' | $HTTPD_IDX 2>/dev/null | grep -q 200 && pass || fail
# 2. Should output \r\n\r\n; instead outputs \r\n\n
title "CGI output bare newlines"
printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '#%#%' && pass || fail
# 3. Should process both requests; instead drops second
title "Multiple requests in one packet"
printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n' | $HTTPD 2>/dev/null | grep -c '^HTTP/1.' | grep -q 2 && pass || fail
# 4. Should return 406 Not Acceptable; instead ignores Accept header
title "Accept header"
printf 'GET / HTTP/1.0\r\nAccept: nothing\r\n\r\n' | $HTTPD 2>/dev/null | grep 406 && pass || fail
# 5. Should serve second request as default MIME-Type (text/plain); instead uses previous mime type
title "Second MIME-Type"
(printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n'
ls / > /dev/null # Delay required to work around test #3
printf 'GET /a HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n') | $HTTPD 2>/dev/null | grep -q 'text/plain\|application/octet-stream' && pass || fail
# 6. Should consume POST data; instead tries to read POST data as second request
title "POST to static HTML"
(printf 'POST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\n';
ls / > /dev/null
printf 'aPOST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\na') | $HTTPD 2>/dev/null | grep -c '200 OK' | grep -q 2 && pass || fail
# 7. HTTP/1.1 should default to keepalive; instead connection is closed
title "HTTP/1.1 default keepalive"
(printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n'
ls / >/dev/null
printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n') | $HTTPD 2>/dev/null | grep -c '^HTTP/' | grep -q 2 && pass || fail
BR
echo "$successes of $tests tests passed ($failures failed)."
exit $failures

View File

@ -110,6 +110,11 @@ class CGITests(BasicTests):
self.assertLinesEqual(se, b'10.1.2.3 200 306 default (null) (null) /cgi/set.cgi\n')
self.assertLinesEqual(so, b'HTTP/1.0 200 OK\r\nServer: eris/2.0\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.0\nSERVER_NAME:default\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')
class BufferingTests(BasicTests):
def testDoubleGet(self):
so, se = self.p.communicate(b'GET / HTTP/1.0\r\n\r\nGET / HTTP/1.0\r\n\r\n')
self.assertLinesEqual(so, b'')
# XXX: Test posting to static html with keepalive
# (it probably won't discard content-length octets)