From 6347e2ec3eac01759fa85572e665c49511764c97 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Tue, 20 Jun 2017 21:40:00 +0200 Subject: [PATCH] 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. --- LICENSE | 2 +- Makefile | 41 ++- config.def.h | 5 +- config.mk | 12 +- quark.c | 835 ++++++++++++++++++++++++++++++--------------------- 5 files changed, 512 insertions(+), 383 deletions(-) diff --git a/LICENSE b/LICENSE index 0b028e1..23dc5f5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC-License -(c) 2016 Laslo Hunhold +(c) 2016-2017 Laslo Hunhold Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/Makefile b/Makefile index 4db2558..5f0d796 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,36 @@ +# See LICENSE file for copyright and license details # quark - simple web server +.POSIX: include config.mk all: quark -quark: quark.o config.h config.mk - ${CC} -o $@ quark.o ${LDFLAGS} - -quark.o: quark.c config.h config.mk - ${CC} -c ${CFLAGS} quark.c +quark: quark.c arg.h config.h config.mk + $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) quark.c $(LDFLAGS) config.h: cp config.def.h $@ clean: - rm -f quark quark.o quark-${VERSION}.tar.gz + rm -f quark -dist: clean - mkdir -p quark-${VERSION} +dist: + rm -rf "quark-$(VERSION)" + mkdir -p "quark-$(VERSION)" cp -R LICENSE Makefile arg.h config.def.h config.mk quark.1 \ - quark.c quark-${VERSION} - tar -cf quark-${VERSION}.tar quark-${VERSION} - gzip quark-${VERSION}.tar - rm -rf quark-${VERSION} + quark.c "quark-$(VERSION)" + tar -cf - "quark-$(VERSION)" | gzip -c > "quark-$(VERSION).tar.gz" + rm -rf "quark-$(VERSION)" install: all - mkdir -p ${DESTDIR}${PREFIX}/bin - cp -f quark ${DESTDIR}${PREFIX}/bin - chmod 755 ${DESTDIR}${PREFIX}/bin/quark - mkdir -p ${DESTDIR}${MANPREFIX}/man1 - cp quark.1 ${DESTDIR}${MANPREFIX}/man1/quark.1 - chmod 644 ${DESTDIR}${MANPREFIX}/man1/quark.1 + mkdir -p "$(DESTDIR)$(PREFIX)/bin" + cp -f quark "$(DESTDIR)$(PREFIX)/bin" + chmod 755 "$(DESTDIR)$(PREFIX)/bin/quark" + mkdir -p "$(DESTDIR)$(MANPREFIX)/man1" + cp quark.1 "$(DESTDIR)$(MANPREFIX)/man1/quark.1" + chmod 644 "$(DESTDIR)$(MANPREFIX)/man1/quark.1" uninstall: - rm -f ${DESTDIR}${PREFIX}/bin/quark - rm -f ${DESTDIR}${MANPREFIX}/man1/quark.1 - -.PHONY: all options clean dist install uninstall + rm -f "$(DESTDIR)$(PREFIX)/bin/quark" + rm -f "$(DESTDIR)$(MANPREFIX)/man1/quark.1" diff --git a/config.def.h b/config.def.h index fe74ced..440ae0f 100644 --- a/config.def.h +++ b/config.def.h @@ -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 *servedir = "."; static const char *docindex = "index.html"; @@ -7,7 +7,8 @@ static const char *user = "nobody"; static const char *group = "nogroup"; static const int maxnprocs = 512; -#define MAXREQLEN 4096 /* >= 4 */ +#define HEADER_MAX 4096 +#define FIELD_MAX 200 static const struct { char *ext; diff --git a/config.mk b/config.mk index 305f59b..b6e5e96 100644 --- a/config.mk +++ b/config.mk @@ -5,16 +5,12 @@ VERSION = 0 # paths PREFIX = /usr/local -MANPREFIX = ${PREFIX}/share/man - -# includes and libs -INCS = -I. -I/usr/include -LIBS = -L/usr/lib -lc +MANPREFIX = $(PREFIX)/man # flags -CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -CFLAGS = -g -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS} -LDFLAGS = ${LIBS} +CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE +CFLAGS = -std=c99 -pedantic -Wall -Os +LDFLAGS = -s # compiler and linker CC = cc diff --git a/quark.c b/quark.c index 1faad5d..571ce53 100644 --- a/quark.c +++ b/quark.c @@ -30,10 +30,60 @@ char *argv0; #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_PARTIAL_CONTENT = 206, S_MOVED_PERMANENTLY = 301, + S_NOT_MODIFIED = 304, S_BAD_REQUEST = 400, S_FORBIDDEN = 403, S_NOT_FOUND = 404, @@ -44,10 +94,11 @@ enum stati { S_VERSION_NOT_SUPPORTED = 505, }; -static char *statistr[] = { +static char *status_str[] = { [S_OK] = "OK", [S_PARTIAL_CONTENT] = "Partial Content", [S_MOVED_PERMANENTLY] = "Moved Permanently", + [S_NOT_MODIFIED] = "Not Modified", [S_BAD_REQUEST] = "Bad Request", [S_FORBIDDEN] = "Forbidden", [S_NOT_FOUND] = "Not Found", @@ -58,107 +109,14 @@ static char *statistr[] = { [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", }; -#undef MIN -#define MIN(x,y) ((x) < (y) ? (x) : (y)) - static char * -timestamp(time_t t) +timestamp(time_t t, char buf[TIMESTAMP_LEN]) { - static char s[30]; - if (!t) 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; -} - -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\r\n\r\n" - "\t%d %s" - "\r\n\t

