Rewrite quark from the ground up again

I noticed that the data structures didn't allow a flexible handling of
the code while trying to extend it to support if-modified-since-responses.
To tackle this, I refactored the data structures and proceeded to
rewrite the server from the ground up, implementing all present features
plus fixing a lot of bugs and introducing the 304 header handling as
requested by many people.

Please report bugs if you find them.

While at it, I refactored the build system as well and updated all
surrounding files respectively.
This commit is contained in:
Laslo Hunhold 2017-06-20 21:40:00 +02:00
parent 29d53f65b7
commit 6347e2ec3e
5 changed files with 512 additions and 383 deletions

View file

@ -1,6 +1,6 @@
ISC-License ISC-License
(c) 2016 Laslo Hunhold <dev@frign.de> (c) 2016-2017 Laslo Hunhold <dev@frign.de>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View file

@ -1,39 +1,36 @@
# See LICENSE file for copyright and license details
# quark - simple web server # quark - simple web server
.POSIX:
include config.mk include config.mk
all: quark all: quark
quark: quark.o config.h config.mk quark: quark.c arg.h config.h config.mk
${CC} -o $@ quark.o ${LDFLAGS} $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) quark.c $(LDFLAGS)
quark.o: quark.c config.h config.mk
${CC} -c ${CFLAGS} quark.c
config.h: config.h:
cp config.def.h $@ cp config.def.h $@
clean: clean:
rm -f quark quark.o quark-${VERSION}.tar.gz rm -f quark
dist: clean dist:
mkdir -p quark-${VERSION} rm -rf "quark-$(VERSION)"
mkdir -p "quark-$(VERSION)"
cp -R LICENSE Makefile arg.h config.def.h config.mk quark.1 \ cp -R LICENSE Makefile arg.h config.def.h config.mk quark.1 \
quark.c quark-${VERSION} quark.c "quark-$(VERSION)"
tar -cf quark-${VERSION}.tar quark-${VERSION} tar -cf - "quark-$(VERSION)" | gzip -c > "quark-$(VERSION).tar.gz"
gzip quark-${VERSION}.tar rm -rf "quark-$(VERSION)"
rm -rf quark-${VERSION}
install: all install: all
mkdir -p ${DESTDIR}${PREFIX}/bin mkdir -p "$(DESTDIR)$(PREFIX)/bin"
cp -f quark ${DESTDIR}${PREFIX}/bin cp -f quark "$(DESTDIR)$(PREFIX)/bin"
chmod 755 ${DESTDIR}${PREFIX}/bin/quark chmod 755 "$(DESTDIR)$(PREFIX)/bin/quark"
mkdir -p ${DESTDIR}${MANPREFIX}/man1 mkdir -p "$(DESTDIR)$(MANPREFIX)/man1"
cp quark.1 ${DESTDIR}${MANPREFIX}/man1/quark.1 cp quark.1 "$(DESTDIR)$(MANPREFIX)/man1/quark.1"
chmod 644 ${DESTDIR}${MANPREFIX}/man1/quark.1 chmod 644 "$(DESTDIR)$(MANPREFIX)/man1/quark.1"
uninstall: uninstall:
rm -f ${DESTDIR}${PREFIX}/bin/quark rm -f "$(DESTDIR)$(PREFIX)/bin/quark"
rm -f ${DESTDIR}${MANPREFIX}/man1/quark.1 rm -f "$(DESTDIR)$(MANPREFIX)/man1/quark.1"
.PHONY: all options clean dist install uninstall

View file

