commit 2714819dfc639098d0531eb3d4f0f5f23708059a
parent 0823ba4c3e480fb5e2c246b8ac6c4783d866ab87
Author: Laslo Hunhold <dev@frign.de>
Date:   Mon, 14 Sep 2020 13:45:24 +0200
Make the serving process interruptible
Ever since I joined suckless and found out that there had been an
(inofficial and cancelled) effort to turn quark into a polling-webserver
(instead of a forking-webserver), I was intrigued to pick up the task
and make it happen.
Back then, my C skills weren't nearly as good, and I had no hopes of
making it possible. Now, this commit marks a major step towards this
goal.
Given the static nature of quark, I wanted to try something out that
is not really possible with a "dynamic" server: Making the serving
process interruptible in constant memory (except dir-listings of
course). This can easily be extended to a polling architecture later
on, but it most importantly warrants a non-blocking I/O scheme and
makes the server more or less immune to sloth attacks (i.e. clients
sending requests very slowly), and provides a more flexible approach to
connections. Any thread can pick up a connection and continue work on
it, without requiring a separate process for each (which might hit the
forking limit at some point). If we hit a point where all connections
are busy (due to many sloth attacks), one can apply arbitrary complex
logic to "cancel" connections that show malicious behaviour (e.g. taking
a long time to send the request header, etc.).
The following aspects were added/changed to introduce the
interruptibility.
 - Define a general purpose "buffer" struct with a buffer_appendf()
   utility function.
 - Change http_send_header() to http_prepare_header_buf() and separate
   the sending part into a general-purpose function http_send_buf().
 - Modify the data_* functions to be based on a progress and operate
   on buffers. This way, we can indefinitely "interrupt" request
   serving and always "pick up" where we left off.
 - Refactor http_recv_header() to operate on the buffer struct instead
   of "raw" parameters.
 - Refactor serve() in main.c accordingly.
 - Introduce BUFFER_SIZE in config.h, which controls the buffer size each
   connection has.
 - Refactor Makefile dependencies and employ strict first-level-header-
   usage (i.e. we explicitly specify what we use with includes in each
   compilation unit, so make(1) can figure the dependencies out; most
   prominently, this moves the arg.h-include into main.c, and requires
   ifdef-guards for config.h).
Signed-off-by: Laslo Hunhold <dev@frign.de>
Diffstat:
| M | Makefile | | | 6 | +++--- | 
| M | config.def.h | | | 9 | +++++++-- | 
| M | data.c | | | 131 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- | 
| M | data.h | | | 13 | ++++++++++--- | 
| M | http.c | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++-------------------------------- | 
| M | http.h | | | 18 | ++++++------------ | 
| M | main.c | | | 61 | ++++++++++++++++++++++++++++++++++++++++++++++++++++--------- | 
| M | util.c | | | 24 | ++++++++++++++++++++++++ | 
| M | util.h | | | 11 | ++++++++++- | 
9 files changed, 260 insertions(+), 133 deletions(-)
diff --git a/Makefile b/Makefile
@@ -8,9 +8,9 @@ COMPONENTS = data http sock util
 
 all: quark
 
-data.o: data.c data.h util.h http.h config.mk
-http.o: http.c http.h util.h http.h data.h config.h config.mk
-main.o: main.c util.h sock.h http.h arg.h config.h config.mk
+data.o: data.c data.h http.h util.h config.mk
+http.o: http.c config.h http.h util.h config.mk
+main.o: main.c arg.h data.h http.h sock.h util.h config.mk
 sock.o: sock.c sock.h util.h config.mk
 util.o: util.c util.h config.mk
 