%d %s

\r\n" - "\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; + return buf; } static size_t @@ -190,9 +148,9 @@ encode(char src[PATH_MAX], char dest[PATH_MAX]) char *s; for (s = src, i = 0; *s; s++) { - if (isalnum(*s) || *s == '~' || *s == '-' || *s == '.' || - *s == '_' || *s > 127) { - i += snprintf(dest + i, PATH_MAX - i, "%%%02X", *s); + if (iscntrl(*s) || (unsigned char)*s > 127) { + i += snprintf(dest + i, PATH_MAX - i, "%%%02X", + (unsigned char)*s); } else { dest[i] = *s; i++; @@ -203,246 +161,450 @@ encode(char src[PATH_MAX], char dest[PATH_MAX]) } static int -listdir(char *dir, int fd) -{ - struct dirent **e = NULL; - static char buf[BUFSIZ]; - size_t buflen; - ssize_t bread, written; - int dirlen, ret, i; +sendbuffer(int fd, char *buf, size_t buflen) { + size_t written; + ssize_t off; - if ((dirlen = scandir(dir, &e, NULL, alphasort)) < 0) { - return sendstatus(S_FORBIDDEN, fd); - } - if ((ret = sendstatus(S_OK, fd, "text/html", (long)-1)) != S_OK) { - return ret; - } - if ((buflen = snprintf(buf, sizeof(buf), "\r\n" - "\r\nIndex of %s" - "\r\n\r\n" - "..
\r\n", - dir)) >= sizeof(buf)) { - return S_INTERNAL_SERVER_ERROR; - } - written = 0; - while (buflen > 0) { - if ((bread = write(fd, buf + written, buflen)) < 0) { - return S_REQUEST_TIMEOUT; + for (written = 0; buflen > 0; written += off, buflen -= off) { + if ((off = write(fd, buf + written, buflen)) < 0) { + return 1; } - written += bread; - buflen -= bread; } - for (i = 0; i < dirlen; i++) { - if (e[i]->d_name[0] == '.') { /* hidden files, ., .. */ + return 0; +} + +static enum status +sendstatus(int fd, enum status s) +{ + static char res[4096], t[TIMESTAMP_LEN]; + size_t len; + + /* assemble error response */ + len = snprintf(res, sizeof(res), + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "%s" + "Content-Type: text/html\r\n" + "\r\n" + "\n\n\t\n" + "\t\t%d %s\n\t\n\t\n" + "\t\t

%d %s

\n\t\n", + s, status_str[s], timestamp(0, t), + (s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "", + s, status_str[s], s, status_str[s]); + + return sendbuffer(fd, res, len) ? S_REQUEST_TIMEOUT : s; +} + +static int +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; } - if ((buflen = snprintf(buf, sizeof(buf), "%s
\r\n", e[i]->d_name, - e[i]->d_name)) >= sizeof(buf)) { - return S_INTERNAL_SERVER_ERROR; + + p += strlen(req_field_str[i]); + + /* a single colon must follow the field name */ + if (*p != ':') { + return sendstatus(fd, S_BAD_REQUEST); } - written = 0; - while (buflen > 0) { - if ((bread = write(fd, buf + written, buflen)) < 0) { - return S_REQUEST_TIMEOUT; - } - written += bread; - buflen -= bread; + + /* 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); } - if ((buflen = snprintf(buf, sizeof(buf), "\r\n\r\n")) - >= sizeof(buf)) { - return S_INTERNAL_SERVER_ERROR; + 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); } - written = 0; - while (buflen > 0) { - if ((bread = write(fd, buf + written, buflen)) < 0) { + + /* 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), + "\n\n\t" + "Index of %s\n" + "\t\n\t\t..", + 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), + "
\n\t\t%s", + e[i]->d_name, e[i]->d_name)) + >= sizeof(buf)) { + return S_INTERNAL_SERVER_ERROR; + } + if (sendbuffer(fd, buf, len)) { + return S_REQUEST_TIMEOUT; + } + } + + /* listing footer */ + if (sendbuffer(fd, "\n\t\n", + sizeof("\n\t\n") - 1)) { return S_REQUEST_TIMEOUT; } - written += bread; - buflen -= bread; } return S_OK; } -static int -handle(int infd, char **url) +static enum status +sendfile(int fd, char *name, struct request *r, struct stat *st, char *mime, + off_t lower, off_t upper) { 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; - size_t reqlen, urllen, i; - ssize_t off, buflen, written; - long lower, upper, fsize, remaining; - int needredirect, ret; - static char req[MAXREQLEN], buf[BUFSIZ], - urlenc[PATH_MAX], urldec[PATH_MAX], - urldecnorm[PATH_MAX], urldecnormind[PATH_MAX], - reqhost[256], range[128], modsince[30]; + struct tm tm; + size_t len, i; + off_t lower, upper; + static char realtarget[PATH_MAX], resheader[HEADER_MAX], + tmptarget[PATH_MAX], t[TIMESTAMP_LEN]; char *p, *q, *mime; - /* get request header */ - for (reqlen = 0; ;) { - if ((off = read(infd, req + reqlen, MAXREQLEN - reqlen)) < 0) { - 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); - } - } + /* check method */ + if (r->method != M_GET && r->method != M_HEAD) { + return sendstatus(fd, S_METHOD_NOT_ALLOWED); } - /* normalization */ - needredirect = 0; - decode(urlenc, urldec); - if (!realpath(urldec, urldecnorm)) { - /* todo: break up the cases */ - return sendstatus((errno == EACCES) ? S_FORBIDDEN : - S_NOT_FOUND, infd); + /* normalize target */ + if (!realpath(r->target, realtarget)) { + return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } - /* hidden path? */ - if (urldecnorm[0] == '.' || strstr(urldecnorm, "/.")) { - return sendstatus(S_FORBIDDEN, infd); + /* reject hidden target */ + if (realtarget[0] == '.' || strstr(realtarget, "/.")) { + return sendstatus(fd, S_FORBIDDEN); } - /* check if file or directory */ - if (stat(urldecnorm, &st) < 0) { - /* todo: break up the cases */ - return sendstatus(S_NOT_FOUND, infd); + + /* stat the target */ + if (stat(realtarget, &st) < 0) { + return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } + if (S_ISDIR(st.st_mode)) { - /* add / at the end, was removed by realpath */ - urllen = strlen(urldecnorm); - if (urldecnorm[urllen - 1] != '/') { - urldecnorm[urllen + 1] = '\0'; - urldecnorm[urllen] = '/'; + /* add / to target if not present */ + len = strlen(realtarget); + if (len == PATH_MAX - 2) { + return sendstatus(fd, S_REQUEST_TOO_LARGE); + } + if (len && realtarget[len - 1] != '/') { + realtarget[len] = '/'; + realtarget[len + 1] = '\0'; + } + } + + /* redirect if targets differ */ + if (strcmp(r->target, realtarget)) { + /* encode realtarget */ + encode(realtarget, tmptarget); + + len = snprintf(resheader, sizeof(resheader), + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "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 (S_ISDIR(st.st_mode)) { + /* append docindex to target */ + if (snprintf(realtarget, sizeof(realtarget), "%s%s", + r->target, docindex) >= sizeof(realtarget)) { + return sendstatus(fd, S_REQUEST_TOO_LARGE); } - /* is a / at the end on the raw string? */ - urllen = strlen(urldec); - if (urldec[urllen - 1] != '/') { - needredirect = 1; - } else if (!needredirect) { - /* check index */ - if (snprintf(urldecnormind, sizeof(urldecnormind), - "%s/%s", urldecnorm, docindex) >= - sizeof(urldecnorm)) { - return sendstatus(S_BAD_REQUEST, infd); - } - if (stat(urldecnormind, &st) < 0) { - /* no index, serve dir */ - if (!listdirs) { - return sendstatus(S_FORBIDDEN, 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); } - return listdir(urldecnorm, infd); } } } - if (strcmp(urldec, urldecnorm)) { - needredirect = 1; - } - if (needredirect) { - encode(urldecnorm, urlenc); - return sendstatus(S_MOVED_PERMANENTLY, infd, urlenc, - reqhost[0] ? reqhost : host); + + /* modified since */ + if (r->field[REQ_MOD][0]) { + /* 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 */ lower = 0; - upper = LONG_MAX; - if (range[0]) { - if (strncmp(range, "bytes=", sizeof("bytes=") - 1)) { - return sendstatus(S_BAD_REQUEST, infd); + upper = st.st_size; + + if (r->field[REQ_RANGE][0]) { + /* 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, '-'))) { - return sendstatus(S_BAD_REQUEST, infd); + return sendstatus(fd, S_BAD_REQUEST); } *(q++) = '\0'; if (p[0]) { @@ -451,74 +613,40 @@ handle(int infd, char **url) if (q[0]) { 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 */ - if (!(fp = fopen(urldecnorm, "r"))) { - return sendstatus(S_FORBIDDEN, infd); - } + /* mime */ mime = "text/plain"; - if ((p = strrchr(urldecnorm, '.'))) { - for (i = 0; i < sizeof(mimes)/sizeof(*mimes); i++) { + if ((p = strrchr(realtarget, '.'))) { + for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) { if (!strcmp(mimes[i].ext, p + 1)) { mime = mimes[i].type; break; } } } - 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 serve(int insock) { + struct request r; struct sockaddr_storage in_sa; struct timeval tv; pid_t p; socklen_t in_sa_len; time_t t; - enum stati status; + enum status status; int infd; - char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], *url = "", - tstmp[25]; + char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], tstmp[25]; while (1) { /* accept incoming connections */ @@ -530,9 +658,10 @@ serve(int insock) continue; } + /* fork and handle */ switch ((p = fork())) { case -1: - fprintf(stderr, "%s: fork: %s", argv0, + fprintf(stderr, "%s: fork: %s\n", argv0, strerror(errno)); break; case 0: @@ -550,7 +679,10 @@ serve(int insock) return; } - status = handle(infd, &url); + /* handle request */ + if (!(status = getrequest(infd, &r))) { + status = sendresponse(infd, &r); + } /* write output to log */ t = time(NULL); @@ -561,12 +693,14 @@ serve(int insock) inet_ntop(AF_INET, &(((struct sockaddr_in *)&in_sa)->sin_addr), 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 { inet_ntop(AF_INET6, &(((struct sockaddr_in6*)&in_sa)->sin6_addr), 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 */ @@ -575,6 +709,7 @@ serve(int insock) close(infd); _exit(0); default: + /* close the connection in the parent */ close(infd); } }