Introduction
Introduction Statistics Contact Development Disclaimer Help
Make the serving process interruptible - quark - quark web server
git clone git://git.suckless.org/quark
Log
Files
Refs
LICENSE
---
commit 2714819dfc639098d0531eb3d4f0f5f23708059a
parent 0823ba4c3e480fb5e2c246b8ac6c4783d866ab87
Author: Laslo Hunhold <[email protected]>
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 <[email protected]>
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 */
You are viewing proxied material from suckless.org. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.