quark

quark web server
git clone git://git.suckless.org/quark
Log | Files | Refs | LICENSE

commit 6347e2ec3eac01759fa85572e665c49511764c97
parent 29d53f65b70dd8f4ed2ae54ac17877dce29c8cf9
Author: Laslo Hunhold <dev@frign.de>
Date:   Tue, 20 Jun 2017 21:40:00 +0200

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.

Diffstat:
MLICENSE | 2+-
MMakefile | 41+++++++++++++++++++----------------------
Mconfig.def.h | 5+++--
Mconfig.mk | 12++++--------
Mquark.c | 805++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
5 files changed, 497 insertions(+), 368 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -1,6 +1,6 @@ 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 purpose with or without fee is hereby granted, provided that the above diff --git 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 @@ -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 @@ -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 @@ -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)); - - 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<!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; - } - } + strftime(buf, TIMESTAMP_LEN, "%a, %d %b %Y %T GMT", gmtime(&t)); - 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; - - 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), "<!DOCTYPE html>\r\n" - "<html>\r\n<head><title>Index of %s" - "</title></head>\r\n<body>\r\n" - "<a href=\"..\">..</a><br />\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; - } - written += bread; - buflen -= bread; - } +sendbuffer(int fd, char *buf, size_t buflen) { + size_t written; + ssize_t off; - for (i = 0; i < dirlen; i++) { - if (e[i]->d_name[0] == '.') { /* hidden files, ., .. */ - continue; - } - if ((buflen = snprintf(buf, sizeof(buf), "<a href=\"%s" - "\">%s</a><br />\r\n", e[i]->d_name, - e[i]->d_name)) >= sizeof(buf)) { - return S_INTERNAL_SERVER_ERROR; - } - written = 0; - while (buflen > 0) { - if ((bread = write(fd, buf + written, buflen)) < 0) { - return S_REQUEST_TIMEOUT; - } - written += bread; - buflen -= bread; + for (written = 0; buflen > 0; written += off, buflen -= off) { + if ((off = write(fd, buf + written, buflen)) < 0) { + return 1; } } - if ((buflen = snprintf(buf, sizeof(buf), "\r\n</body></html>\r\n")) - >= sizeof(buf)) { - return S_INTERNAL_SERVER_ERROR; - } - written = 0; - while (buflen > 0) { - if ((bread = write(fd, buf + written, buflen)) < 0) { - return S_REQUEST_TIMEOUT; - } - written += bread; - buflen -= bread; - } + return 0; +} - return S_OK; +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" + "<!DOCTYPE html>\n<html>\n\t<head>\n" + "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n" + "\t\t<h1>%d %s</h1>\n\t</body>\n</html>", + 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 -handle(int infd, char **url) +getrequest(int fd, struct request *r) { - FILE *fp; - 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]; - 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); + 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; } - reqlen += off; - if (reqlen >= 4 && !memcmp(req + reqlen - 4, "\r\n\r\n", 4)) { + hlen += off; + if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { break; } - if (reqlen == MAXREQLEN) { - return sendstatus(S_REQUEST_TOO_LARGE, infd); + if (hlen == sizeof(h)) { + return sendstatus(fd, S_REQUEST_TOO_LARGE); } } - if (reqlen < 2) { - return sendstatus(S_BAD_REQUEST, infd); + + /* remove terminating empty line */ + if (hlen < 2) { + return sendstatus(fd, S_BAD_REQUEST); } - reqlen -= 2; /* remove last \r\n */ - req[reqlen] = '\0'; /* make it safe */ + hlen -= 2; + + /* null-terminate the header */ + h[hlen] = '\0'; - /* 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); + /* + * 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; + } } - for (p = req + sizeof("GET ") - 1; *p && *p != ' '; p++) - ; - if (!*p) { - return sendstatus(S_BAD_REQUEST, infd); + if (i == NUM_REQ_METHODS) { + return sendstatus(fd, S_BAD_REQUEST); } - *p = '\0'; - if (snprintf(urlenc, sizeof(urlenc), "%s", - req + sizeof("GET ") - 1) >= sizeof(urlenc)) { - return sendstatus(S_BAD_REQUEST, infd); + + /* a single space must follow the method */ + if (h[mlen] != ' ') { + return sendstatus(fd, S_BAD_REQUEST); } - *url = urldecnorm; - if (!strlen(urlenc)) { - return sendstatus(S_BAD_REQUEST, infd); + + /* 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); } - p += sizeof(" ") - 1; + 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(S_BAD_REQUEST, infd); + 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(S_VERSION_NOT_SUPPORTED, infd); + return sendstatus(fd, S_VERSION_NOT_SUPPORTED); } p += sizeof("1.*") - 1; + + /* check terminator */ if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return sendstatus(S_BAD_REQUEST, infd); + return sendstatus(fd, S_BAD_REQUEST); } + + /* basis for next step */ 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); + /* + * 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; } - } else if (!strncmp(p, "Range:", sizeof("Range:") - 1)) { - p += sizeof("Range:") - 1; - while (isspace(*p)) { - p++; + } + if (i == NUM_REQ_FIELDS) { + /* unmatched field, skip this line */ + if (!(q = strstr(p, "\r\n"))) { + return sendstatus(fd, S_BAD_REQUEST); } - if (snprintf(range, sizeof(range), "%s", p) >= - sizeof(range)) { - return sendstatus(S_INTERNAL_SERVER_ERROR, - infd); + p = q + (sizeof("\r\n") - 1); + continue; + } + + p += strlen(req_field_str[i]); + + /* a single colon must follow the field name */ + if (*p != ':') { + return sendstatus(fd, S_BAD_REQUEST); + } + + /* 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; } - } else if (!strncmp(p, "If-Modified-Since:", - sizeof("If-Modified-Since:") - 1)) { - p+= sizeof("If-Modified-Since:") - 1; - while (isspace(*p)) { - p++; + + /* 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)) { + return S_INTERNAL_SERVER_ERROR; } - if (snprintf(modsince, sizeof(modsince), "%s", p) >= - sizeof(modsince)) { - return sendstatus(S_INTERNAL_SERVER_ERROR, - infd); + if (sendbuffer(fd, buf, len)) { + return S_REQUEST_TIMEOUT; } } + + /* listing footer */ + if (sendbuffer(fd, "\n\t</body>\n</html>", + sizeof("\n\t</body>\n</html>") - 1)) { + return S_REQUEST_TIMEOUT; + } } - /* 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); + return S_OK; +} + +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); } - /* hidden path? */ - if (urldecnorm[0] == '.' || strstr(urldecnorm, "/.")) { - return sendstatus(S_FORBIDDEN, infd); + /* seek to lower bound */ + if (fseek(fp, lower, SEEK_SET)) { + return sendstatus(fd, S_INTERNAL_SERVER_ERROR); } - /* check if file or directory */ - if (stat(urldecnorm, &st) < 0) { - /* todo: break up the cases */ - return sendstatus(S_NOT_FOUND, infd); + + /* 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); } - 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] = '/'; - } - - /* 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); + 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; } - if (stat(urldecnormind, &st) < 0) { - /* no index, serve dir */ - if (!listdirs) { - return sendstatus(S_FORBIDDEN, infd); + remaining -= bread; + p = buf; + while (bread > 0) { + bwritten = write(fd, p, bread); + if (bwritten <= 0) { + return S_REQUEST_TIMEOUT; } - return listdir(urldecnorm, infd); + bread -= bwritten; + p += bwritten; } } } - if (strcmp(urldec, urldecnorm)) { - needredirect = 1; + + return s; +} + +static enum status +sendresponse(int fd, struct request *r) +{ + struct stat st; + 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; + + /* check method */ + if (r->method != M_GET && r->method != M_HEAD) { + return sendstatus(fd, S_METHOD_NOT_ALLOWED); + } + + /* normalize target */ + if (!realpath(r->target, realtarget)) { + return sendstatus(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); + } + + /* reject hidden target */ + if (realtarget[0] == '.' || strstr(realtarget, "/.")) { + return sendstatus(fd, S_FORBIDDEN); + } + + /* 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 / 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 (needredirect) { - encode(urldecnorm, urlenc); - return sendstatus(S_MOVED_PERMANENTLY, infd, urlenc, - reqhost[0] ? reqhost : host); + + 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); + } + + /* 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); + } + } + } + } + + /* 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); } - } - /* serve file */ - if (!(fp = fopen(urldecnorm, "r"))) { - return sendstatus(S_FORBIDDEN, infd); + /* sanitize range */ + if (lower < 0 || upper < 0 || lower > upper) { + return sendstatus(fd, S_BAD_REQUEST); + } + upper = MIN(st.st_size, upper); } + + /* 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); } }