commit 2714819dfc639098d0531eb3d4f0f5f23708059a
Author:     Laslo Hunhold <[email protected]>
AuthorDate: Mon Sep 14 13:45:24 2020 +0200
Commit:     Laslo Hunhold <[email protected]>
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 <[email protected]>

diff --git a/Makefile b/Makefile
index 0c0ebed..f005eaf 100644
--- 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
index 6d7f690..56f62aa 100644
--- 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
index 3b6b2e5..7ba4821 100644
--- 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
index 91aedf5..77d18ba 100644
--- 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
index 96e673a..36ed5ef 100644
--- 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
index 078423b..a7255b4 100644
--- 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
index 49c8656..4f7b75f 100644
--- 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
index b281613..764615b 100644
--- 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
index bc7f8ec..27de3b1 100644
--- 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 */

Reply via email to