@ -1,4 +1,4 @@
static const char *host = "127.0.0.1"; static const char *host = "localhost";
static const char *port = "80"; static const char *port = "80";
static const char *servedir = "."; static const char *servedir = ".";
static const char *docindex = "index.html"; static const char *docindex = "index.html";
@ -7,7 +7,8 @@ static const char *user = "nobody";
static const char *group = "nogroup"; static const char *group = "nogroup";
static const int maxnprocs = 512; static const int maxnprocs = 512;
#define MAXREQLEN 4096 /* >= 4 */ #define HEADER_MAX 4096
#define FIELD_MAX 200
static const struct { static const struct {
char *ext; char *ext;

View file

@ -5,16 +5,12 @@ VERSION = 0
# paths # paths
PREFIX = /usr/local PREFIX = /usr/local
MANPREFIX = ${PREFIX}/share/man MANPREFIX = $(PREFIX)/man
# includes and libs
INCS = -I. -I/usr/include
LIBS = -L/usr/lib -lc
# flags # flags
CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE
CFLAGS = -g -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS} CFLAGS = -std=c99 -pedantic -Wall -Os
LDFLAGS = ${LIBS} LDFLAGS = -s
# compiler and linker # compiler and linker
CC = cc CC = cc

829
quark.c
View file

@ -30,10 +30,60 @@ char *argv0;
#include "config.h" #include "config.h"
enum stati { #undef MIN
#define MIN(x,y) ((x) < (y) ? (x) : (y))
#undef MAX
#define MAX(x,y) ((x) > (y) ? (x) : (y))
#define TIMESTAMP_LEN 30
enum req_field {
REQ_HOST,
REQ_RANGE,
REQ_MOD,
NUM_REQ_FIELDS,
};
static char *req_field_str[] = {
[REQ_HOST] = "Host",
[REQ_RANGE] = "Range",
[REQ_MOD] = "If-Modified-Since",
};
enum req_method {
M_OPTIONS,
M_GET,
M_HEAD,
M_POST,
M_PUT,
M_DELETE,
M_TRACE,
M_CONNECT,
NUM_REQ_METHODS,
};
static char *req_method_str[] = {
[M_OPTIONS] = "OPTIONS",
[M_GET] = "GET",
[M_HEAD] = "HEAD",
[M_POST] = "POST",
[M_PUT] = "PUT",
[M_DELETE] = "DELETE",
[M_TRACE] = "TRACE",
[M_CONNECT] = "CONNECT",
};
struct request {
enum req_method method;
char target[PATH_MAX];
char field[NUM_REQ_FIELDS][FIELD_MAX];
};
enum status {
S_OK = 200, S_OK = 200,
S_PARTIAL_CONTENT = 206, S_PARTIAL_CONTENT = 206,
S_MOVED_PERMANENTLY = 301, S_MOVED_PERMANENTLY = 301,
S_NOT_MODIFIED = 304,
S_BAD_REQUEST = 400, S_BAD_REQUEST = 400,
S_FORBIDDEN = 403, S_FORBIDDEN = 403,
S_NOT_FOUND = 404, S_NOT_FOUND = 404,
@ -44,10 +94,11 @@ enum stati {
S_VERSION_NOT_SUPPORTED = 505, S_VERSION_NOT_SUPPORTED = 505,
}; };
static char *statistr[] = { static char *status_str[] = {
[S_OK] = "OK", [S_OK] = "OK",
[S_PARTIAL_CONTENT] = "Partial Content", [S_PARTIAL_CONTENT] = "Partial Content",
[S_MOVED_PERMANENTLY] = "Moved Permanently", [S_MOVED_PERMANENTLY] = "Moved Permanently",
[S_NOT_MODIFIED] = "Not Modified",
[S_BAD_REQUEST] = "Bad Request", [S_BAD_REQUEST] = "Bad Request",
[S_FORBIDDEN] = "Forbidden", [S_FORBIDDEN] = "Forbidden",
[S_NOT_FOUND] = "Not Found", [S_NOT_FOUND] = "Not Found",
@ -58,107 +109,14 @@ static char *statistr[] = {
[S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported",
}; };
#undef MIN
#define MIN(x,y) ((x) < (y) ? (x) : (y))
static char * static char *
timestamp(time_t t) timestamp(time_t t, char buf[TIMESTAMP_LEN])
{ {
static char s[30];
if (!t) if (!t)
t = time(NULL); t = time(NULL);
strftime(s, sizeof(s), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t)); strftime(buf, TIMESTAMP_LEN, "%a, %d %b %Y %T GMT", gmtime(&t));
return s; return buf;
}
static int
sendstatus(enum stati code, int fd, ...)
{
va_list ap;
char buf[4096];
size_t written, buflen;
ssize_t ret;
long lower, upper, size;
buflen = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %s\r\n", code,
statistr[code]);
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Date: %s\r\n", timestamp(0));
va_start(ap, fd);
switch (code) {
case S_OK: /* arg-list: mime, size */
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Type: %s\r\n",
va_arg(ap, char *));
if ((size = va_arg(ap, long)) >= 0) {
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Length: %ld\r\n",
size);
}
break;
case S_PARTIAL_CONTENT: /* arg-list: mime, lower, upper, size */
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Type: %s\r\n",
va_arg(ap, char *));
lower = va_arg(ap, long);
upper = va_arg(ap, long);
size = va_arg(ap, long);
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Range: bytes %ld-%ld/%ld\r\n",
lower, upper, size);
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Length: %ld\r\n",
(upper - lower) + 1);
break;
case S_MOVED_PERMANENTLY: /* arg-list: host, url */
if (!strcmp(port, "80")) {
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Location: http://%s%s\r\n",
va_arg(ap, char *),
va_arg(ap, char *));
} else {
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Location: http://%s:%s%s\r\n",
va_arg(ap, char *), port,
va_arg(ap, char *));
}
break;
case S_METHOD_NOT_ALLOWED: /* arg-list: none */
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Allow: GET\r\n");
break;
default:
break;
}
va_end(ap);
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Connection: close\r\n");
if (code != S_OK && code != S_PARTIAL_CONTENT) {
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"Content-Type: text/html\r\n");
buflen += snprintf(buf + buflen, sizeof(buf) - buflen,
"\r\n<!DOCTYPE html>\r\n<html>\r\n"
"\t<head><title>%d %s</title></head>"
"\r\n\t<body><h1>%d %s</h1></body>\r\n"
"</html>\r\n", code, statistr[code],
code, statistr[code]);
} else {
buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "\r\n");
}
for (written = 0; buflen > 0; written += ret, buflen -= ret) {
if ((ret = write(fd, buf + written, buflen)) < 0) {
code = S_REQUEST_TIMEOUT;
break;
}
}
return code;
} }
static size_t static size_t
@ -190,9 +148,9 @@ encode(char src[PATH_MAX], char dest[PATH_MAX])
char *s; char *s;
for (s = src, i = 0; *s; s++) { for (s = src, i = 0; *s; s++) {
if (isalnum(*s) || *s == '~' || *s == '-' || *s == '.' || if (iscntrl(*s) || (unsigned char)*s > 127) {
*s == '_' || *s > 127) { i += snprintf(dest + i, PATH_MAX - i, "%%%02X",
i += snprintf(dest + i, PATH_MAX - i, "%%%02X", *s); (unsigned char)*s);
} else { } else {
dest[i] = *s; dest[i] = *s;
i++; i++;
@ -203,246 +161,450 @@ encode(char src[PATH_MAX], char dest[PATH_MAX])
} }
static int static int
listdir(char *dir, int fd) sendbuffer(int fd, char *buf, size_t buflen) {
size_t written;
ssize_t off;
for (written = 0; buflen > 0; written += off, buflen -= off) {
if ((off = write(fd, buf + written, buflen)) < 0) {
return 1;
}
}
return 0;
}
static enum status
sendstatus(int fd, enum status s)
{ {
struct dirent **e = NULL; static char res[4096], t[TIMESTAMP_LEN];
static char buf[BUFSIZ]; size_t len;
size_t buflen;
ssize_t bread, written;
int dirlen, ret, i;
if ((dirlen = scandir(dir, &e, NULL, alphasort)) < 0) { /* assemble error response */
return sendstatus(S_FORBIDDEN, fd); len = snprintf(res, sizeof(res),
} "HTTP/1.1 %d %s\r\n"
if ((ret = sendstatus(S_OK, fd, "text/html", (long)-1)) != S_OK) { "Date: %s\r\n"
return ret; "Connection: close\r\n"
} "%s"
if ((buflen = snprintf(buf, sizeof(buf), "<!DOCTYPE html>\r\n" "Content-Type: text/html\r\n"
"<html>\r\n<head><title>Index of %s" "\r\n"
"</title></head>\r\n<body>\r\n" "<!DOCTYPE html>\n<html>\n\t<head>\n"
"<a href=\"..\">..</a><br />\r\n", "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
dir)) >= sizeof(buf)) { "\t\t<h1>%d %s</h1>\n\t</body>\n</html>",
return S_INTERNAL_SERVER_ERROR; s, status_str[s], timestamp(0, t),
} (s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "",
written = 0; s, status_str[s], s, status_str[s]);
while (buflen > 0) {
if ((bread = write(fd, buf + written, buflen)) < 0) { return sendbuffer(fd, res, len) ? S_REQUEST_TIMEOUT : s;
return S_REQUEST_TIMEOUT;
}
written += bread;
buflen -= bread;
} }
for (i = 0; i < dirlen; i++) { static int
if (e[i]->d_name[0] == '.') { /* hidden files, ., .. */ getrequest(int fd, struct request *r)
{
size_t hlen, i, mlen;
ssize_t off;
char h[HEADER_MAX], *p, *q;
/*
* receive header
*/
for (hlen = 0; ;) {
if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) {
return sendstatus(fd, S_REQUEST_TIMEOUT);
} else if (off == 0) {
break;
}
hlen += off;
if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) {
break;
}
if (hlen == sizeof(h)) {
return sendstatus(fd, S_REQUEST_TOO_LARGE);
}
}
/* remove terminating empty line */
if (hlen < 2) {
return sendstatus(fd, S_BAD_REQUEST);
}
hlen -= 2;
/* null-terminate the header */
h[hlen] = '\0';
/*
* parse request line
*/
/* METHOD */
for (i = 0; i < NUM_REQ_METHODS; i++) {
mlen = strlen(req_method_str[i]);
if (!strncmp(req_method_str[i], h, mlen)) {
r->method = i;
break;
}
}
if (i == NUM_REQ_METHODS) {
return sendstatus(fd, S_BAD_REQUEST);
}
/* a single space must follow the method */
if (h[mlen] != ' ') {
return sendstatus(fd, S_BAD_REQUEST);
}
/* basis for next step */
p = h + mlen + 1;
/* TARGET */
if (!(q = strchr(p, ' '))) {
return sendstatus(fd, S_BAD_REQUEST);
}
*q = '\0';
if (q - p + 1 > PATH_MAX) {
return sendstatus(fd, S_REQUEST_TOO_LARGE);
}
memcpy(r->target, p, q - p + 1);
decode(r->target, r->target);
/* basis for next step */
p = q + 1;
/* HTTP-VERSION */
if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) {
return sendstatus(fd, S_BAD_REQUEST);
}
p += sizeof("HTTP/") - 1;
if (strncmp(p, "1.0", sizeof("1.0") - 1) &&
strncmp(p, "1.1", sizeof("1.1") - 1)) {
return sendstatus(fd, S_VERSION_NOT_SUPPORTED);
}
p += sizeof("1.*") - 1;
/* check terminator */
if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) {
return sendstatus(fd, S_BAD_REQUEST);
}
/* basis for next step */
p += sizeof("\r\n") - 1;
/*
* parse request-fields
*/
/* empty all fields */
for (i = 0; i < NUM_REQ_FIELDS; i++) {
r->field[i][0] = '\0';
}
/* match field type */
for (; *p != '\0';) {
for (i = 0; i < NUM_REQ_FIELDS; i++) {
if (!strncmp(p, req_field_str[i],
strlen(req_field_str[i]))) {
break;
}
}
if (i == NUM_REQ_FIELDS) {
/* unmatched field, skip this line */
if (!(q = strstr(p, "\r\n"))) {
return sendstatus(fd, S_BAD_REQUEST);
}
p = q + (sizeof("\r\n") - 1);
continue; continue;
} }
if ((buflen = snprintf(buf, sizeof(buf), "<a href=\"%s"
"\">%s</a><br />\r\n", e[i]->d_name, p += strlen(req_field_str[i]);
e[i]->d_name)) >= sizeof(buf)) {
return S_INTERNAL_SERVER_ERROR; /* a single colon must follow the field name */
} if (*p != ':') {
written = 0; return sendstatus(fd, S_BAD_REQUEST);
while (buflen > 0) {
if ((bread = write(fd, buf + written, buflen)) < 0) {
return S_REQUEST_TIMEOUT;
}
written += bread;
buflen -= bread;
}
} }
if ((buflen = snprintf(buf, sizeof(buf), "\r\n</body></html>\r\n")) /* skip whitespace */
for (++p; *p == ' '; p++)
;
/* extract field content */
if (!(q = strstr(p, "\r\n"))) {
return sendstatus(fd, S_BAD_REQUEST);
}
*q = '\0';
if (q - p + 1 > FIELD_MAX) {
return sendstatus(fd, S_REQUEST_TOO_LARGE);
}
memcpy(r->field[i], p, q - p + 1);
/* go to next line */
p = q + (sizeof("\r\n") - 1);
}
return 0;
}
static enum status
senddir(int fd, char *name, struct request *r)
{
struct dirent **e;
size_t len, i;
int dirlen;
static char resheader[HEADER_MAX], buf[BUFSIZ], t[TIMESTAMP_LEN];
/* read directory */
if ((dirlen = scandir(name, &e, NULL, alphasort)) < 0) {
return sendstatus(fd, S_FORBIDDEN);
}
/* send header as late as possible */
len = snprintf(resheader, sizeof(resheader),
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Type: text/html\r\n"
"\r\n",
S_OK, status_str[S_OK], timestamp(0, t));
if (sendbuffer(fd, resheader, len)) {
return S_REQUEST_TIMEOUT;
}
if (r->method == M_GET) {
/* listing header */
if ((len = snprintf(buf, sizeof(buf),
"<!DOCTYPE html>\n<html>\n\t<head>"
"<title>Index of %s</title></head>\n"
"\t<body>\n\t\t<a href=\"..\">..</a>",
name)) >= sizeof(buf)) {
return S_INTERNAL_SERVER_ERROR;
}
if (sendbuffer(fd, buf, len)) {
return S_REQUEST_TIMEOUT;
}
/* listing */
for (i = 0; i < dirlen; i++) {
/* skip hidden files, "." and ".." */
if (e[i]->d_name[0] == '.') {
continue;
}
/* entry line */
if ((len = snprintf(buf, sizeof(buf),
"<br />\n\t\t<a href=\"%s\">%s</a>",
e[i]->d_name, e[i]->d_name))
>= sizeof(buf)) { >= sizeof(buf)) {
return S_INTERNAL_SERVER_ERROR; return S_INTERNAL_SERVER_ERROR;
} }
written = 0; if (sendbuffer(fd, buf, len)) {
while (buflen > 0) { return S_REQUEST_TIMEOUT;
if ((bread = write(fd, buf + written, buflen)) < 0) { }
}
/* listing footer */
if (sendbuffer(fd, "\n\t</body>\n</html>",
sizeof("\n\t</body>\n</html>") - 1)) {
return S_REQUEST_TIMEOUT; return S_REQUEST_TIMEOUT;
} }
written += bread;
buflen -= bread;
} }
return S_OK; return S_OK;
} }
static int static enum status
handle(int infd, char **url) sendfile(int fd, char *name, struct request *r, struct stat *st, char *mime,
off_t lower, off_t upper)
{ {
FILE *fp; FILE *fp;
enum status s;
size_t len;
ssize_t bread, bwritten;
off_t remaining;
int range;
static char resheader[HEADER_MAX], buf[BUFSIZ], *p, t1[TIMESTAMP_LEN],
t2[TIMESTAMP_LEN];
/* open file */
if (!(fp = fopen(name, "r"))) {
return sendstatus(fd, S_FORBIDDEN);
}
/* seek to lower bound */
if (fseek(fp, lower, SEEK_SET)) {
return sendstatus(fd, S_INTERNAL_SERVER_ERROR);
}
/* send header as late as possible */
range = r->field[REQ_RANGE][0];
s = range ? S_PARTIAL_CONTENT : S_OK;
len = snprintf(resheader, sizeof(resheader),
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Last-Modified: %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n",
s, status_str[s], timestamp(0, t1),
timestamp(st->st_mtim.tv_sec, t2), mime, upper - lower);
if (range) {
len += snprintf(resheader + len, sizeof(resheader) - len,
"Content-Range: bytes %zu-%zu/%zu\r\n",
lower, upper - 1, st->st_size);
}
len += snprintf(resheader + len, sizeof(resheader) - len, "\r\n");
if (sendbuffer(fd, resheader, len)) {
return S_REQUEST_TIMEOUT;
}
if (r->method == M_GET) {
/* write data until upper bound is hit */
remaining = upper - lower + 1;
while ((bread = fread(buf, 1, MIN(sizeof(buf), remaining), fp))) {
if (bread < 0) {
return S_INTERNAL_SERVER_ERROR;
}
remaining -= bread;
p = buf;
while (bread > 0) {
bwritten = write(fd, p, bread);
if (bwritten <= 0) {
return S_REQUEST_TIMEOUT;
}
bread -= bwritten;
p += bwritten;
}
}
}
return s;
}
static enum status
sendresponse(int fd, struct request *r)
{
struct stat st; struct stat st;
size_t reqlen, urllen, i; struct tm tm;
ssize_t off, buflen, written; size_t len, i;
long lower, upper, fsize, remaining; off_t lower, upper;
int needredirect, ret; static char realtarget[PATH_MAX], resheader[HEADER_MAX],
static char req[MAXREQLEN], buf[BUFSIZ], tmptarget[PATH_MAX], t[TIMESTAMP_LEN];
urlenc[PATH_MAX], urldec[PATH_MAX],
urldecnorm[PATH_MAX], urldecnormind[PATH_MAX],
reqhost[256], range[128], modsince[30];
char *p, *q, *mime; char *p, *q, *mime;
/* get request header */ /* check method */
for (reqlen = 0; ;) { if (r->method != M_GET && r->method != M_HEAD) {
if ((off = read(infd, req + reqlen, MAXREQLEN - reqlen)) < 0) { return sendstatus(fd, S_METHOD_NOT_ALLOWED);
return sendstatus(S_REQUEST_TIMEOUT, infd);
} else if (off == 0) {
break;
}
reqlen += off;
if (reqlen >= 4 && !memcmp(req + reqlen - 4, "\r\n\r\n", 4)) {
break;
}
if (reqlen == MAXREQLEN) {
return sendstatus(S_REQUEST_TOO_LARGE, infd);
}
}
if (reqlen < 2) {
return sendstatus(S_BAD_REQUEST, infd);
}
reqlen -= 2; /* remove last \r\n */
req[reqlen] = '\0'; /* make it safe */
/* parse request line */
if (reqlen < 3) {
return sendstatus(S_BAD_REQUEST, infd);
} else if (strncmp(req, "GET", sizeof("GET") - 1)) {
return sendstatus(S_METHOD_NOT_ALLOWED, infd);
} else if (req[3] != ' ') {
return sendstatus(S_BAD_REQUEST, infd);
}
for (p = req + sizeof("GET ") - 1; *p && *p != ' '; p++)
;
if (!*p) {
return sendstatus(S_BAD_REQUEST, infd);
}
*p = '\0';
if (snprintf(urlenc, sizeof(urlenc), "%s",
req + sizeof("GET ") - 1) >= sizeof(urlenc)) {
return sendstatus(S_BAD_REQUEST, infd);
}
*url = urldecnorm;
if (!strlen(urlenc)) {
return sendstatus(S_BAD_REQUEST, infd);
}
p += sizeof(" ") - 1;
if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) {
return sendstatus(S_BAD_REQUEST, infd);
}
p += sizeof("HTTP/") - 1;
if (strncmp(p, "1.0", sizeof("1.0") - 1) &&
strncmp(p, "1.1", sizeof("1.1") - 1)) {
return sendstatus(S_VERSION_NOT_SUPPORTED, infd);
}
p += sizeof("1.*") - 1;
if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) {
return sendstatus(S_BAD_REQUEST, infd);
}
p += sizeof("\r\n") - 1;
/* parse header fields */
for (; (q = strstr(p, "\r\n")); p = q + sizeof("\r\n") - 1) {
*q = '\0';
if (!strncmp(p, "Host:", sizeof("Host:") - 1)) {
p += sizeof("Host:") - 1;
while (isspace(*p)) {
p++;
}
if (snprintf(reqhost, sizeof(reqhost), "%s", p) >=
sizeof(reqhost)) {
return sendstatus(S_INTERNAL_SERVER_ERROR,
infd);
}
} else if (!strncmp(p, "Range:", sizeof("Range:") - 1)) {
p += sizeof("Range:") - 1;
while (isspace(*p)) {
p++;
}
if (snprintf(range, sizeof(range), "%s", p) >=
sizeof(range)) {
return sendstatus(S_INTERNAL_SERVER_ERROR,
infd);
}
} else if (!strncmp(p, "If-Modified-Since:",
sizeof("If-Modified-Since:") - 1)) {
p+= sizeof("If-Modified-Since:") - 1;
while (isspace(*p)) {
p++;
}
if (snprintf(modsince, sizeof(modsince), "%s", p) >=
sizeof(modsince)) {
return sendstatus(S_INTERNAL_SERVER_ERROR,
infd);
}
}
} }
/* normalization */ /* normalize target */
needredirect = 0; if (!realpath(r->target, realtarget)) {
decode(urlenc, urldec); return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND);
if (!realpath(urldec, urldecnorm)) {
/* todo: break up the cases */
return sendstatus((errno == EACCES) ? S_FORBIDDEN :
S_NOT_FOUND, infd);
} }
/* hidden path? */ /* reject hidden target */
if (urldecnorm[0] == '.' || strstr(urldecnorm, "/.")) { if (realtarget[0] == '.' || strstr(realtarget, "/.")) {
return sendstatus(S_FORBIDDEN, infd); return sendstatus(fd, S_FORBIDDEN);
} }
/* check if file or directory */
if (stat(urldecnorm, &st) < 0) { /* stat the target */
/* todo: break up the cases */ if (stat(realtarget, &st) < 0) {
return sendstatus(S_NOT_FOUND, infd); return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND);
} }
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
/* add / at the end, was removed by realpath */ /* add / to target if not present */
urllen = strlen(urldecnorm); len = strlen(realtarget);
if (urldecnorm[urllen - 1] != '/') { if (len == PATH_MAX - 2) {
urldecnorm[urllen + 1] = '\0'; return sendstatus(fd, S_REQUEST_TOO_LARGE);
urldecnorm[urllen] = '/'; }
if (len && realtarget[len - 1] != '/') {
realtarget[len] = '/';
realtarget[len + 1] = '\0';
}
} }
/* is a / at the end on the raw string? */ /* redirect if targets differ */
urllen = strlen(urldec); if (strcmp(r->target, realtarget)) {
if (urldec[urllen - 1] != '/') { /* encode realtarget */
needredirect = 1; encode(realtarget, tmptarget);
} else if (!needredirect) {
/* check index */ len = snprintf(resheader, sizeof(resheader),
if (snprintf(urldecnormind, sizeof(urldecnormind), "HTTP/1.1 %d %s\r\n"
"%s/%s", urldecnorm, docindex) >= "Date: %s\r\n"
sizeof(urldecnorm)) { "Connection: close\r\n"
return sendstatus(S_BAD_REQUEST, infd); "Location: %s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY], timestamp(0, t),
tmptarget);
return sendbuffer(fd, resheader, len) ? S_REQUEST_TIMEOUT :
S_MOVED_PERMANENTLY;
} }
if (stat(urldecnormind, &st) < 0) {
/* no index, serve dir */ if (S_ISDIR(st.st_mode)) {
if (!listdirs) { /* append docindex to target */
return sendstatus(S_FORBIDDEN, infd); if (snprintf(realtarget, sizeof(realtarget), "%s%s",
r->target, docindex) >= sizeof(realtarget)) {
return sendstatus(fd, S_REQUEST_TOO_LARGE);
} }
return listdir(urldecnorm, infd);
/* stat the docindex, which must be a regular file */
if (stat(realtarget, &st) < 0 || !S_ISREG(st.st_mode)) {
if (listdirs) {
/* remove index suffix and serve dir */
realtarget[strlen(realtarget) -
strlen(docindex)] = '\0';
return senddir(fd, realtarget, r);
} else {
/* reject */
if (!S_ISREG(st.st_mode) || errno == EACCES) {
return sendstatus(fd, S_FORBIDDEN);
} else {
return sendstatus(fd, S_NOT_FOUND);
} }
} }
} }
if (strcmp(urldec, urldecnorm)) {
needredirect = 1;
} }
if (needredirect) {
encode(urldecnorm, urlenc); /* modified since */
return sendstatus(S_MOVED_PERMANENTLY, infd, urlenc, if (r->field[REQ_MOD][0]) {
reqhost[0] ? reqhost : host); /* parse field */
if (!strptime(r->field[REQ_MOD], "%a, %d %b %Y %T GMT", &tm)) {
return sendstatus(fd, S_BAD_REQUEST);
}
/* compare with last modification date of the file */
if (difftime(st.st_mtim.tv_sec, mktime(&tm)) <= 0) {
len = snprintf(resheader, sizeof(resheader),
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"\r\n",
S_NOT_MODIFIED, status_str[S_NOT_MODIFIED],
timestamp(0, t));
if (sendbuffer(fd, resheader, len)) {
return S_REQUEST_TIMEOUT;
}
}
} }
/* range */ /* range */
lower = 0; lower = 0;
upper = LONG_MAX; upper = st.st_size;
if (range[0]) {
if (strncmp(range, "bytes=", sizeof("bytes=") - 1)) { if (r->field[REQ_RANGE][0]) {
return sendstatus(S_BAD_REQUEST, infd); /* parse field */
p = r->field[REQ_RANGE];
if (strncmp(p, "bytes=", sizeof("bytes=") - 1)) {
return sendstatus(fd, S_BAD_REQUEST);
} }
p = range + sizeof("bytes=") - 1; p += sizeof("bytes=") - 1;
if (!(q = strchr(p, '-'))) { if (!(q = strchr(p, '-'))) {
return sendstatus(S_BAD_REQUEST, infd); return sendstatus(fd, S_BAD_REQUEST);
} }
*(q++) = '\0'; *(q++) = '\0';
if (p[0]) { if (p[0]) {
@ -451,14 +613,17 @@ handle(int infd, char **url)
if (q[0]) { if (q[0]) {
upper = atoi(q); upper = atoi(q);
} }
/* sanitize range */
if (lower < 0 || upper < 0 || lower > upper) {
return sendstatus(fd, S_BAD_REQUEST);
}
upper = MIN(st.st_size, upper);
} }
/* serve file */ /* mime */
if (!(fp = fopen(urldecnorm, "r"))) {
return sendstatus(S_FORBIDDEN, infd);
}
mime = "text/plain"; mime = "text/plain";
if ((p = strrchr(urldecnorm, '.'))) { if ((p = strrchr(realtarget, '.'))) {
for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) { for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) {
if (!strcmp(mimes[i].ext, p + 1)) { if (!strcmp(mimes[i].ext, p + 1)) {
mime = mimes[i].type; mime = mimes[i].type;
@ -466,59 +631,22 @@ handle(int infd, char **url)
} }
} }
} }
if (fseek(fp, 0, SEEK_END) || (fsize = ftell(fp)) < 0) {
return sendstatus(S_INTERNAL_SERVER_ERROR, infd);
}
rewind(fp);
if (fsize && upper > fsize) {
upper = fsize - 1;
}
if (fseek(fp, lower, SEEK_SET)) {
return sendstatus(S_INTERNAL_SERVER_ERROR, infd);
}
if (!range[0]) {
if ((ret = sendstatus(S_OK, infd, mime, (long)fsize)) != S_OK) {
return ret;
}
} else {
if ((ret = sendstatus(S_PARTIAL_CONTENT, infd, mime, lower,
upper, fsize)) != S_PARTIAL_CONTENT) {
return ret;
}
}
remaining = (upper - lower) + 1;
while ((buflen = fread(buf, 1, MIN(sizeof(buf), remaining),
fp))) {
remaining -= buflen;
if (buflen < 0) {
return S_INTERNAL_SERVER_ERROR;
}
p = buf;
while (buflen > 0) {
written = write(infd, p, buflen);
if (written <= 0) {
return S_REQUEST_TIMEOUT;
}
buflen -= written;
p += written;
}
}
return S_OK; return sendfile(fd, realtarget, r, &st, mime, lower, upper);
} }
static void static void
serve(int insock) serve(int insock)
{ {
struct request r;
struct sockaddr_storage in_sa; struct sockaddr_storage in_sa;
struct timeval tv; struct timeval tv;
pid_t p; pid_t p;
socklen_t in_sa_len; socklen_t in_sa_len;
time_t t; time_t t;
enum stati status; enum status status;
int infd; int infd;
char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], *url = "", char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], tstmp[25];
tstmp[25];
while (1) { while (1) {
/* accept incoming connections */ /* accept incoming connections */
@ -530,9 +658,10 @@ serve(int insock)
continue; continue;
} }
/* fork and handle */
switch ((p = fork())) { switch ((p = fork())) {
case -1: case -1:
fprintf(stderr, "%s: fork: %s", argv0, fprintf(stderr, "%s: fork: %s\n", argv0,
strerror(errno)); strerror(errno));
break; break;
case 0: case 0:
@ -550,7 +679,10 @@ serve(int insock)
return; return;
} }
status = handle(infd, &url); /* handle request */
if (!(status = getrequest(infd, &r))) {
status = sendresponse(infd, &r);
}
/* write output to log */ /* write output to log */
t = time(NULL); t = time(NULL);
@ -561,12 +693,14 @@ serve(int insock)
inet_ntop(AF_INET, inet_ntop(AF_INET,
&(((struct sockaddr_in *)&in_sa)->sin_addr), &(((struct sockaddr_in *)&in_sa)->sin_addr),
inip4, sizeof(inip4)); inip4, sizeof(inip4));
printf("%s\t%s\t%d\t%s\n", tstmp, inip4, status, url); printf("%s\t%s\t%d\t%s\n", tstmp, inip4,
status, r.target);
} else { } else {
inet_ntop(AF_INET6, inet_ntop(AF_INET6,
&(((struct sockaddr_in6*)&in_sa)->sin6_addr), &(((struct sockaddr_in6*)&in_sa)->sin6_addr),
inip6, sizeof(inip6)); inip6, sizeof(inip6));
printf("%s\t%s\t%d\t%s\n", tstmp, inip6, status, url); printf("%s\t%s\t%d\t%s\n", tstmp, inip6,
status, r.target);
} }
/* clean up and finish */ /* clean up and finish */
@ -575,6 +709,7 @@ serve(int insock)
close(infd); close(infd);
_exit(0); _exit(0);
default: default:
/* close the connection in the parent */
close(infd); close(infd);
} }
} }