[hackers] [quark] Make the serving process interruptible || Laslo Hunhold

From: <git_AT_suckless.org>
Date: Mon, 14 Sep 2020 14:56:49 +0200 (CEST)

commit 2714819dfc639098d0531eb3d4f0f5f23708059a
Author: Laslo Hunhold <dev_AT_frign.de>
AuthorDate: Mon Sep 14 13:45:24 2020 +0200
Commit: Laslo Hunhold <dev_AT_frign.de>
CommitDate: Mon Sep 14 13:45:24 2020 +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_AT_frign.de>

diff --git a/Makefile b/Makefile
index 0c0ebed..f005eaf 100644
--- a/Makefile
+++ b/Makefile
_AT_@ -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
index 6d7f690..56f62aa 100644
--- a/config.def.h
+++ b/config.def.h
_AT_@ -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 {
_AT_@ -32,3 +35,5 @@ static const struct {
         { "ogv", "video/ogg" },
         { "webm", "video/webm" },
 };
+
+#endif /* CONFIG_H */
diff --git a/data.c b/data.c
index 3b6b2e5..7ba4821 100644
--- a/data.c
+++ b/data.c
_AT_@ -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)
 {
_AT_@ -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;
_AT_@ -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;
_AT_@ -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:
_AT_@ -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"))) {
_AT_@ -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
index 91aedf5..77d18ba 100644
--- a/data.h
+++ b/data.h
_AT_@ -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
index 96e673a..36ed5ef 100644
--- a/http.c
+++ b/http.c
_AT_@ -17,7 +17,6 @@
 #include <unistd.h>
 
 #include "config.h"
-#include "data.h"
 #include "http.h"
 #include "util.h"
 
_AT_@ -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;
 }
 
_AT_@ -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
_AT_@ -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
index 078423b..a7255b4 100644
--- a/http.h
+++ b/http.h
_AT_@ -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,
_AT_@ -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,
_AT_@ -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
index 49c8656..4f7b75f 100644
--- a/main.c
+++ b/main.c
_AT_@ -16,6 +16,7 @@
 #include <time.h>
 #include <unistd.h>
 
+#include "arg.h"
 #include "data.h"
 #include "http.h"
 #include "sock.h"
_AT_@ -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
index b281613..764615b 100644
--- a/util.c
+++ b/util.c
_AT_@ -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
index bc7f8ec..27de3b1 100644
--- a/util.h
+++ b/util.h
_AT_@ -6,7 +6,7 @@
 #include <stddef.h>
 #include <time.h>
 
-#include "arg.h"
+#include "config.h"
 
 /* main server struct */
 struct vhost {
_AT_@ -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
_AT_@ -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 */
Received on Mon Sep 14 2020 - 14:56:49 CEST

This archive was generated by hypermail 2.3.0 : Mon Sep 14 2020 - 15:00:32 CEST