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 */