[hackers] [quark] Rewrite quark from the ground up again || Laslo Hunhold

From: <git_AT_suckless.org>
Date: Tue, 20 Jun 2017 21:45:02 +0200 (CEST)

commit 6347e2ec3eac01759fa85572e665c49511764c97
Author: Laslo Hunhold <dev_AT_frign.de>
AuthorDate: Tue Jun 20 21:40:00 2017 +0200
Commit: Laslo Hunhold <dev_AT_frign.de>
CommitDate: Tue Jun 20 21:40:00 2017 +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.

diff --git a/LICENSE b/LICENSE
index 0b028e1..23dc5f5 100644
--- a/LICENSE
+++ b/LICENSE
_AT_@ -1,6 +1,6 @@
 ISC-License
 
-(c) 2016 Laslo Hunhold <dev_AT_frign.de>
+(c) 2016-2017 Laslo Hunhold <dev_AT_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
index 4db2558..5f0d796 100644
--- a/Makefile
+++ b/Makefile
_AT_@ -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 $_AT_ 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 $_AT_ $(CPPFLAGS) $(CFLAGS) quark.c $(LDFLAGS)
 
 config.h:
         cp config.def.h $_AT_
 
 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
_AT_@ -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";
_AT_@ -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
_AT_@ -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
_AT_@ -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,
_AT_@ -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",
_AT_@ -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
_AT_@ -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++;
_AT_@ -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]) {
_AT_@ -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 */
_AT_@ -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:
_AT_@ -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);
_AT_@ -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 */
_AT_@ -575,6 +709,7 @@ serve(int insock)
                         close(infd);
                         _exit(0);
                 default:
+ /* close the connection in the parent */
                         close(infd);
                 }
         }
Received on Tue Jun 20 2017 - 21:45:02 CEST

This archive was generated by hypermail 2.3.0 : Tue Jun 20 2017 - 21:48:20 CEST