diff --git a/config.def.h b/config.def.h
@@ -1,5 +1,8 @@
-#define HEADER_MAX 4096
-#define FIELD_MAX  200
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define BUFFER_SIZE 4096
+#define FIELD_MAX   200
 
 /* mime-types */
 static const struct {
@@ -32,3 +35,5 @@ static const struct {
 	{ "ogv",   "video/ogg" },
 	{ "webm",  "video/webm" },
 };
+
+#endif /* CONFIG_H */
diff --git a/data.c b/data.c
@@ -7,10 +7,17 @@
 #include <time.h>
 #include <unistd.h>
 
-#include "http.h"
 #include "data.h"
+#include "http.h"
 #include "util.h"
 
+enum status (* const data_fct[])(const struct response *,
+                                 struct buffer *, size_t *) = {
+	[RESTYPE_ERROR]      = data_prepare_error_buf,
+	[RESTYPE_FILE]       = data_prepare_file_buf,
+	[RESTYPE_DIRLISTING] = data_prepare_dirlisting_buf,
+};
+
 static int
 compareent(const struct dirent **d1, const struct dirent **d2)
 {
@@ -84,7 +91,8 @@ html_escape(const char *src, char *dst, size_t dst_siz)
 }
 
 enum status
-data_send_dirlisting(int fd, const struct response *res)
+data_prepare_dirlisting_buf(const struct response *res,
+                            struct buffer *buf, size_t *progress)
 {
 	enum status ret = 0;
 	struct dirent **e;
@@ -92,24 +100,29 @@ data_send_dirlisting(int fd, const struct response *res)
 	int dirlen;
 	char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
 
+	/* reset buffer */
+	memset(buf, 0, sizeof(*buf));
+
 	/* read directory */
 	if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) {
 		return S_FORBIDDEN;
 	}
 
-	/* listing header (we use esc because sizeof(esc) >= PATH_MAX) */
-	html_escape(res->uri, esc, MIN(PATH_MAX, sizeof(esc)));
-	if (dprintf(fd,
-	            "<!DOCTYPE html>\n<html>\n\t<head>"
-	            "<title>Index of %s</title></head>\n"
-	            "\t<body>\n\t\t<a href=\"..\">..</a>",
-	            esc) < 0) {
-		ret = S_REQUEST_TIMEOUT;
-		goto cleanup;
+	if (*progress == 0) {
+		/* write listing header (sizeof(esc) >= PATH_MAX) */
+		html_escape(res->uri, esc, MIN(PATH_MAX, sizeof(esc)));
+		if (buffer_appendf(buf,
+		                   "<!DOCTYPE html>\n<html>\n\t<head>"
+		                   "<title>Index of %s</title></head>\n"
+		                   "\t<body>\n\t\t<a href=\"..\">..</a>",
+		                   esc) < 0) {
+			ret = S_REQUEST_TIMEOUT;
+			goto cleanup;
+		}
 	}
 
-	/* listing */
-	for (i = 0; i < (size_t)dirlen; i++) {
+	/* listing entries */
+	for (i = *progress; i < (size_t)dirlen; i++) {
 		/* skip hidden files, "." and ".." */
 		if (e[i]->d_name[0] == '.') {
 			continue;
@@ -117,20 +130,25 @@ data_send_dirlisting(int fd, const struct response *res)
 
 		/* entry line */
 		html_escape(e[i]->d_name, esc, sizeof(esc));
-		if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
-		            esc,
-		            (e[i]->d_type == DT_DIR) ? "/" : "",
-		            esc,
-		            suffix(e[i]->d_type)) < 0) {
-			ret = S_REQUEST_TIMEOUT;
-			goto cleanup;
+		if (buffer_appendf(buf,
+		                   "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
+		                   esc,
+		                   (e[i]->d_type == DT_DIR) ? "/" : "",
+		                   esc,
+		                   suffix(e[i]->d_type))) {
+			/* buffer full */
+			break;
 		}
 	}
+	*progress = i;
 
-	/* listing footer */
-	if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
-		ret = S_REQUEST_TIMEOUT;
-		goto cleanup;
+	if (*progress == (size_t)dirlen) {
+		/* listing footer */
+		if (buffer_appendf(buf, "\n\t</body>\n</html>\n") < 0) {
+			ret = S_REQUEST_TIMEOUT;
+			goto cleanup;
+		}
+		(*progress)++;
 	}
 
 cleanup:
@@ -143,28 +161,40 @@ cleanup:
 }
 
 enum status
-data_send_error(int fd, const struct response *res)
+data_prepare_error_buf(const struct response *res, struct buffer *buf,
+                   size_t *progress)
 {
-	if (dprintf(fd,
-	            "<!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>\n",
-	            res->status, status_str[res->status],
-		    res->status, status_str[res->status]) < 0) {
-		return S_REQUEST_TIMEOUT;
+	/* reset buffer */
+	memset(buf, 0, sizeof(*buf));
+
+	if (*progress == 0) {
+		/* write error body */
+		if (buffer_appendf(buf,
+		                   "<!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>\n",
+		                   res->status, status_str[res->status],
+			           res->status, status_str[res->status])) {
+			return S_INTERNAL_SERVER_ERROR;
+		}
+		(*progress)++;
 	}
 
 	return 0;
 }
 
 enum status
-data_send_file(int fd, const struct response *res)
+data_prepare_file_buf(const struct response *res, struct buffer *buf,
+                  size_t *progress)
 {
 	FILE *fp;
 	enum status ret = 0;
-	ssize_t bread, bwritten;
+	ssize_t r;
 	size_t remaining;
-	static char buf[BUFSIZ], *p;
+
+	/* reset buffer */
+	memset(buf, 0, sizeof(*buf));
 
 	/* open file */
 	if (!(fp = fopen(res->path, "r"))) {
@@ -172,33 +202,26 @@ data_send_file(int fd, const struct response *res)
 		goto cleanup;
 	}
 
-	/* seek to lower bound */
-	if (fseek(fp, res->file.lower, SEEK_SET)) {
+	/* seek to lower bound + progress */
+	if (fseek(fp, res->file.lower + *progress, SEEK_SET)) {
 		ret = S_INTERNAL_SERVER_ERROR;
 		goto cleanup;
 	}
 
-	/* write data until upper bound is hit */
-	remaining = res->file.upper - res->file.lower + 1;
-
-	while ((bread = fread(buf, 1, MIN(sizeof(buf),
-	                      remaining), fp))) {
-		if (bread < 0) {
+	/* read data into buf */
+	remaining = res->file.upper - res->file.lower + 1 - *progress;
+	while ((r = fread(buf->data + buf->len, 1,
+	                  MIN(sizeof(buf->data) - buf->len,
+			  remaining), fp))) {
+		if (r < 0) {
 			ret = S_INTERNAL_SERVER_ERROR;
 			goto cleanup;
 		}
-		remaining -= bread;
-		p = buf;
-		while (bread > 0) {
-			bwritten = write(fd, p, bread);
-			if (bwritten <= 0) {
-				ret = S_REQUEST_TIMEOUT;
-				goto cleanup;
-			}
-			bread -= bwritten;
-			p += bwritten;
-		}
+		buf->len += r;
+		*progress += r;
+		remaining -= r;
 	}
+
 cleanup:
 	if (fp) {
 		fclose(fp);
diff --git a/data.h b/data.h
@@ -3,9 +3,16 @@
 #define DATA_H
 
 #include "http.h"
+#include "util.h"
 
-enum status data_send_dirlisting(int, const struct response *);
-enum status data_send_error(int, const struct response *);
-enum status data_send_file(int, const struct response *);
+extern enum status (* const data_fct[])(const struct response *,
+                                        struct buffer *, size_t *);
+
+enum status data_prepare_dirlisting_buf(const struct response *,
+                                    struct buffer *, size_t *);
+enum status data_prepare_error_buf(const struct response *,
+                                   struct buffer *, size_t *);
+enum status data_prepare_file_buf(const struct response *,
+                              struct buffer *, size_t *);
 
 #endif /* DATA_H */
diff --git a/http.c b/http.c
@@ -17,7 +17,6 @@
 #include <unistd.h>
 
 #include "config.h"
-#include "data.h"
 #include "http.h"
 #include "util.h"
 
@@ -58,43 +57,69 @@ const char *res_field_str[] = {
 	[RES_CONTENT_TYPE]   = "Content-Type",
 };
 
-enum status (* const body_fct[])(int, const struct response *) = {
-	[RESTYPE_ERROR]      = data_send_error,
-	[RESTYPE_FILE]       = data_send_file,
-	[RESTYPE_DIRLISTING] = data_send_dirlisting,
-};
-
 enum status
-http_send_header(int fd, const struct response *res)
+http_prepare_header_buf(const struct response *res, struct buffer *buf)
 {
-	char t[FIELD_MAX];
+	char tstmp[FIELD_MAX];
 	size_t i;
 
-	if (timestamp(t, sizeof(t), time(NULL))) {
-		return S_INTERNAL_SERVER_ERROR;
+	/* reset buffer */
+	memset(buf, 0, sizeof(*buf));
+
+	/* generate timestamp */
+	if (timestamp(tstmp, sizeof(tstmp), time(NULL))) {
+		goto err;
 	}
 
-	if (dprintf(fd,
-	            "HTTP/1.1 %d %s\r\n"
-	            "Date: %s\r\n"
-	            "Connection: close\r\n",
-	            res->status, status_str[res->status], t) < 0) {
-		return S_REQUEST_TIMEOUT;
+	/* write data */
+	if (buffer_appendf(buf,
+	                   "HTTP/1.1 %d %s\r\n"
+	                   "Date: %s\r\n"
+	                   "Connection: close\r\n",
+	                   res->status, status_str[res->status], tstmp)) {
+		goto err;
 	}
 
 	for (i = 0; i < NUM_RES_FIELDS; i++) {
-		if (res->field[i][0] != '\0') {
-			if (dprintf(fd, "%s: %s\r\n", res_field_str[i],
-			            res->field[i]) < 0) {
-				return S_REQUEST_TIMEOUT;
-			}
+		if (res->field[i][0] != '\0' &&
+		    buffer_appendf(buf, "%s: %s\r\n", res_field_str[i],
+		                   res->field[i])) {
+			goto err;
 		}
 	}
 
-	if (dprintf(fd, "\r\n") < 0) {
-		return S_REQUEST_TIMEOUT;
+	if (buffer_appendf(buf, "\r\n")) {
+		goto err;
+	}
+
+	return 0;
+err:
+	memset(buf, 0, sizeof(*buf));
+	return S_INTERNAL_SERVER_ERROR;
+}
+
+enum status
+http_send_buf(int fd, struct buffer *buf)
+{
+	size_t remaining;
+	ssize_t r;
+
+	if (buf == NULL || buf->off > sizeof(buf->data)) {
+		return S_INTERNAL_SERVER_ERROR;
+	}
+
+	remaining = buf->len - buf->off;
+	while (remaining > 0) {
+		if ((r = write(fd, buf->data + buf->off, remaining)) <= 0) {
+			return S_REQUEST_TIMEOUT;
+		}
+		buf->off += r;
+		remaining -= r;
 	}
 
+	/* set off to 0 to indicate that we have finished */
+	buf->off = 0;
+
 	return 0;
 }
 
@@ -117,38 +142,48 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX])
 }
 
 enum status
-http_recv_header(int fd, char *h, size_t hsiz, size_t *off)
+http_recv_header(int fd, struct buffer *buf)
 {
+	enum status s;
 	ssize_t r;
 
-	if (h == NULL || off == NULL || *off > hsiz) {
-		return S_INTERNAL_SERVER_ERROR;
+	if (buf->off > sizeof(buf->data)) {
+		s = S_INTERNAL_SERVER_ERROR;
+		goto err;
 	}
 
 	while (1) {
-		if ((r = read(fd, h + *off, hsiz - *off)) <= 0) {
-			return S_REQUEST_TIMEOUT;
+		if ((r = read(fd, buf->data + buf->off,
+		              sizeof(buf->data) - buf->off)) <= 0) {
+			s = S_REQUEST_TIMEOUT;
+			goto err;
 		}
-		*off += r;
+		buf->off += r;
 
 		/* check if we are done (header terminated) */
-		if (*off >= 4 && !memcmp(h + *off - 4, "\r\n\r\n", 4)) {
+		if (buf->off >= 4 && !memcmp(buf->data + buf->off - 4,
+		                             "\r\n\r\n", 4)) {
 			break;
 		}
 
 		/* buffer is full or read over, but header is not terminated */
-		if (r == 0 || *off == hsiz) {
-			return S_REQUEST_TOO_LARGE;
+		if (r == 0 || buf->off == sizeof(buf->data)) {
+			s = S_REQUEST_TOO_LARGE;
+			goto err;
 		}
 	}
 
 	/* header is complete, remove last \r\n and null-terminate */
-	h[*off - 2] = '\0';
+	buf->data[buf->off - 2] = '\0';
 
-	/* set *off to 0 to indicate we are finished */
-	*off = 0;
+	/* set buffer length to length and offset to 0 to indicate success */
+	buf->len = buf->off - 2;
+	buf->off = 0;
 
 	return 0;
+err:
+	memset(buf, 0, sizeof(*buf));
+	return s;
 }
 
 enum status
@@ -840,16 +875,3 @@ http_prepare_error_response(const struct request *req,
 		}
 	}
 }
-
-enum status
-http_send_body(int fd, const struct response *res,
-               const struct request *req)
-{
-	enum status s;
-
-	if (req->method == M_GET && (s = body_fct[res->type](fd, res))) {
-		return s;
-	}
-
-	return 0;
-}
diff --git a/http.h b/http.h
@@ -5,11 +5,9 @@
 #include <limits.h>
 #include <sys/socket.h>
 
+#include "config.h"
 #include "util.h"
 
-#define HEADER_MAX 4096
-#define FIELD_MAX 200
-
 enum req_field {
 	REQ_HOST,
 	REQ_RANGE,
@@ -83,8 +81,6 @@ struct response {
 	} file;
 };
 
-extern enum status (* const body_fct[])(int, const struct response *);
-
 enum conn_state {
 	C_VACANT,
 	C_RECV_HEADER,
@@ -97,21 +93,19 @@ struct connection {
 	enum conn_state state;
 	int fd;
 	struct sockaddr_storage ia;
-	char header[HEADER_MAX]; /* general req/res-header buffer */
-	size_t off;              /* general offset (header/file/dir) */
 	struct request req;
 	struct response res;
+	struct buffer buf;
+	size_t progress;
 };
 
-enum status http_send_header(int, const struct response *);
-enum status http_send_status(int, enum status);
-enum status http_recv_header(int, char *, size_t, size_t *);
+enum status http_prepare_header_buf(const struct response *, struct buffer *);
+enum status http_send_buf(int, struct buffer *);
+enum status http_recv_header(int, struct buffer *);
 enum status http_parse_header(const char *, struct request *);
 void http_prepare_response(const struct request *, struct response *,
                            const struct server *);
 void http_prepare_error_response(const struct request *,
                                  struct response *, enum status);
-enum status http_send_body(int, const struct response *,
-                           const struct request *);
 
 #endif /* HTTP_H */
diff --git a/main.c b/main.c
@@ -16,6 +16,7 @@
 #include <time.h>
 #include <unistd.h>
 
+#include "arg.h"
 #include "data.h"
 #include "http.h"
 #include "sock.h"
@@ -53,24 +54,66 @@ serve(struct connection *c, const struct server *srv)
 
 	/* set connection timeout */
 	if (sock_set_timeout(c->fd, 30)) {
-		goto cleanup;
+		warn("sock_set_timeout: Failed");
 	}
 
-	/* handle request */
-	if ((s = http_recv_header(c->fd, c->header, LEN(c->header), &c->off)) ||
-	    (s = http_parse_header(c->header, &c->req))) {
+	/* read header */
+	memset(&c->buf, 0, sizeof(c->buf));
+	if ((s = http_recv_header(c->fd, &c->buf))) {
 		http_prepare_error_response(&c->req, &c->res, s);
-	} else {
-		http_prepare_response(&c->req, &c->res, srv);
+		goto response;
 	}
 
-	if ((s = http_send_header(c->fd, &c->res)) ||
-	    (s = http_send_body(c->fd, &c->res, &c->req))) {
+	/* parse header */
+	if ((s = http_parse_header(c->buf.data, &c->req))) {
+		http_prepare_error_response(&c->req, &c->res, s);
+		goto response;
+	}
+
+	/* prepare response struct */
+	http_prepare_response(&c->req, &c->res, srv);
+
+response:
+	/* generate response header */
+	if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
+		http_prepare_error_response(&c->req, &c->res, s);
+		if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
+			/* couldn't generate the header, we failed for good */
+			c->res.status = s;
+			goto err;
+		}
+	}
+
+	/* send header */
+	if ((s = http_send_buf(c->fd, &c->buf))) {
 		c->res.status = s;
+		goto err;
 	}
 
+	/* send body */
+	if (c->req.method == M_GET) {
+		for (;;) {
+			/* fill buffer with body data */
+			if ((s = data_fct[c->res.type](&c->res, &c->buf,
+			                               &c->progress))) {
+				c->res.status = s;
+				goto err;
+			}
+
+			/* if done, exit loop */
+			if (c->buf.len == 0) {
+				break;
+			}
+
+			/* send buffer */
+			if ((s = http_send_buf(c->fd, &c->buf))) {
+				c->res.status = s;
+			}
+		}
+	}
+err:
 	logmsg(c);
-cleanup:
+
 	/* clean up and finish */
 	shutdown(c->fd, SHUT_RD);
 	shutdown(c->fd, SHUT_WR);
diff --git a/util.c b/util.c
@@ -182,3 +182,27 @@ reallocarray(void *optr, size_t nmemb, size_t size)
 	}
 	return realloc(optr, size * nmemb);
 }
+
+int
+buffer_appendf(struct buffer *buf, const char *suffixfmt, ...)
+{
+	va_list ap;
+	int ret;
+
+	va_start(ap, suffixfmt);
+	ret = vsnprintf(buf->data + buf->len,
+	                sizeof(buf->data) - buf->len, suffixfmt, ap);
+	va_end(ap);
+
+	if (ret < 0 || (size_t)ret >= (sizeof(buf->data) - buf->len)) {
+		/* truncation occured, discard and error out */
+		memset(buf->data + buf->len, 0,
+		       sizeof(buf->data) - buf->len);
+		return 1;
+	}
+
+	/* increase buffer length by number of bytes written */
+	buf->len += ret;
+
+	return 0;
+}
diff --git a/util.h b/util.h
@@ -6,7 +6,7 @@
 #include <stddef.h>
 #include <time.h>
 
-#include "arg.h"
+#include "config.h"
 
 /* main server struct */
 struct vhost {
@@ -34,6 +34,13 @@ struct server {
 	size_t map_len;
 };
 
+/* general purpose buffer */
+struct buffer {
+	char data[BUFFER_SIZE];
+	size_t len;
+	size_t off;
+};
+
 #undef MIN
 #define MIN(x,y)  ((x) < (y) ? (x) : (y))
 #undef MAX
@@ -56,4 +63,6 @@ int prepend(char *, size_t, const char *);
 void *reallocarray(void *, size_t, size_t);
 long long strtonum(const char *, long long, long long, const char **);
 
+int buffer_appendf(struct buffer *, const char *, ...);
+
 #endif /* UTIL_H */