quark

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

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:
MMakefile | 6+++---
Mconfig.def.h | 9+++++++--
Mdata.c | 131++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mdata.h | 13++++++++++---
Mhttp.c | 120+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mhttp.h | 18++++++------------
Mmain.c | 61++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mutil.c | 24++++++++++++++++++++++++
Mutil.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 */