commit ff0f14b9872a7bb6927e8f8a800681522cf79ecc
parent b638d66e068fe677e065a39f4bc3639ab7438c09
Author: MikoĊaj Lenczewski <mikolaj@lenczewski.org>
Date: Wed, 3 Dec 2025 11:42:25 +0000
Update name of projects
Diffstat:
| M | .gitignore | | | 3 | ++- |
| M | build.sh | | | 12 | +++++------- |
| M | debug.sh | | | 2 | +- |
| R | sslterm/main.c -> proxy/main.c | | | 0 | |
| A | serve/connection.c | | | 433 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serve/main.c | | | 294 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serve/net.c | | | 128 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serve/serve.h | | | 764 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serve/uri.c | | | 229 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serve/utils.c | | | 33 | +++++++++++++++++++++++++++++++++ |
| D | website/connection.c | | | 433 | ------------------------------------------------------------------------------- |
| D | website/main.c | | | 294 | ------------------------------------------------------------------------------- |
| D | website/net.c | | | 128 | ------------------------------------------------------------------------------- |
| D | website/uri.c | | | 229 | ------------------------------------------------------------------------------- |
| D | website/utils.c | | | 33 | --------------------------------- |
| D | website/website.h | | | 762 | ------------------------------------------------------------------------------- |
16 files changed, 1889 insertions(+), 1888 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -2,5 +2,6 @@ bin/
out/**
!out/.keep
-imgui.ini
**/.*.swp
+imgui.ini
+tags
diff --git a/build.sh b/build.sh
@@ -2,18 +2,16 @@
CC="${CC:-clang}"
-WARNINGS="-Wall -Wextra -Wpedantic ${WERROR:+-Werror}"
+WARNINGS="-Wall -Wextra ${WERROR:+-Werror} -Wno-unused-parameter"
CFLAGS="-std=c11 -g -O0"
-CPPFLAGS=""
-LDFLAGS=""
set -ex
mkdir -p bin
-$CC -o bin/website website/main.c \
- $WARNINGS $CFLAGS $CPPFLAGS $LDFLAGS $(pkgconf --cflags --libs liburing)
+$CC -o bin/serve serve/main.c \
+ $WARNINGS $CFLAGS $(pkgconf --cflags --libs liburing)
-#$CC -o bin/sslterm sslterm/main.c \
-# $WARNINGS $CLFAGS $CPPFLAGS $LDFLAGS $(pkgconf --cflags --libs liburing openssl)
+#$CC -o bin/proxy proxy/main.c \
+# $WARNINGS $CLFAGS $(pkgconf --cflags --libs liburing openssl)
diff --git a/debug.sh b/debug.sh
@@ -2,4 +2,4 @@
set -ex
-lldbgui -- bin/website -v $@
+lldbgui -- bin/serve -v $@
diff --git a/sslterm/main.c b/proxy/main.c
diff --git a/serve/connection.c b/serve/connection.c
@@ -0,0 +1,433 @@
+#include "serve.h"
+
+extern inline enum http_method
+method_from_str(char *method);
+
+extern inline char const *
+method_str(enum http_method method);
+
+extern inline enum http_version
+version_from_str(char *veresion);
+
+extern inline char const *
+version_str(enum http_version version);
+
+extern inline char const *
+status_str(enum http_status status);
+
+extern inline char const *
+content_encoding_str(enum content_encoding encoding);
+
+extern inline enum content_encoding
+content_encoding_from_list(char *list);
+
+extern inline char const *
+content_type_str(enum content_type type);
+
+extern inline bool
+content_type_is_text(enum content_type type);
+
+extern inline enum content_type
+content_type_from_mime(char *mime);
+
+extern inline enum content_type
+content_type_from_ext(char *ext, size_t len);
+
+void
+parse_http_request(char *src, struct http_request *request, struct http_headers *headers)
+{
+ char *saveptr;
+ request->method = method_from_str(strtok_r(src, " ", &saveptr));
+ char *rawuri = strtok_r(NULL, " ", &saveptr);
+ request->uri = uri_parse(rawuri, strlen(rawuri));
+ request->version = version_from_str(strtok_r(NULL, "\r\n", &saveptr));
+
+ memset(headers, 0, sizeof *headers);
+
+ headers->keep_alive = request->version > HTTP_09;
+
+ char *header;
+ while ((header = strtok_r(NULL, "\r\n", &saveptr))) {
+ char *val;
+ char *key = strtok_r(header, ": ", &val);
+
+ while (isspace(*val)) /* strip leading whitespace */
+ val++;
+
+ for (char *ptr = key; *ptr; ptr++) /* normalise as lowercase */
+ *ptr = isalpha(*ptr) ? tolower(*ptr) : *ptr;
+
+ if (strcmp(key, "connection") == 0) {
+ for (char *ptr = val; *ptr; ptr++)
+ *ptr = isalpha(*ptr) ? tolower(*ptr) : *ptr;
+ headers->keep_alive = !(strcmp(val, "close") == 0);
+ } else if (strcmp(key, "accept") == 0) {
+ headers->accept = val;
+ } else if (strcmp(key, "accept-encoding") == 0) {
+ headers->encoding = content_encoding_from_list(val);
+ } else if (strcmp(key, "range") == 0) {
+ // TODO: implement me!
+ } else if (strcmp(key, "content-type") == 0) {
+ headers->content_type = content_type_from_mime(val);
+ } else if (strcmp(key, "content-length") == 0) {
+ headers->content_length = strtoull(val, NULL, 0);
+ }
+ }
+}
+
+void
+handle_accept(struct io_uring *ioring, struct connection *conn, int sock)
+{
+ conn->socket = sock;
+ conn->cap = sizeof conn->buf;
+ conn->len = conn->cur = 0;
+
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+ conn->timeout = get_timeout(&now, CONNECTION_TIMEOUT_MS);
+
+ enqueue_recv(ioring, &conn->ioreq, conn->socket, conn->buf, conn->cap, 0);
+}
+
+static bool
+have_request_headers(struct connection *conn)
+{
+ char *last_parse_end = conn->buf + conn->cur;
+ char *hdrs = MAX(last_parse_end - (strlen(HTTP_TERM) - 1), conn->buf);
+ char *hdrs_end = strnstr(hdrs, conn->len - conn->cur, HTTP_TERM);
+
+ if (hdrs_end)
+ *hdrs_end = '\0';
+
+ return hdrs_end != NULL;
+}
+
+void
+handle_request_chunk(struct io_uring *ioring, struct connection *conn, int webroot, int res)
+{
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+// if (get_timestamp(&conn->timeout) < get_timestamp(&now))
+// goto error_timeout;
+
+ conn->len += res;
+
+ int have_headers = have_request_headers(conn);
+ conn->cur += res;
+
+ if (!have_headers && conn->len == conn->cap) /* cannot read more data */
+ goto error_payload_too_large;
+
+ if (!have_headers) { /* can read more data */
+ enqueue_recv(ioring, &conn->ioreq, conn->socket,
+ conn->buf + conn->len, conn->cap - conn->len, 0);
+ return;
+ }
+
+ /* have request header, parse it */
+ handle_request(ioring, conn, webroot);
+ return;
+
+error_timeout:
+ error_connection(ioring, conn, HTTP_408, false);
+ return;
+
+error_payload_too_large:
+ error_connection(ioring, conn, HTTP_413, true);
+ return;
+}
+
+static bool
+sanitise_uri_to_path(struct uri *uri, char buf[static PATH_MAX])
+{
+ if (uri->path.len == 0)
+ return false;
+
+ if (!urifrag_try_normalise(&uri->path))
+ return false;
+
+ urifrag_remove_dot_segments(&uri->path);
+
+ if (PATH_MAX <= uri->path.len)
+ return false;
+
+ if (*(uri->path.ptr) == '/') {
+ uri->path.ptr++;
+ uri->path.len--;
+ }
+
+ char *end = stpncpy(buf, uri->path.ptr, uri->path.len);
+ *end = '\0';
+
+ size_t written = end - buf;
+ if (!written || end[-1] == '/') {
+ if (PATH_MAX < written + strlen("index.html"))
+ return false;
+
+ *stpncpy(end, "index.html", PATH_MAX - written - strlen("index.html")) = '\0';
+ }
+
+ return true;
+}
+
+static void
+prepare_response_headers(struct connection *conn, enum http_status status,
+ struct http_headers *headers)
+{
+ conn->len = conn->cur = 0;
+
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "HTTP/1.1 %d %s" HTTP_SEP, status, status_str(status));
+
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "Connection: %s" HTTP_SEP, headers->keep_alive ? "keep-alive" : "close");
+
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "Content-Length: %" PRIu64 HTTP_SEP, headers->content_length);
+
+ if (headers->have_range) {
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "Content-Range: bytes %" PRIu64 "-%" PRIu64 "/%" PRIu64 HTTP_SEP,
+ headers->range.begin, headers->range.end, headers->content_length);
+ }
+
+ if (headers->content_length) {
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "Content-Type: %s", content_type_str(headers->content_type));
+
+ if (content_type_is_text(headers->content_type)) {
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "; charset=utf-8");
+ }
+
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len, HTTP_SEP);
+
+ if (headers->encoding) {
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
+ "Content-Encoding: %s" HTTP_SEP,
+ content_encoding_str(headers->encoding));
+ }
+ }
+
+ conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len, HTTP_SEP);
+
+ assert(conn->len < conn->cap);
+
+ if (opts.verbose)
+ printf("response headers: %zu bytes\n---\n%.*s",
+ conn->len, (int) conn->len, conn->buf);
+}
+
+void
+handle_request(struct io_uring *ioring, struct connection *conn, int webroot)
+{
+ parse_http_request(conn->buf, &conn->request, &conn->request_headers);
+
+ if (opts.verbose) {
+ printf("\trequest: %s, %s\n", method_str(conn->request.method), version_str(conn->request.version));
+ printf("\turi: path: '%.*s'\n", conn->request.uri.path.len, conn->request.uri.path.ptr);
+ printf("\tkeep-alive: %d\n", conn->request_headers.keep_alive);
+ printf("\taccept: %s\n", conn->request_headers.accept);
+ printf("\taccept-encoding: %d\n", conn->request_headers.encoding);
+ printf("\trange: %" PRIu64 "-%" PRIu64 "\n", conn->request_headers.range.begin, conn->request_headers.range.end);
+ printf("\tcontent-type: %d\n", conn->request_headers.content_type);
+ printf("\tcontent-length: %" PRIu64 "\n", conn->request_headers.content_length);
+ }
+
+ if (conn->request.method != HTTP_HEAD && conn->request.method != HTTP_GET)
+ goto error_not_implemented;
+
+ if (conn->request.version != HTTP_11)
+ goto error_http_version_not_supported;
+
+ if (conn->request_headers.content_length) /* we do not support request bodies */
+ goto error_unprocessable_request;
+
+ char path[PATH_MAX];
+ if (!sanitise_uri_to_path(&conn->request.uri, path))
+ goto error_bad_request;
+
+ if (opts.verbose) {
+ printf("\tsanitised uri: '%s'\n", path);
+ }
+
+ // TODO: read from file cache instead of reading directly?
+ conn->response.fd = openat(webroot, path, O_RDONLY | O_CLOEXEC);
+ if (conn->response.fd < 0) {
+ if (errno == EACCES)
+ goto error_forbidden;
+
+ if (errno == ENOENT)
+ goto error_not_found;
+
+ goto error_internal_server_error;
+ }
+
+ struct stat statbuf;
+ if (fstat(conn->response.fd, &statbuf) < 0) {
+ close(conn->response.fd);
+ goto error_internal_server_error;
+ }
+
+ if (!S_ISREG(statbuf.st_mode)) {
+ close(conn->response.fd);
+ goto error_not_found;
+ }
+
+ conn->response.len = statbuf.st_size;
+ conn->response.off = 0;
+
+ if (conn->request_headers.have_range) {
+ if (conn->request_headers.range.begin < conn->request_headers.range.end)
+ goto error_bad_request;
+
+ if (conn->response.len < conn->request_headers.range.begin ||
+ conn->response.len < conn->request_headers.range.end)
+ goto error_range_not_satisfiable;
+
+ conn->response.len = conn->request_headers.range.end
+ - conn->request_headers.range.begin;
+ conn->response.off = conn->request_headers.range.begin;
+ }
+
+ char *ext = strrchr(path, '.');
+ conn->response_headers = (struct http_headers) {
+ .keep_alive = conn->request_headers.keep_alive,
+ .encoding = ENCODING_NONE,
+ .have_range = conn->request_headers.have_range,
+ .range = conn->request_headers.range,
+ .content_type = content_type_from_ext(ext, strlen(ext)),
+ .content_length = conn->response.len,
+ };
+
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+ conn->timeout = get_timeout(&now, CONNECTION_TIMEOUT_MS);
+
+ enum http_status status = conn->response_headers.have_range ? HTTP_206 : HTTP_200;
+ prepare_response_headers(conn, status, &conn->response_headers);
+
+ if (conn->request.method == HTTP_HEAD) {
+ close(conn->response.fd); /* no body to send, dont delay close */
+ conn->response.fd = -1;
+
+ enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
+ } else if (conn->request.method == HTTP_GET) { /* prepare body */
+ size_t len = MIN(conn->response.len, conn->cap - conn->len);
+ enqueue_read(ioring, &conn->ioreq, conn->response.fd,
+ conn->buf + conn->len, len, conn->response.off);
+ }
+
+ return;
+
+error_bad_request:
+ error_connection(ioring, conn, HTTP_400, true);
+ return;
+
+error_forbidden:
+ error_connection(ioring, conn, HTTP_403, true);
+ return;
+
+error_not_found:
+ error_connection(ioring, conn, HTTP_404, true);
+ return;
+
+error_range_not_satisfiable:
+ error_connection(ioring, conn, HTTP_416, true);
+ return;
+
+error_unprocessable_request:
+ error_connection(ioring, conn, HTTP_422, true);
+ return;
+
+error_internal_server_error:
+ error_connection(ioring, conn, HTTP_500, true);
+ return;
+
+error_not_implemented:
+ error_connection(ioring, conn, HTTP_501, true);
+ return;
+
+error_http_version_not_supported:
+ error_connection(ioring, conn, HTTP_505, true);
+ return;
+}
+
+void
+handle_response_chunk(struct io_uring *ioring, struct connection *conn, int res)
+{
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+// if (get_timestamp(&conn->timeout) < get_timestamp(&now))
+// goto error_timeout;
+
+ conn->cur += res;
+
+ if (conn->cur < conn->len) { /* have remaining buffered data to send */
+ enqueue_send(ioring, &conn->ioreq, conn->socket,
+ conn->buf + conn->cur, conn->len - conn->cur, 0);
+ return;
+ }
+
+ if (conn->response.len) { /* have remaining data to read into buffer */
+ conn->len = conn->cur = 0;
+
+ size_t len = MIN(conn->response.len, conn->cap);
+ enqueue_read(ioring, &conn->ioreq, conn->response.fd,
+ conn->buf, len, conn->response.off);
+ return;
+ } else if (conn->response.fd > 0) { /* no more data to read, close file */
+ close(conn->response.fd);
+ conn->response.fd = -1;
+ }
+
+ if (conn->response_headers.keep_alive) {
+ conn->len = conn->cur = 0;
+ enqueue_recv(ioring, &conn->ioreq, conn->socket,
+ conn->buf, conn->cap, 0);
+ return;
+ }
+
+error_timeout:
+ close_connection(ioring, conn);
+ return;
+}
+
+void
+handle_fileread_chunk(struct io_uring *ioring, struct connection *conn, int res)
+{
+ conn->response.off += res;
+ conn->response.len -= res;
+ conn->len += res;
+
+ enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
+}
+
+void
+error_connection(struct io_uring *ioring, struct connection *conn,
+ enum http_status err, bool linger)
+{
+ struct http_headers error_headers = {
+ .keep_alive = linger,
+ .content_length = 0,
+ };
+
+ prepare_response_headers(conn, err, &error_headers);
+ enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
+}
+
+void
+close_connection(struct io_uring *ioring, struct connection *conn)
+{
+ enqueue_close(ioring, &conn->ioreq, conn->socket);
+}
+
+extern inline int
+connection_pool_init(struct connection_pool *pool, size_t capacity);
+
+extern inline struct connection *
+connection_pool_alloc(struct connection_pool *pool);
+
+extern inline void
+connection_pool_free(struct connection_pool *restrict pool,
+ struct connection *restrict conn);
diff --git a/serve/main.c b/serve/main.c
@@ -0,0 +1,294 @@
+#include "serve.h"
+
+struct opts opts = {
+ .verbose = 0,
+ .host = "localhost",
+ .port = "8080",
+};
+
+#define OPTSTR "hva:p:"
+static void
+usage(char *prog)
+{
+ fprintf(stderr, "Usage: %s [-hv] [-a <addr>] [-p <port>] <webroot>\n", prog);
+ fprintf(stderr, "\t-h : display this help information\n");
+ fprintf(stderr, "\t-v : enable verbose logging\n");
+ fprintf(stderr, "\t-a : set the server socket bind address (default: localhost)\n");
+ fprintf(stderr, "\t-p : set the server socket bind port (default: 8080)\n");
+ fprintf(stderr, "\twebroot : directory out of which to serve files\n");
+}
+
+static int
+parse_opts(int argc, char **argv)
+{
+ int opt;
+ while ((opt = getopt(argc, argv, OPTSTR)) > 0) {
+ switch (opt) {
+ case 'v':
+ opts.verbose = 1;
+ break;
+
+ case 'a':
+ opts.host = optarg;
+ break;
+
+ case 'p':
+ opts.port = optarg;
+ break;
+
+ default:
+ return -1;
+ }
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "Missing webroot\n");
+ return -1;
+ }
+
+ opts.webroot = argv[optind];
+
+ return 0;
+}
+
+sig_atomic_t quit;
+
+static void
+close_handler(int sig, siginfo_t *info, void *uctx)
+{
+ (void) info;
+ (void) uctx;
+
+ fprintf(stderr, "Signal %d caught. Quitting\n", sig);
+
+ quit = 1;
+}
+
+sig_atomic_t reload_tree;
+
+static void
+reload_handler(int sig, siginfo_t *info, void *uctx)
+{
+ (void) info;
+ (void) uctx;
+
+ fprintf(stderr, "Signal %d caught. Reloading file tree\n", sig);
+
+ reload_tree = 1;
+}
+
+int
+serve(struct io_uring *ioring, int webroot, int sock, struct connection_pool *pool)
+{
+ listen(sock, SERVER_LISTEN_BACKLOG);
+
+ struct sockaddr_storage client_addr;
+ socklen_t client_addrlen = sizeof client_addr;
+
+ struct ioreq accept_req = {
+ .type = IOREQ_ACCEPT,
+ .fd = sock,
+ .accept.addr = (struct sockaddr *) &client_addr,
+ .accept.addrlen = &client_addrlen,
+ .accept.flags = 0,
+ };
+
+ enqueue_ioreq(ioring, &accept_req);
+ submit_ioreqs(ioring);
+
+ quit = 0;
+ while (!quit) {
+ struct io_uring_cqe *cqe;
+ if (io_uring_wait_cqe(ioring, &cqe) < 0) {
+ fprintf(stderr, "io_uring_wait_cqe() failed\n");
+ return EXIT_FAILURE;
+ }
+
+ unsigned head, seen = 0;
+ io_uring_for_each_cqe(ioring, head, cqe) {
+ struct ioreq *ioreq = io_uring_cqe_get_data(cqe);
+ if (!ioreq) { /* overloaded client error messages */
+ // TODO: do something?
+ }
+
+ int res = cqe->res;
+ switch (ioreq->type) {
+ case IOREQ_ACCEPT: {
+ if (res < 0) { /* multishot failed, restart? */
+ fprintf(stderr, "multishot accept failed\n");
+ return EXIT_FAILURE;
+ }
+
+ struct connection *conn = connection_pool_alloc(pool);
+ if (!conn) { /* overloaded server */
+ close(res);
+ goto next_cqe;
+ }
+
+ if (opts.verbose) {
+ char host[NI_MAXHOST];
+ char port[NI_MAXSERV];
+ getnameinfo((struct sockaddr *) &client_addr,
+ client_addrlen,
+ host, sizeof host,
+ port, sizeof port,
+ NI_NUMERICHOST | NI_NUMERICSERV);
+
+ fprintf(stderr, "[%d] Client connection from %s:%s\n",
+ res, host, port);
+ }
+
+ handle_accept(ioring, conn, res);
+ } break;
+
+ case IOREQ_RECV: {
+ struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
+ assert(conn);
+
+ if (res <= 0) {
+ if (opts.verbose)
+ fprintf(stderr, "recv(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+ if (opts.verbose)
+ fprintf(stderr, "[%d] Client recv: %d bytes\n",
+ conn->socket, res);
+
+ handle_request_chunk(ioring, conn, webroot, res);
+ } break;
+
+ case IOREQ_SEND: {
+ struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
+ assert(conn);
+
+ if (res <= 0) {
+ if (opts.verbose)
+ fprintf(stderr, "send(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+ if (opts.verbose)
+ fprintf(stderr, "[%d] Client send(): %d bytes\n",
+ conn->socket, res);
+
+ handle_response_chunk(ioring, conn, res);
+ } break;
+
+ case IOREQ_READ: {
+ struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
+ assert(conn);
+
+ if (res <= 0) {
+ if (opts.verbose)
+ fprintf(stderr, "read(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+ if (opts.verbose)
+ fprintf(stderr, "[%d] Client read: %d bytes\n",
+ conn->socket, res);
+
+ handle_fileread_chunk(ioring, conn, res);
+ } break;
+
+ case IOREQ_CLOSE: {
+ struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
+ assert(conn);
+
+ if (opts.verbose)
+ fprintf(stderr, "[%d] Client closed connection\n",
+ conn->socket);
+
+ connection_pool_free(pool, conn);
+ } break;
+ }
+
+next_cqe:
+ seen++;
+ }
+
+ io_uring_cq_advance(ioring, seen);
+
+ submit_ioreqs(ioring);
+ }
+
+ return EXIT_SUCCESS;
+}
+
+int
+main(int argc, char **argv)
+{
+ if (parse_opts(argc, argv) < 0) {
+ usage(argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ struct sigaction on_close = {
+ .sa_sigaction = close_handler, .sa_flags = SA_SIGINFO,
+ };
+
+ sigaction(SIGINT, &on_close, NULL);
+ sigaction(SIGKILL, &on_close, NULL);
+
+ /* we want to cache the webroot tree in memory, and we need to know
+ * when tree has changed. we could use inotify for this, but that has
+ * the possibility of failure (overflow) and is more complicated than
+ * the program updating our tree sending us a signal to reload
+ */
+ struct sigaction on_reload = {
+ .sa_sigaction = reload_handler, .sa_flags = SA_SIGINFO,
+ };
+
+ sigaction(SIGHUP, &on_reload, NULL);
+
+ struct connection_pool pool;
+ if (connection_pool_init(&pool, CONNECTION_POOL_CAPACITY) < 0) {
+ fprintf(stderr, "Failed to initialise connection pool\n");
+ exit(EXIT_FAILURE);
+ }
+
+ struct io_uring ioring;
+ if (ioring_init(&ioring)) {
+ fprintf(stderr, "Failed to initialise io_uring\n");
+ exit(EXIT_FAILURE);
+ }
+
+ int webroot = open(opts.webroot, O_DIRECTORY | O_PATH | O_CLOEXEC);
+ if (webroot < 0) {
+ fprintf(stderr, "Failed to open webroot %s\n", opts.webroot);
+ exit(EXIT_FAILURE);
+ }
+
+ int sock = create_server_socket(opts.host, opts.port);
+ if (sock < 0) {
+ fprintf(stderr, "Failed to create and bind server socket to %s:%s\n",
+ opts.host, opts.port);
+ exit(EXIT_FAILURE);
+ }
+
+ struct sockaddr_storage addr;
+ socklen_t addrlen = sizeof addr;
+ if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) {
+ fprintf(stderr, "Failed to get bound socket addr\n");
+ exit(EXIT_FAILURE);
+ }
+
+ char bound_host[NI_MAXHOST], bound_port[NI_MAXSERV];
+ getnameinfo((struct sockaddr *) &addr, addrlen,
+ bound_host, sizeof bound_host,
+ bound_port, sizeof bound_port,
+ NI_NUMERICHOST | NI_NUMERICSERV);
+
+ fprintf(stderr, "Server socket bound to %s:%s\n", bound_host, bound_port);
+
+ exit(serve(&ioring, webroot, sock, &pool));
+}
+
+#include "utils.c"
+#include "net.c"
+#include "uri.c"
+#include "connection.c"
diff --git a/serve/net.c b/serve/net.c
@@ -0,0 +1,128 @@
+#include "serve.h"
+
+int
+ioring_init(struct io_uring *ioring)
+{
+ /* NOTE: we want to ensure that we can submit one operation for each
+ * connection in the worst case, as well as a single multihit
+ * accept for the server socket, so size our ioring submission
+ * queue appropriately.
+ */
+ unsigned entries = 1 + (CONNECTION_POOL_CAPACITY * CONNECTION_CONCURRENT_OPS);
+
+ return -io_uring_queue_init(entries, ioring, 0);
+}
+
+void
+enqueue_ioreq(struct io_uring *ioring, struct ioreq *ioreq)
+{
+ struct io_uring_sqe *sqe = io_uring_get_sqe(ioring);
+ if (!sqe) {
+ submit_ioreqs(ioring);
+ sqe = io_uring_get_sqe(ioring);
+ }
+
+ assert(sqe);
+
+ switch (ioreq->type) {
+ case IOREQ_ACCEPT:
+ io_uring_prep_multishot_accept(sqe,
+ ioreq->fd,
+ ioreq->accept.addr,
+ ioreq->accept.addrlen,
+ ioreq->accept.flags);
+ break;
+
+ case IOREQ_RECV:
+ io_uring_prep_recv(sqe,
+ ioreq->fd,
+ ioreq->recv.buf,
+ ioreq->recv.len,
+ ioreq->recv.flags);
+ break;
+
+ case IOREQ_SEND:
+ io_uring_prep_send(sqe,
+ ioreq->fd,
+ ioreq->send.buf,
+ ioreq->send.len,
+ ioreq->send.flags);
+ break;
+
+ case IOREQ_READ:
+ io_uring_prep_read(sqe,
+ ioreq->fd,
+ ioreq->read.buf,
+ ioreq->read.len,
+ ioreq->read.off);
+ break;
+
+ case IOREQ_CLOSE:
+ io_uring_prep_close(sqe,
+ ioreq->fd);
+ break;
+ }
+
+ io_uring_sqe_set_data(sqe, ioreq);
+}
+
+extern inline void
+enqueue_accept(struct io_uring *ioring, struct ioreq *req, int fd,
+ struct sockaddr *addr, socklen_t *addrlen, int flags);
+
+extern inline void
+enqueue_recv(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, int flags);
+
+extern inline void
+enqueue_send(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, int flags);
+
+extern inline void
+enqueue_read(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, uint64_t off);
+
+extern inline void
+enqueue_close(struct io_uring *ioring, struct ioreq *req, int fd);
+
+void
+submit_ioreqs(struct io_uring *ioring)
+{
+ io_uring_submit(ioring);
+}
+
+int
+create_server_socket(char const *restrict host, char const *restrict port)
+{
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ .ai_protocol = IPPROTO_TCP,
+ .ai_flags = AI_NUMERICSERV,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo(host, port, &hints, &addrinfo))) {
+ fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(res));
+ return -1;
+ }
+
+ int fd;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ fd = socket(ptr->ai_family, ptr->ai_socktype | O_CLOEXEC, ptr->ai_protocol);
+ if (fd < 0)
+ continue;
+
+ if (bind(fd, ptr->ai_addr, ptr->ai_addrlen) == 0)
+ break;
+
+ close(fd);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) /* failed to bind to any addresses */
+ return -1;
+
+ return fd;
+}
diff --git a/serve/serve.h b/serve/serve.h
@@ -0,0 +1,764 @@
+#ifndef SERVE_H
+#define SERVE_H
+
+#define _GNU_SOURCE 1
+
+#include <assert.h>
+#include <ctype.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <signal.h>
+
+#include <getopt.h>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include <sys/socket.h>
+#include <netdb.h>
+
+#include <sys/mman.h>
+
+#include <liburing.h>
+
+/* configuration
+ * ===========================================================================
+ */
+
+#define SERVER_LISTEN_BACKLOG 16
+
+#define CONNECTION_POOL_CAPACITY 1024
+
+#define CONNECTION_CONCURRENT_OPS 2
+#define CONNECTION_BUFFER_SIZE 8192
+#define CONNECTION_TIMEOUT_MS -1
+
+/* utilities
+ * ===========================================================================
+ */
+
+#define NANOS 1000000000ULL
+
+#define KiB 1024ULL
+#define MiB (1024ULL * KiB)
+#define GiB (1024ULL * MiB)
+#define TiB (1024ULL * GiB)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+#define TO_PARENT(child, T, member) \
+ ((child) ? ((T *) ((uintptr_t) child - offsetof(T, member))) : NULL)
+
+struct str {
+ char *ptr;
+ size_t len;
+};
+
+inline char *
+strnstr(char *restrict src, size_t len, char const *restrict marker)
+{
+ char const *end = src + len;
+
+ while (src < end) {
+ char *ptr = src;
+ char const *cur = marker;
+ while (ptr < end && *cur && *ptr++ == *cur)
+ cur++;
+
+ if (*cur == '\0')
+ return src;
+
+ if (ptr == end)
+ goto end;
+
+ src = ptr;
+ }
+
+end:
+ return NULL;
+}
+
+#define ALIGN_PREV(v, align) ((v) & ~((align) - 1))
+#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + ((align) - 1), (align))
+
+struct arena {
+ void *ptr;
+ uint64_t cap, len;
+};
+
+inline void
+arena_reset(struct arena *arena)
+{
+ arena->len = 0;
+}
+
+inline void *
+arena_alloc(struct arena *arena, uint64_t size, uint64_t align)
+{
+ uint64_t aligned_len = ALIGN_NEXT(arena->len, align);
+ if (aligned_len + size > arena->cap)
+ return NULL;
+
+ void *ptr = (void *) ((uintptr_t) arena->ptr + aligned_len);
+ arena->len = aligned_len + size;
+
+ return ptr;
+}
+
+#define ALLOC_ARRAY(arena, T, n) \
+ arena_alloc((arena), sizeof(T) * (n), alignof(T))
+
+#define ALLOC_SIZED(arena, T) ALLOC_ARRAY((arena), T, 1)
+
+struct list_node {
+ struct list_node *prev, *next;
+};
+
+#define FROM_NODE(node, T, member) TO_PARENT((node), T, member)
+
+struct list {
+ struct list_node *head, *tail;
+};
+
+inline void
+list_push_head(struct list *restrict list, struct list_node *restrict node)
+{
+ if (!list->tail)
+ list->tail = node;
+
+ if (list->head)
+ list->head->prev = node;
+
+ node->next = list->head;
+ list->head = node;
+}
+
+inline void
+list_push_tail(struct list *restrict list, struct list_node *restrict node)
+{
+ if (!list->head)
+ list->head = node;
+
+ if (list->tail)
+ list->tail->next = node;
+
+ node->prev = list->tail;
+ list->tail = node;
+}
+
+inline struct list_node *
+list_pop_head(struct list *list)
+{
+ struct list_node *node = list->head;
+
+ if (node)
+ list->head = list->head->next;
+
+ return node;
+}
+
+inline struct list_node *
+list_pop_tail(struct list *list)
+{
+ struct list_node *node = list->tail;
+
+ if (node)
+ list->tail = list->tail->prev;
+
+ return node;
+}
+
+inline struct timespec
+get_timeout(struct timespec const *ts, uint64_t timeout_ms)
+{
+ return (struct timespec) {
+ .tv_sec = ts->tv_sec + (timeout_ms / 1000),
+ .tv_nsec = ts->tv_nsec + ((timeout_ms % 1000) * (NANOS / 1000)),
+ };
+}
+
+inline uint64_t
+get_timestamp(struct timespec const *ts)
+{
+ return (NANOS * ts->tv_sec) + ts->tv_nsec;
+}
+
+inline void
+timespec_diff(struct timespec const *restrict lhs,
+ struct timespec const *restrict rhs,
+ struct timespec *restrict res)
+{
+ res->tv_sec = lhs->tv_sec - rhs->tv_sec;
+ res->tv_nsec = lhs->tv_nsec - rhs->tv_nsec;
+
+ if (res->tv_nsec < 0) {
+ res->tv_sec -= 1;
+ res->tv_nsec += NANOS;
+ }
+}
+
+/* network interaction
+ * ===========================================================================
+ */
+
+#define IOREQ_MAX_IOVS 8
+
+struct ioreq {
+ enum {
+ IOREQ_ACCEPT,
+ IOREQ_RECV,
+ IOREQ_SEND,
+ IOREQ_READ,
+ IOREQ_CLOSE,
+ } type;
+
+ int fd;
+
+ union {
+ struct {
+ struct sockaddr *addr;
+ socklen_t *addrlen;
+ int flags;
+ } accept;
+
+ struct {
+ void *buf;
+ size_t len;
+ int flags;
+ } recv, send;
+
+ struct {
+ void *buf;
+ size_t len;
+ uint64_t off;
+ } read;
+ };
+};
+
+int
+ioring_init(struct io_uring *ioring);
+
+void
+enqueue_ioreq(struct io_uring *ioring, struct ioreq *ioreq);
+
+inline void
+enqueue_accept(struct io_uring *ioring, struct ioreq *req, int fd,
+ struct sockaddr *addr, socklen_t *addrlen, int flags)
+{
+ req->type = IOREQ_ACCEPT;
+ req->fd = fd;
+ req->accept.addr = addr;
+ req->accept.addrlen = addrlen;
+ req->accept.flags = flags;
+
+ enqueue_ioreq(ioring, req);
+}
+
+inline void
+enqueue_recv(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, int flags)
+{
+ req->type = IOREQ_RECV;
+ req->fd = fd;
+ req->recv.buf = buf;
+ req->recv.len = len;
+ req->recv.flags = flags;
+
+ enqueue_ioreq(ioring, req);
+}
+
+inline void
+enqueue_send(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, int flags)
+{
+ req->type = IOREQ_SEND;
+ req->fd = fd;
+ req->send.buf = buf;
+ req->send.len = len;
+ req->send.flags = flags;
+
+ enqueue_ioreq(ioring, req);
+}
+
+inline void
+enqueue_read(struct io_uring *ioring, struct ioreq *req, int fd,
+ void *buf, size_t len, uint64_t off)
+{
+ req->type = IOREQ_READ;
+ req->fd = fd;
+ req->read.buf = buf;
+ req->read.len = len;
+ req->read.off = off;
+
+ enqueue_ioreq(ioring, req);
+}
+
+inline void
+enqueue_close(struct io_uring *ioring, struct ioreq *req, int fd)
+{
+ req->type = IOREQ_CLOSE;
+ req->fd = fd;
+
+ enqueue_ioreq(ioring, req);
+}
+
+void
+submit_ioreqs(struct io_uring *ioring);
+
+int
+create_server_socket(char const *restrict host, char const *restrict port);
+
+/* uri parsing and normalisation
+ * ===========================================================================
+ */
+
+struct urifrag {
+ char *ptr;
+ int len;
+};
+
+bool
+urifrag_try_normalise(struct urifrag *frag);
+
+void
+urifrag_remove_dot_segments(struct urifrag *frag);
+
+struct uri {
+ struct urifrag scheme;
+ struct urifrag user;
+ struct urifrag host;
+ struct urifrag port;
+ struct urifrag path;
+ struct urifrag query;
+ struct urifrag fragment;
+};
+
+struct uri
+uri_parse(char *buf, size_t len);
+
+/* http connection state
+ * ===========================================================================
+ */
+
+#define HTTP_SEP "\r\n"
+#define HTTP_TERM HTTP_SEP HTTP_SEP
+
+enum http_method {
+ UNKNOWN_METHOD,
+ HTTP_HEAD,
+ HTTP_GET,
+};
+
+inline enum http_method
+method_from_str(char *method)
+{
+ if (strcmp(method, "HEAD") == 0)
+ return HTTP_HEAD;
+
+ if (strcmp(method, "GET") == 0)
+ return HTTP_GET;
+
+ return UNKNOWN_METHOD;
+}
+
+inline char const *
+method_str(enum http_method method)
+{
+ switch (method) {
+ case HTTP_HEAD: return "HEAD";
+ case HTTP_GET: return "GET";
+ default: return NULL;
+ }
+}
+
+enum http_version {
+ HTTP_09,
+ HTTP_10,
+ HTTP_11,
+ HTTP_20,
+ HTTP_30,
+};
+
+inline enum http_version
+version_from_str(char *version)
+{
+ if (strcmp(version, "HTTP/0.9") == 0)
+ return HTTP_09;
+
+ if (strcmp(version, "HTTP/1.0") == 0)
+ return HTTP_10;
+
+ if (strcmp(version, "HTTP/1.1") == 0)
+ return HTTP_11;
+
+ if (strcmp(version, "HTTP/2") == 0)
+ return HTTP_20;
+
+ if (strcmp(version, "HTTP/3") == 0)
+ return HTTP_30;
+
+ return HTTP_09;
+}
+
+inline char const *
+version_str(enum http_version version)
+{
+ switch (version) {
+ case HTTP_09: return "HTTP/0.9";
+ case HTTP_10: return "HTTP/1.0";
+ case HTTP_11: return "HTTP/1.1";
+ case HTTP_20: return "HTTP/2";
+ case HTTP_30: return "HTTP/3";
+ default: return NULL;
+ }
+}
+
+enum http_status {
+ HTTP_200 = 200, /* OK */
+ HTTP_204 = 204, /* No Content */
+ HTTP_206 = 206, /* Partial Content */
+
+ HTTP_400 = 400, /* Bad Request */
+ HTTP_401 = 401, /* Unauthorized */
+ HTTP_403 = 403, /* Forbidden */
+ HTTP_404 = 404, /* Not Found */
+ HTTP_405 = 405, /* Method Not Allowed */
+ HTTP_406 = 406, /* Not Acceptable */
+ HTTP_408 = 408, /* Request Timeout */
+ HTTP_411 = 411, /* Length Required */
+ HTTP_413 = 413, /* Payload Too Large */
+ HTTP_415 = 415, /* Unsupported Media Type */
+ HTTP_416 = 416, /* Range Not Satisfiable */
+ HTTP_422 = 422, /* Unprocessable Content */
+ HTTP_431 = 431, /* Request Header Fields Too Large */
+
+ HTTP_500 = 500, /* Internal Server Error */
+ HTTP_501 = 501, /* Not Implemented */
+ HTTP_503 = 503, /* Service Unavailable */
+ HTTP_505 = 505, /* HTTP Version Not Supported */
+};
+
+inline char const *
+status_str(enum http_status status)
+{
+ switch (status) {
+ case HTTP_200: return "OK";
+ case HTTP_204: return "No Content";
+ case HTTP_206: return "Partial Content";
+
+ case HTTP_400: return "Bad Request";
+ case HTTP_401: return "Unauthorized";
+ case HTTP_403: return "Forbidden";
+ case HTTP_404: return "Not Found";
+ case HTTP_405: return "Method Not Allowed";
+ case HTTP_406: return "Not Acceptable";
+ case HTTP_408: return "Request Timeout";
+ case HTTP_411: return "Length Required";
+ case HTTP_413: return "Payload Too Large";
+ case HTTP_415: return "Unsupported Media Type";
+ case HTTP_416: return "Range Not Satisfiable";
+ case HTTP_422: return "Unprocessable Content";
+ case HTTP_431: return "Request Header Fields Too Large";
+
+ case HTTP_500: return "Internal Server Error";
+ case HTTP_501: return "Not Implemented";
+ case HTTP_503: return "Service Unavailable";
+ case HTTP_505: return "HTTP Version Not Supported";
+
+ default: return NULL;
+ }
+}
+
+enum content_encoding {
+ ENCODING_NONE,
+ ENCODING_GZIP,
+};
+
+inline char const *
+content_encoding_str(enum content_encoding encoding)
+{
+ switch (encoding) {
+ case ENCODING_GZIP: return "gzip";
+ default: return NULL;
+ }
+}
+
+inline enum content_encoding
+content_encoding_from_list(char *list)
+{
+ enum content_encoding best_encoding = ENCODING_NONE;
+
+ char *enc, *saveptr;
+ for (enc = strtok_r(list, ", ", &saveptr);
+ enc;
+ enc = strtok_r(NULL, ", ", &saveptr)) {
+ if (strcmp(enc, "gzip") == 0)
+ best_encoding = MAX(best_encoding, ENCODING_GZIP);
+ }
+
+ return best_encoding;
+}
+
+enum content_type {
+ APPLICATION_OCTET_STREAM,
+
+ IMAGE_BMP,
+ IMAGE_GIF,
+ IMAGE_JPEG,
+ IMAGE_PNG,
+ IMAGE_SVG,
+
+ TEXT_PLAIN,
+ TEXT_HTML,
+ TEXT_CSS,
+ TEXT_JAVASCRIPT,
+
+ ANY_CONTENT_TYPE,
+};
+
+inline char const *
+content_type_str(enum content_type type)
+{
+ switch (type) {
+ case APPLICATION_OCTET_STREAM: return "application/octet-stream";
+
+ case IMAGE_BMP: return "image/bmp";
+ case IMAGE_GIF: return "image/gif";
+ case IMAGE_JPEG: return "image/jpeg";
+ case IMAGE_PNG: return "image/png";
+ case IMAGE_SVG: return "image/svg+xml";
+
+ case TEXT_PLAIN: return "text/plain";
+ case TEXT_HTML: return "text/html";
+ case TEXT_CSS: return "text/css";
+ case TEXT_JAVASCRIPT: return "text/javascript";
+
+ default: return NULL;
+ }
+}
+
+inline bool
+content_type_is_text(enum content_type type)
+{
+ switch (type) {
+ case IMAGE_SVG:
+ case TEXT_PLAIN:
+ case TEXT_HTML:
+ case TEXT_CSS:
+ case TEXT_JAVASCRIPT:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+inline enum content_type
+content_type_from_mime(char *mime)
+{
+ if (strcmp(mime, "*/*") == 0)
+ return ANY_CONTENT_TYPE;
+
+ if (strcmp(mime, "image/bmp") == 0)
+ return IMAGE_BMP;
+
+ if (strcmp(mime, "image/gif") == 0)
+ return IMAGE_GIF;
+
+ if (strcmp(mime, "image/jpeg") == 0)
+ return IMAGE_JPEG;
+
+ if (strcmp(mime, "image/png") == 0)
+ return IMAGE_PNG;
+
+ if (strcmp(mime, "image/svg+xml") == 0)
+ return IMAGE_SVG;
+
+ if (strcmp(mime, "text/plain") == 0)
+ return TEXT_PLAIN;
+
+ if (strcmp(mime, "text/html") == 0)
+ return TEXT_HTML;
+
+ if (strcmp(mime, "text/css") == 0)
+ return TEXT_CSS;
+
+ if (strcmp(mime, "text/javascript") == 0)
+ return TEXT_JAVASCRIPT;
+
+ return APPLICATION_OCTET_STREAM;
+}
+
+inline enum content_type
+content_type_from_ext(char *ext, size_t len)
+{
+ if (!ext)
+ return APPLICATION_OCTET_STREAM;
+
+ if (len == strlen(".bmp") && strncmp(ext, ".bmp", len) == 0)
+ return IMAGE_BMP;
+
+ if (len == strlen(".gif") && strncmp(ext, ".gif", len) == 0)
+ return IMAGE_GIF;
+
+ if ((len == strlen(".jpg") && strncmp(ext, ".jpg", len) == 0) ||
+ (len == strlen(".jpeg") && strncmp(ext, ".jpeg", len) == 0))
+ return IMAGE_JPEG;
+
+ if (len == strlen(".png") && strncmp(ext, ".png", len) == 0)
+ return IMAGE_PNG;
+
+ if (len == strlen(".svg") && strncmp(ext, ".svg", len) == 0)
+ return IMAGE_SVG;
+
+ if (len == strlen(".txt") && strncmp(ext, ".txt", len) == 0)
+ return TEXT_PLAIN;
+
+ if (len == strlen(".html") && strncmp(ext, ".html", len) == 0)
+ return TEXT_HTML;
+
+ if (len == strlen(".css") && strncmp(ext, ".css", len) == 0)
+ return TEXT_CSS;
+
+ if (len == strlen(".js") && strncmp(ext, ".js", len) == 0)
+ return TEXT_JAVASCRIPT;
+
+ return APPLICATION_OCTET_STREAM;
+}
+
+struct http_request {
+ enum http_method method;
+ enum http_version version;
+ struct uri uri;
+};
+
+struct http_headers {
+ int keep_alive;
+ char *accept;
+ enum content_encoding encoding;
+ bool have_range;
+ struct { uint64_t begin, end; } range;
+ enum content_type content_type;
+ uint64_t content_length;
+};
+
+void
+parse_http_request(char *src, struct http_request *request, struct http_headers *headers);
+
+struct http_response {
+ int fd;
+ size_t len;
+ uint64_t off;
+};
+
+struct connection {
+ int socket;
+
+ struct ioreq ioreq;
+
+ char buf[CONNECTION_BUFFER_SIZE];
+ size_t cap, len, cur;
+
+ struct http_request request;
+ struct http_headers request_headers;
+ struct http_response response;
+ struct http_headers response_headers;
+
+ struct timespec timeout;
+
+ struct list_node list_node;
+};
+
+void
+handle_accept(struct io_uring *ioring, struct connection *conn, int sock);
+
+void
+handle_request_chunk(struct io_uring *ioring, struct connection *conn, int webroot, int res);
+
+void
+handle_request(struct io_uring *ioring, struct connection *conn, int webroot);
+
+void
+handle_response_chunk(struct io_uring *ioring, struct connection *conn, int res);
+
+void
+handle_fileread_chunk(struct io_uring *ioring, struct connection *conn, int res);
+
+void
+error_connection(struct io_uring *ioring, struct connection *conn,
+ enum http_status err, bool linger);
+
+void
+close_connection(struct io_uring *ioring, struct connection *conn);
+
+struct connection_pool {
+ struct connection *ptr;
+ struct list freelist;
+};
+
+inline int
+connection_pool_init(struct connection_pool *pool, size_t capacity)
+{
+ size_t cap = capacity * sizeof *pool->ptr;
+ int prot = PROT_READ | PROT_WRITE;
+ int flags = MAP_PRIVATE | MAP_ANONYMOUS;
+
+ if ((pool->ptr = mmap(NULL, cap, prot, flags, -1, 0)) == MAP_FAILED) {
+ fprintf(stderr, "Failed to map memory for connection pool\n");
+ return -1;
+ }
+
+ madvise(pool->ptr, cap, MADV_HUGEPAGE);
+
+ pool->freelist.head = pool->freelist.tail = NULL;
+ for (size_t i = 0; i < capacity; i++)
+ list_push_tail(&pool->freelist, &pool->ptr[i].list_node);
+
+ return 0;
+}
+
+inline struct connection *
+connection_pool_alloc(struct connection_pool *pool)
+{
+ struct list_node *node = list_pop_head(&pool->freelist);
+ return FROM_NODE(node, struct connection, list_node);
+}
+
+inline void
+connection_pool_free(struct connection_pool *restrict pool,
+ struct connection *restrict conn)
+{
+ list_push_tail(&pool->freelist, &conn->list_node);
+}
+
+/* server application
+ * ===========================================================================
+ */
+
+struct opts {
+ int verbose;
+ char *host, *port;
+ char *webroot;
+};
+
+extern struct opts opts;
+
+extern sig_atomic_t quit;
+extern sig_atomic_t reload_tree;
+
+int
+serve(struct io_uring *ioring, int webroot, int sock, struct connection_pool *pool);
+
+#endif /* SERVE_H */
diff --git a/serve/uri.c b/serve/uri.c
@@ -0,0 +1,229 @@
+#include "serve.h"
+
+static inline unsigned char
+fromxdigit(char c)
+{
+ static unsigned char lut[1 << CHAR_BIT] = {
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
+ ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+
+ ['a'] = 10, ['A'] = 10, ['b'] = 11, ['B'] = 11,
+ ['c'] = 12, ['C'] = 12, ['d'] = 13, ['D'] = 13,
+ ['e'] = 14, ['E'] = 14, ['f'] = 15, ['F'] = 15,
+ };
+
+ return lut[(unsigned char) c];
+}
+
+bool
+urifrag_try_normalise(struct urifrag *frag)
+{
+ char *cur = frag->ptr;
+
+ for (int i = 0; i < frag->len; i++) {
+ char buf[3] = {
+ frag->ptr[i],
+ (i+1 < frag->len) ? frag->ptr[i+1] : '\0',
+ (i+2 < frag->len) ? frag->ptr[i+2] : '\0',
+ };
+
+ unsigned char decoded = buf[0];
+ if (decoded == '%') {
+ if (!isxdigit(buf[1]) || !isxdigit(buf[2]))
+ return false;
+
+ decoded = (fromxdigit(buf[1]) << 4) | fromxdigit(buf[2]);
+ }
+
+ *cur++ = decoded;
+ }
+
+ frag->len = cur - frag->ptr;
+
+ return true;
+}
+
+static inline char *
+prev_dot_segment(char *begin, char *cur)
+{
+ while (begin < cur) {
+ if (*(--cur) == '/')
+ return cur;
+ }
+
+ return begin;
+}
+
+void
+urifrag_remove_dot_segments(struct urifrag *frag)
+{
+ /* TODO: fix me! */
+
+ char *wrcur = frag->ptr, *rdcur = frag->ptr, *end = frag->ptr + frag->len;
+
+ while (rdcur < end) {
+ char buf[3] = {
+ rdcur[0],
+ ((rdcur+1) < end) ? rdcur[1] : '\0',
+ ((rdcur+2) < end) ? rdcur[2] : '\0',
+ };
+
+ if (buf[0] == '.' && buf[1] == '.' && (buf[2] == '/' || buf[2] == '\0')) {
+ wrcur = prev_dot_segment(frag->ptr, wrcur - 1);
+ rdcur += 2;
+ } else if (buf[0] == '.' && (buf[1] == '/' || buf[1] == '\0')) {
+ rdcur += 2;
+ } else {
+ *wrcur++ = *rdcur++;
+ }
+ }
+
+ frag->len = wrcur - frag->ptr;
+}
+
+struct uri
+uri_parse(char *buf, size_t len)
+{
+ /* uri = [scheme ':'] ['//' authority] path ['?' query] ['#' fragment]
+ * authority = [user '@'] host [':' port]
+ */
+
+ struct uri result;
+ memset(&result, 0, sizeof result);
+
+ char *cur = buf, *end = buf + len;
+
+ int state = 0;
+ struct urifrag frag = {
+ .ptr = cur,
+ .len = 0,
+ };
+
+ while (true) {
+#define __STATE(n, toks) \
+ { state = (n); cur += toks; frag.ptr = cur; frag.len = 0; continue; }
+
+ char buf[3] = {
+ (cur < end) ? cur[0] : '\0',
+ (cur+1 < end) ? cur[1] : '\0',
+ (cur+2 < end) ? cur[2] : '\0',
+ };
+
+ switch (state) {
+ case 0: { /* scheme or authority or path */
+ if (buf[0] == ':') {
+ result.scheme = frag;
+ __STATE(1, 1)
+ } else if (buf[0] == '/' && buf[1] == '/') {
+ __STATE(2, 2)
+ } else if (buf[0] == '?') {
+ result.path = frag;
+ __STATE(6, 1)
+ } else if (buf[0] == '#') {
+ result.path = frag;
+ __STATE(7, 1)
+ } else if (buf[0] == '\0') {
+ result.path = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 1: { /* authority or path */
+ if (buf[0] == '/' && buf[1] == '/') {
+ __STATE(2, 2)
+ } else if (buf[0] == '?') {
+ result.path = frag;
+ __STATE(6, 1)
+ } else if (buf[0] == '#') {
+ result.path = frag;
+ __STATE(7, 1)
+ } else if (buf[0] == '\0') {
+ result.path = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 2: { /* authority */
+ if (buf[0] == '@') {
+ result.user = frag;
+ __STATE(3, 1)
+ } else if (buf[0] == ':') {
+ result.host = frag;
+ __STATE(4, 1)
+ } else if (buf[0] == '/') {
+ result.host = frag;
+ __STATE(5, 0)
+ } else if (buf[0] == '\0') {
+ result.host = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 3: { /* host */
+ if (buf[0] == ':') {
+ result.host = frag;
+ __STATE(4, 1)
+ } else if (buf[0] == '/') {
+ result.host = frag;
+ __STATE(5, 0)
+ } else if (buf[0] == '\0') {
+ result.host = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 4: { /* port */
+ if (!isdigit(buf[0])) {
+ result.port = frag;
+ __STATE(5, 0)
+ } else if (buf[0] == '\0') {
+ result.port = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 5: { /* path */
+ if (buf[0] == '?') {
+ result.path = frag;
+ __STATE(6, 1)
+ } else if (buf[0] == '#') {
+ result.path = frag;
+ __STATE(7, 1)
+ } else if (buf[0] == '\0') {
+ result.path = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 6: { /* query */
+ if (buf[0] == '#') {
+ result.query = frag;
+ __STATE(7, 1)
+ } else if (buf[0] == '\0') {
+ result.query = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 7: { /* fragment */
+ if (buf[0] == '\0') {
+ result.fragment = frag;
+ __STATE(99, 0)
+ }
+ } break;
+
+ case 99: /* end-of-string */
+ goto end;
+
+ }
+
+ frag.len++;
+
+ cur++;
+
+#undef __STATE
+ }
+
+end:
+ return result;
+}
diff --git a/serve/utils.c b/serve/utils.c
@@ -0,0 +1,33 @@
+#include "serve.h"
+
+extern inline char *
+strnstr(char *restrict str, size_t len, char const *restrict needle);
+
+extern inline void
+arena_reset(struct arena *arena);
+
+extern inline void *
+arena_alloc(struct arena *arena, uint64_t size, uint64_t align);
+
+extern inline void
+list_push_head(struct list *restrict list, struct list_node *restrict node);
+
+extern inline void
+list_push_tail(struct list *restrict list, struct list_node *restrict node);
+
+extern inline struct list_node *
+list_pop_head(struct list *list);
+
+extern inline struct list_node *
+list_pop_tail(struct list *list);
+
+extern inline struct timespec
+get_timeout(struct timespec const *ts, uint64_t timeout_ms);
+
+extern inline uint64_t
+get_timestamp(struct timespec const *ts);
+
+extern inline void
+timespec_diff(struct timespec const *restrict lhs,
+ struct timespec const *restrict rhs,
+ struct timespec *restrict res);
diff --git a/website/connection.c b/website/connection.c
@@ -1,433 +0,0 @@
-#include "website.h"
-
-extern inline enum http_method
-method_from_str(char *method);
-
-extern inline char const *
-method_str(enum http_method method);
-
-extern inline enum http_version
-version_from_str(char *veresion);
-
-extern inline char const *
-version_str(enum http_version version);
-
-extern inline char const *
-status_str(enum http_status status);
-
-extern inline char const *
-content_encoding_str(enum content_encoding encoding);
-
-extern inline enum content_encoding
-content_encoding_from_list(char *list);
-
-extern inline char const *
-content_type_str(enum content_type type);
-
-extern inline bool
-content_type_is_text(enum content_type type);
-
-extern inline enum content_type
-content_type_from_mime(char *mime);
-
-extern inline enum content_type
-content_type_from_ext(char *ext, size_t len);
-
-void
-parse_http_request(char *src, struct http_request *request, struct http_headers *headers)
-{
- char *saveptr;
- request->method = method_from_str(strtok_r(src, " ", &saveptr));
- char *rawuri = strtok_r(NULL, " ", &saveptr);
- request->uri = uri_parse(rawuri, strlen(rawuri));
- request->version = version_from_str(strtok_r(NULL, "\r\n", &saveptr));
-
- memset(headers, 0, sizeof *headers);
-
- headers->keep_alive = request->version > HTTP_09;
-
- char *header;
- while ((header = strtok_r(NULL, "\r\n", &saveptr))) {
- char *val;
- char *key = strtok_r(header, ": ", &val);
-
- while (isspace(*val)) /* strip leading whitespace */
- val++;
-
- for (char *ptr = key; *ptr; ptr++) /* normalise as lowercase */
- *ptr = isalpha(*ptr) ? tolower(*ptr) : *ptr;
-
- if (strcmp(key, "connection") == 0) {
- for (char *ptr = val; *ptr; ptr++)
- *ptr = isalpha(*ptr) ? tolower(*ptr) : *ptr;
- headers->keep_alive = !(strcmp(val, "close") == 0);
- } else if (strcmp(key, "accept") == 0) {
- headers->accept = val;
- } else if (strcmp(key, "accept-encoding") == 0) {
- headers->encoding = content_encoding_from_list(val);
- } else if (strcmp(key, "range") == 0) {
- // TODO: implement me!
- } else if (strcmp(key, "content-type") == 0) {
- headers->content_type = content_type_from_mime(val);
- } else if (strcmp(key, "content-length") == 0) {
- headers->content_length = strtoull(val, NULL, 0);
- }
- }
-}
-
-void
-handle_accept(struct io_uring *ioring, struct connection *conn, int sock)
-{
- conn->socket = sock;
- conn->cap = sizeof conn->buf;
- conn->len = conn->cur = 0;
-
- struct timespec now;
- clock_gettime(CLOCK_MONOTONIC_RAW, &now);
- conn->timeout = get_timeout(&now, CONNECTION_TIMEOUT_MS);
-
- enqueue_recv(ioring, &conn->ioreq, conn->socket, conn->buf, conn->cap, 0);
-}
-
-static bool
-have_request_headers(struct connection *conn)
-{
- char *last_parse_end = conn->buf + conn->cur;
- char *hdrs = MAX(last_parse_end - (strlen(HTTP_TERM) - 1), conn->buf);
- char *hdrs_end = strnstr(hdrs, conn->len - conn->cur, HTTP_TERM);
-
- if (hdrs_end)
- *hdrs_end = '\0';
-
- return hdrs_end != NULL;
-}
-
-void
-handle_request_chunk(struct io_uring *ioring, struct connection *conn, int webroot, int res)
-{
- struct timespec now;
- clock_gettime(CLOCK_MONOTONIC_RAW, &now);
-// if (get_timestamp(&conn->timeout) < get_timestamp(&now))
-// goto error_timeout;
-
- conn->len += res;
-
- int have_headers = have_request_headers(conn);
- conn->cur += res;
-
- if (!have_headers && conn->len == conn->cap) /* cannot read more data */
- goto error_payload_too_large;
-
- if (!have_headers) { /* can read more data */
- enqueue_recv(ioring, &conn->ioreq, conn->socket,
- conn->buf + conn->len, conn->cap - conn->len, 0);
- return;
- }
-
- /* have request header, parse it */
- handle_request(ioring, conn, webroot);
- return;
-
-error_timeout:
- error_connection(ioring, conn, HTTP_408, false);
- return;
-
-error_payload_too_large:
- error_connection(ioring, conn, HTTP_413, true);
- return;
-}
-
-static bool
-sanitise_uri_to_path(struct uri *uri, char buf[static PATH_MAX])
-{
- if (uri->path.len == 0)
- return false;
-
- if (!urifrag_try_normalise(&uri->path))
- return false;
-
- urifrag_remove_dot_segments(&uri->path);
-
- if (PATH_MAX <= uri->path.len)
- return false;
-
- if (*(uri->path.ptr) == '/') {
- uri->path.ptr++;
- uri->path.len--;
- }
-
- char *end = stpncpy(buf, uri->path.ptr, uri->path.len);
- *end = '\0';
-
- size_t written = end - buf;
- if (!written || end[-1] == '/') {
- if (PATH_MAX < written + strlen("index.html"))
- return false;
-
- *stpncpy(end, "index.html", PATH_MAX - written - strlen("index.html")) = '\0';
- }
-
- return true;
-}
-
-static void
-prepare_response_headers(struct connection *conn, enum http_status status,
- struct http_headers *headers)
-{
- conn->len = conn->cur = 0;
-
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "HTTP/1.1 %d %s" HTTP_SEP, status, status_str(status));
-
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "Connection: %s" HTTP_SEP, headers->keep_alive ? "keep-alive" : "close");
-
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "Content-Length: %" PRIu64 HTTP_SEP, headers->content_length);
-
- if (headers->have_range) {
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "Content-Range: bytes %" PRIu64 "-%" PRIu64 "/%" PRIu64 HTTP_SEP,
- headers->range.begin, headers->range.end, headers->content_length);
- }
-
- if (headers->content_length) {
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "Content-Type: %s", content_type_str(headers->content_type));
-
- if (content_type_is_text(headers->content_type)) {
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "; charset=utf-8");
- }
-
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len, HTTP_SEP);
-
- if (headers->encoding) {
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len,
- "Content-Encoding: %s" HTTP_SEP,
- content_encoding_str(headers->encoding));
- }
- }
-
- conn->len += snprintf(conn->buf + conn->len, conn->cap - conn->len, HTTP_SEP);
-
- assert(conn->len < conn->cap);
-
- if (opts.verbose)
- printf("response headers: %zu bytes\n---\n%.*s",
- conn->len, (int) conn->len, conn->buf);
-}
-
-void
-handle_request(struct io_uring *ioring, struct connection *conn, int webroot)
-{
- parse_http_request(conn->buf, &conn->request, &conn->request_headers);
-
- if (opts.verbose) {
- printf("\trequest: %s, %s\n", method_str(conn->request.method), version_str(conn->request.version));
- printf("\turi: path: '%.*s'\n", conn->request.uri.path.len, conn->request.uri.path.ptr);
- printf("\tkeep-alive: %d\n", conn->request_headers.keep_alive);
- printf("\taccept: %s\n", conn->request_headers.accept);
- printf("\taccept-encoding: %d\n", conn->request_headers.encoding);
- printf("\trange: %" PRIu64 "-%" PRIu64 "\n", conn->request_headers.range.begin, conn->request_headers.range.end);
- printf("\tcontent-type: %d\n", conn->request_headers.content_type);
- printf("\tcontent-length: %" PRIu64 "\n", conn->request_headers.content_length);
- }
-
- if (conn->request.method != HTTP_HEAD && conn->request.method != HTTP_GET)
- goto error_not_implemented;
-
- if (conn->request.version != HTTP_11)
- goto error_http_version_not_supported;
-
- if (conn->request_headers.content_length) /* we do not support request bodies */
- goto error_unprocessable_request;
-
- char path[PATH_MAX];
- if (!sanitise_uri_to_path(&conn->request.uri, path))
- goto error_bad_request;
-
- if (opts.verbose) {
- printf("\tsanitised uri: '%s'\n", path);
- }
-
- // TODO: read from file cache instead of reading directly?
- conn->response.fd = openat(webroot, path, O_RDONLY | O_CLOEXEC);
- if (conn->response.fd < 0) {
- if (errno == EACCES)
- goto error_forbidden;
-
- if (errno == ENOENT)
- goto error_not_found;
-
- goto error_internal_server_error;
- }
-
- struct stat statbuf;
- if (fstat(conn->response.fd, &statbuf) < 0) {
- close(conn->response.fd);
- goto error_internal_server_error;
- }
-
- if (!S_ISREG(statbuf.st_mode)) {
- close(conn->response.fd);
- goto error_not_found;
- }
-
- conn->response.len = statbuf.st_size;
- conn->response.off = 0;
-
- if (conn->request_headers.have_range) {
- if (conn->request_headers.range.begin < conn->request_headers.range.end)
- goto error_bad_request;
-
- if (conn->response.len < conn->request_headers.range.begin ||
- conn->response.len < conn->request_headers.range.end)
- goto error_range_not_satisfiable;
-
- conn->response.len = conn->request_headers.range.end
- - conn->request_headers.range.begin;
- conn->response.off = conn->request_headers.range.begin;
- }
-
- char *ext = strrchr(path, '.');
- conn->response_headers = (struct http_headers) {
- .keep_alive = conn->request_headers.keep_alive,
- .encoding = ENCODING_NONE,
- .have_range = conn->request_headers.have_range,
- .range = conn->request_headers.range,
- .content_type = content_type_from_ext(ext, strlen(ext)),
- .content_length = conn->response.len,
- };
-
- struct timespec now;
- clock_gettime(CLOCK_MONOTONIC_RAW, &now);
- conn->timeout = get_timeout(&now, CONNECTION_TIMEOUT_MS);
-
- enum http_status status = conn->response_headers.have_range ? HTTP_206 : HTTP_200;
- prepare_response_headers(conn, status, &conn->response_headers);
-
- if (conn->request.method == HTTP_HEAD) {
- close(conn->response.fd); /* no body to send, dont delay close */
- conn->response.fd = -1;
-
- enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
- } else if (conn->request.method == HTTP_GET) { /* prepare body */
- size_t len = MIN(conn->response.len, conn->cap - conn->len);
- enqueue_read(ioring, &conn->ioreq, conn->response.fd,
- conn->buf + conn->len, len, conn->response.off);
- }
-
- return;
-
-error_bad_request:
- error_connection(ioring, conn, HTTP_400, true);
- return;
-
-error_forbidden:
- error_connection(ioring, conn, HTTP_403, true);
- return;
-
-error_not_found:
- error_connection(ioring, conn, HTTP_404, true);
- return;
-
-error_range_not_satisfiable:
- error_connection(ioring, conn, HTTP_416, true);
- return;
-
-error_unprocessable_request:
- error_connection(ioring, conn, HTTP_422, true);
- return;
-
-error_internal_server_error:
- error_connection(ioring, conn, HTTP_500, true);
- return;
-
-error_not_implemented:
- error_connection(ioring, conn, HTTP_501, true);
- return;
-
-error_http_version_not_supported:
- error_connection(ioring, conn, HTTP_505, true);
- return;
-}
-
-void
-handle_response_chunk(struct io_uring *ioring, struct connection *conn, int res)
-{
- struct timespec now;
- clock_gettime(CLOCK_MONOTONIC_RAW, &now);
-// if (get_timestamp(&conn->timeout) < get_timestamp(&now))
-// goto error_timeout;
-
- conn->cur += res;
-
- if (conn->cur < conn->len) { /* have remaining buffered data to send */
- enqueue_send(ioring, &conn->ioreq, conn->socket,
- conn->buf + conn->cur, conn->len - conn->cur, 0);
- return;
- }
-
- if (conn->response.len) { /* have remaining data to read into buffer */
- conn->len = conn->cur = 0;
-
- size_t len = MIN(conn->response.len, conn->cap);
- enqueue_read(ioring, &conn->ioreq, conn->response.fd,
- conn->buf, len, conn->response.off);
- return;
- } else if (conn->response.fd > 0) { /* no more data to read, close file */
- close(conn->response.fd);
- conn->response.fd = -1;
- }
-
- if (conn->response_headers.keep_alive) {
- conn->len = conn->cur = 0;
- enqueue_recv(ioring, &conn->ioreq, conn->socket,
- conn->buf, conn->cap, 0);
- return;
- }
-
-error_timeout:
- close_connection(ioring, conn);
- return;
-}
-
-void
-handle_fileread_chunk(struct io_uring *ioring, struct connection *conn, int res)
-{
- conn->response.off += res;
- conn->response.len -= res;
- conn->len += res;
-
- enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
-}
-
-void
-error_connection(struct io_uring *ioring, struct connection *conn,
- enum http_status err, bool linger)
-{
- struct http_headers error_headers = {
- .keep_alive = linger,
- .content_length = 0,
- };
-
- prepare_response_headers(conn, err, &error_headers);
- enqueue_send(ioring, &conn->ioreq, conn->socket, conn->buf, conn->len, 0);
-}
-
-void
-close_connection(struct io_uring *ioring, struct connection *conn)
-{
- enqueue_close(ioring, &conn->ioreq, conn->socket);
-}
-
-extern inline int
-connection_pool_init(struct connection_pool *pool, size_t capacity);
-
-extern inline struct connection *
-connection_pool_alloc(struct connection_pool *pool);
-
-extern inline void
-connection_pool_free(struct connection_pool *restrict pool,
- struct connection *restrict conn);
diff --git a/website/main.c b/website/main.c
@@ -1,294 +0,0 @@
-#include "website.h"
-
-struct opts opts = {
- .verbose = 0,
- .host = "localhost",
- .port = "8080",
-};
-
-#define OPTSTR "hva:p:"
-static void
-usage(char *prog)
-{
- fprintf(stderr, "Usage: %s [-hv] [-a <addr>] [-p <port>] <webroot>\n", prog);
- fprintf(stderr, "\t-h : display this help information\n");
- fprintf(stderr, "\t-v : enable verbose logging\n");
- fprintf(stderr, "\t-a : set the server socket bind address (default: localhost)\n");
- fprintf(stderr, "\t-p : set the server socket bind port (default: 8080)\n");
- fprintf(stderr, "\twebroot : directory out of which to serve files\n");
-}
-
-static int
-parse_opts(int argc, char **argv)
-{
- int opt;
- while ((opt = getopt(argc, argv, OPTSTR)) > 0) {
- switch (opt) {
- case 'v':
- opts.verbose = 1;
- break;
-
- case 'a':
- opts.host = optarg;
- break;
-
- case 'p':
- opts.port = optarg;
- break;
-
- default:
- return -1;
- }
- }
-
- if (optind >= argc) {
- fprintf(stderr, "Missing webroot\n");
- return -1;
- }
-
- opts.webroot = argv[optind];
-
- return 0;
-}
-
-sig_atomic_t quit;
-
-static void
-close_handler(int sig, siginfo_t *info, void *uctx)
-{
- (void) info;
- (void) uctx;
-
- fprintf(stderr, "Signal %d caught. Quitting\n", sig);
-
- quit = 1;
-}
-
-sig_atomic_t reload_tree;
-
-static void
-reload_handler(int sig, siginfo_t *info, void *uctx)
-{
- (void) info;
- (void) uctx;
-
- fprintf(stderr, "Signal %d caught. Reloading file tree\n", sig);
-
- reload_tree = 1;
-}
-
-int
-serve(struct io_uring *ioring, int webroot, int sock, struct connection_pool *pool)
-{
- listen(sock, SERVER_LISTEN_BACKLOG);
-
- struct sockaddr_storage client_addr;
- socklen_t client_addrlen = sizeof client_addr;
-
- struct ioreq accept_req = {
- .type = IOREQ_ACCEPT,
- .fd = sock,
- .accept.addr = (struct sockaddr *) &client_addr,
- .accept.addrlen = &client_addrlen,
- .accept.flags = 0,
- };
-
- enqueue_ioreq(ioring, &accept_req);
- submit_ioreqs(ioring);
-
- quit = 0;
- while (!quit) {
- struct io_uring_cqe *cqe;
- if (io_uring_wait_cqe(ioring, &cqe) < 0) {
- fprintf(stderr, "io_uring_wait_cqe() failed\n");
- return EXIT_FAILURE;
- }
-
- unsigned head, seen = 0;
- io_uring_for_each_cqe(ioring, head, cqe) {
- struct ioreq *ioreq = io_uring_cqe_get_data(cqe);
- if (!ioreq) { /* overloaded client error messages */
- // TODO: do something?
- }
-
- int res = cqe->res;
- switch (ioreq->type) {
- case IOREQ_ACCEPT: {
- if (res < 0) { /* multishot failed, restart? */
- fprintf(stderr, "multishot accept failed\n");
- return EXIT_FAILURE;
- }
-
- struct connection *conn = connection_pool_alloc(pool);
- if (!conn) { /* overloaded server */
- close(res);
- goto next_cqe;
- }
-
- if (opts.verbose) {
- char host[NI_MAXHOST];
- char port[NI_MAXSERV];
- getnameinfo((struct sockaddr *) &client_addr,
- client_addrlen,
- host, sizeof host,
- port, sizeof port,
- NI_NUMERICHOST | NI_NUMERICSERV);
-
- fprintf(stderr, "[%d] Client connection from %s:%s\n",
- res, host, port);
- }
-
- handle_accept(ioring, conn, res);
- } break;
-
- case IOREQ_RECV: {
- struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
- assert(conn);
-
- if (res <= 0) {
- if (opts.verbose)
- fprintf(stderr, "recv(): %d (%s), closing connection\n", res, strerror(-res));
- close_connection(ioring, conn);
- break;
- }
-
- if (opts.verbose)
- fprintf(stderr, "[%d] Client recv: %d bytes\n",
- conn->socket, res);
-
- handle_request_chunk(ioring, conn, webroot, res);
- } break;
-
- case IOREQ_SEND: {
- struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
- assert(conn);
-
- if (res <= 0) {
- if (opts.verbose)
- fprintf(stderr, "send(): %d (%s), closing connection\n", res, strerror(-res));
- close_connection(ioring, conn);
- break;
- }
-
- if (opts.verbose)
- fprintf(stderr, "[%d] Client send(): %d bytes\n",
- conn->socket, res);
-
- handle_response_chunk(ioring, conn, res);
- } break;
-
- case IOREQ_READ: {
- struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
- assert(conn);
-
- if (res <= 0) {
- if (opts.verbose)
- fprintf(stderr, "read(): %d (%s), closing connection\n", res, strerror(-res));
- close_connection(ioring, conn);
- break;
- }
-
- if (opts.verbose)
- fprintf(stderr, "[%d] Client read: %d bytes\n",
- conn->socket, res);
-
- handle_fileread_chunk(ioring, conn, res);
- } break;
-
- case IOREQ_CLOSE: {
- struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
- assert(conn);
-
- if (opts.verbose)
- fprintf(stderr, "[%d] Client closed connection\n",
- conn->socket);
-
- connection_pool_free(pool, conn);
- } break;
- }
-
-next_cqe:
- seen++;
- }
-
- io_uring_cq_advance(ioring, seen);
-
- submit_ioreqs(ioring);
- }
-
- return EXIT_SUCCESS;
-}
-
-int
-main(int argc, char **argv)
-{
- if (parse_opts(argc, argv) < 0) {
- usage(argv[0]);
- exit(EXIT_FAILURE);
- }
-
- struct sigaction on_close = {
- .sa_sigaction = close_handler, .sa_flags = SA_SIGINFO,
- };
-
- sigaction(SIGINT, &on_close, NULL);
- sigaction(SIGKILL, &on_close, NULL);
-
- /* we want to cache the webroot tree in memory, and we need to know
- * when tree has changed. we could use inotify for this, but that has
- * the possibility of failure (overflow) and is more complicated than
- * the program updating our tree sending us a signal to reload
- */
- struct sigaction on_reload = {
- .sa_sigaction = reload_handler, .sa_flags = SA_SIGINFO,
- };
-
- sigaction(SIGHUP, &on_reload, NULL);
-
- struct connection_pool pool;
- if (connection_pool_init(&pool, CONNECTION_POOL_CAPACITY) < 0) {
- fprintf(stderr, "Failed to initialise connection pool\n");
- exit(EXIT_FAILURE);
- }
-
- struct io_uring ioring;
- if (ioring_init(&ioring)) {
- fprintf(stderr, "Failed to initialise io_uring\n");
- exit(EXIT_FAILURE);
- }
-
- int webroot = open(opts.webroot, O_DIRECTORY | O_PATH | O_CLOEXEC);
- if (webroot < 0) {
- fprintf(stderr, "Failed to open webroot %s\n", opts.webroot);
- exit(EXIT_FAILURE);
- }
-
- int sock = create_server_socket(opts.host, opts.port);
- if (sock < 0) {
- fprintf(stderr, "Failed to create and bind server socket to %s:%s\n",
- opts.host, opts.port);
- exit(EXIT_FAILURE);
- }
-
- struct sockaddr_storage addr;
- socklen_t addrlen = sizeof addr;
- if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) {
- fprintf(stderr, "Failed to get bound socket addr\n");
- exit(EXIT_FAILURE);
- }
-
- char bound_host[NI_MAXHOST], bound_port[NI_MAXSERV];
- getnameinfo((struct sockaddr *) &addr, addrlen,
- bound_host, sizeof bound_host,
- bound_port, sizeof bound_port,
- NI_NUMERICHOST | NI_NUMERICSERV);
-
- fprintf(stderr, "Server socket bound to %s:%s\n", bound_host, bound_port);
-
- exit(serve(&ioring, webroot, sock, &pool));
-}
-
-#include "utils.c"
-#include "net.c"
-#include "uri.c"
-#include "connection.c"
diff --git a/website/net.c b/website/net.c
@@ -1,128 +0,0 @@
-#include "website.h"
-
-int
-ioring_init(struct io_uring *ioring)
-{
- /* NOTE: we want to ensure that we can submit one operation for each
- * connection in the worst case, as well as a single multihit
- * accept for the server socket, so size our ioring submission
- * queue appropriately.
- */
- unsigned entries = 1 + (CONNECTION_POOL_CAPACITY * CONNECTION_CONCURRENT_OPS);
-
- return -io_uring_queue_init(entries, ioring, 0);
-}
-
-void
-enqueue_ioreq(struct io_uring *ioring, struct ioreq *ioreq)
-{
- struct io_uring_sqe *sqe = io_uring_get_sqe(ioring);
- if (!sqe) {
- submit_ioreqs(ioring);
- sqe = io_uring_get_sqe(ioring);
- }
-
- assert(sqe);
-
- switch (ioreq->type) {
- case IOREQ_ACCEPT:
- io_uring_prep_multishot_accept(sqe,
- ioreq->fd,
- ioreq->accept.addr,
- ioreq->accept.addrlen,
- ioreq->accept.flags);
- break;
-
- case IOREQ_RECV:
- io_uring_prep_recv(sqe,
- ioreq->fd,
- ioreq->recv.buf,
- ioreq->recv.len,
- ioreq->recv.flags);
- break;
-
- case IOREQ_SEND:
- io_uring_prep_send(sqe,
- ioreq->fd,
- ioreq->send.buf,
- ioreq->send.len,
- ioreq->send.flags);
- break;
-
- case IOREQ_READ:
- io_uring_prep_read(sqe,
- ioreq->fd,
- ioreq->read.buf,
- ioreq->read.len,
- ioreq->read.off);
- break;
-
- case IOREQ_CLOSE:
- io_uring_prep_close(sqe,
- ioreq->fd);
- break;
- }
-
- io_uring_sqe_set_data(sqe, ioreq);
-}
-
-extern inline void
-enqueue_accept(struct io_uring *ioring, struct ioreq *req, int fd,
- struct sockaddr *addr, socklen_t *addrlen, int flags);
-
-extern inline void
-enqueue_recv(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, int flags);
-
-extern inline void
-enqueue_send(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, int flags);
-
-extern inline void
-enqueue_read(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, uint64_t off);
-
-extern inline void
-enqueue_close(struct io_uring *ioring, struct ioreq *req, int fd);
-
-void
-submit_ioreqs(struct io_uring *ioring)
-{
- io_uring_submit(ioring);
-}
-
-int
-create_server_socket(char const *restrict host, char const *restrict port)
-{
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_STREAM,
- .ai_protocol = IPPROTO_TCP,
- .ai_flags = AI_NUMERICSERV,
- }, *addrinfo, *ptr;
-
- int res;
- if ((res = getaddrinfo(host, port, &hints, &addrinfo))) {
- fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(res));
- return -1;
- }
-
- int fd;
- for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
- fd = socket(ptr->ai_family, ptr->ai_socktype | O_CLOEXEC, ptr->ai_protocol);
- if (fd < 0)
- continue;
-
- if (bind(fd, ptr->ai_addr, ptr->ai_addrlen) == 0)
- break;
-
- close(fd);
- }
-
- freeaddrinfo(addrinfo);
-
- if (!ptr) /* failed to bind to any addresses */
- return -1;
-
- return fd;
-}
diff --git a/website/uri.c b/website/uri.c
@@ -1,229 +0,0 @@
-#include "website.h"
-
-static inline unsigned char
-fromxdigit(char c)
-{
- static unsigned char lut[1 << CHAR_BIT] = {
- ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
- ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
-
- ['a'] = 10, ['A'] = 10, ['b'] = 11, ['B'] = 11,
- ['c'] = 12, ['C'] = 12, ['d'] = 13, ['D'] = 13,
- ['e'] = 14, ['E'] = 14, ['f'] = 15, ['F'] = 15,
- };
-
- return lut[(unsigned char) c];
-}
-
-bool
-urifrag_try_normalise(struct urifrag *frag)
-{
- char *cur = frag->ptr;
-
- for (int i = 0; i < frag->len; i++) {
- char buf[3] = {
- frag->ptr[i],
- (i+1 < frag->len) ? frag->ptr[i+1] : '\0',
- (i+2 < frag->len) ? frag->ptr[i+2] : '\0',
- };
-
- unsigned char decoded = buf[0];
- if (decoded == '%') {
- if (!isxdigit(buf[1]) || !isxdigit(buf[2]))
- return false;
-
- decoded = (fromxdigit(buf[1]) << 4) | fromxdigit(buf[2]);
- }
-
- *cur++ = decoded;
- }
-
- frag->len = cur - frag->ptr;
-
- return true;
-}
-
-static inline char *
-prev_dot_segment(char *begin, char *cur)
-{
- while (begin < cur) {
- if (*(--cur) == '/')
- return cur;
- }
-
- return begin;
-}
-
-void
-urifrag_remove_dot_segments(struct urifrag *frag)
-{
- /* TODO: fix me! */
-
- char *wrcur = frag->ptr, *rdcur = frag->ptr, *end = frag->ptr + frag->len;
-
- while (rdcur < end) {
- char buf[3] = {
- rdcur[0],
- ((rdcur+1) < end) ? rdcur[1] : '\0',
- ((rdcur+2) < end) ? rdcur[2] : '\0',
- };
-
- if (buf[0] == '.' && buf[1] == '.' && (buf[2] == '/' || buf[2] == '\0')) {
- wrcur = prev_dot_segment(frag->ptr, wrcur - 1);
- rdcur += 2;
- } else if (buf[0] == '.' && (buf[1] == '/' || buf[1] == '\0')) {
- rdcur += 2;
- } else {
- *wrcur++ = *rdcur++;
- }
- }
-
- frag->len = wrcur - frag->ptr;
-}
-
-struct uri
-uri_parse(char *buf, size_t len)
-{
- /* uri = [scheme ':'] ['//' authority] path ['?' query] ['#' fragment]
- * authority = [user '@'] host [':' port]
- */
-
- struct uri result;
- memset(&result, 0, sizeof result);
-
- char *cur = buf, *end = buf + len;
-
- int state = 0;
- struct urifrag frag = {
- .ptr = cur,
- .len = 0,
- };
-
- while (true) {
-#define __STATE(n, toks) \
- { state = (n); cur += toks; frag.ptr = cur; frag.len = 0; continue; }
-
- char buf[3] = {
- (cur < end) ? cur[0] : '\0',
- (cur+1 < end) ? cur[1] : '\0',
- (cur+2 < end) ? cur[2] : '\0',
- };
-
- switch (state) {
- case 0: { /* scheme or authority or path */
- if (buf[0] == ':') {
- result.scheme = frag;
- __STATE(1, 1)
- } else if (buf[0] == '/' && buf[1] == '/') {
- __STATE(2, 2)
- } else if (buf[0] == '?') {
- result.path = frag;
- __STATE(6, 1)
- } else if (buf[0] == '#') {
- result.path = frag;
- __STATE(7, 1)
- } else if (buf[0] == '\0') {
- result.path = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 1: { /* authority or path */
- if (buf[0] == '/' && buf[1] == '/') {
- __STATE(2, 2)
- } else if (buf[0] == '?') {
- result.path = frag;
- __STATE(6, 1)
- } else if (buf[0] == '#') {
- result.path = frag;
- __STATE(7, 1)
- } else if (buf[0] == '\0') {
- result.path = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 2: { /* authority */
- if (buf[0] == '@') {
- result.user = frag;
- __STATE(3, 1)
- } else if (buf[0] == ':') {
- result.host = frag;
- __STATE(4, 1)
- } else if (buf[0] == '/') {
- result.host = frag;
- __STATE(5, 0)
- } else if (buf[0] == '\0') {
- result.host = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 3: { /* host */
- if (buf[0] == ':') {
- result.host = frag;
- __STATE(4, 1)
- } else if (buf[0] == '/') {
- result.host = frag;
- __STATE(5, 0)
- } else if (buf[0] == '\0') {
- result.host = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 4: { /* port */
- if (!isdigit(buf[0])) {
- result.port = frag;
- __STATE(5, 0)
- } else if (buf[0] == '\0') {
- result.port = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 5: { /* path */
- if (buf[0] == '?') {
- result.path = frag;
- __STATE(6, 1)
- } else if (buf[0] == '#') {
- result.path = frag;
- __STATE(7, 1)
- } else if (buf[0] == '\0') {
- result.path = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 6: { /* query */
- if (buf[0] == '#') {
- result.query = frag;
- __STATE(7, 1)
- } else if (buf[0] == '\0') {
- result.query = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 7: { /* fragment */
- if (buf[0] == '\0') {
- result.fragment = frag;
- __STATE(99, 0)
- }
- } break;
-
- case 99: /* end-of-string */
- goto end;
-
- }
-
- frag.len++;
-
- cur++;
-
-#undef __STATE
- }
-
-end:
- return result;
-}
diff --git a/website/utils.c b/website/utils.c
@@ -1,33 +0,0 @@
-#include "website.h"
-
-extern inline char *
-strnstr(char *restrict str, size_t len, char const *restrict needle);
-
-extern inline void
-arena_reset(struct arena *arena);
-
-extern inline void *
-arena_alloc(struct arena *arena, uint64_t size, uint64_t align);
-
-extern inline void
-list_push_head(struct list *restrict list, struct list_node *restrict node);
-
-extern inline void
-list_push_tail(struct list *restrict list, struct list_node *restrict node);
-
-extern inline struct list_node *
-list_pop_head(struct list *list);
-
-extern inline struct list_node *
-list_pop_tail(struct list *list);
-
-extern inline struct timespec
-get_timeout(struct timespec const *ts, uint64_t timeout_ms);
-
-extern inline uint64_t
-get_timestamp(struct timespec const *ts);
-
-extern inline void
-timespec_diff(struct timespec const *restrict lhs,
- struct timespec const *restrict rhs,
- struct timespec *restrict res);
diff --git a/website/website.h b/website/website.h
@@ -1,762 +0,0 @@
-#ifndef WEBSITE_H
-#define WEBSITE_H
-
-#include <assert.h>
-#include <ctype.h>
-#include <inttypes.h>
-#include <limits.h>
-#include <stdalign.h>
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <signal.h>
-
-#include <getopt.h>
-
-#include <fcntl.h>
-#include <sys/stat.h>
-
-#include <sys/socket.h>
-#include <netdb.h>
-
-#include <sys/mman.h>
-
-#include <liburing.h>
-
-/* configuration
- * ===========================================================================
- */
-
-#define SERVER_LISTEN_BACKLOG 16
-
-#define CONNECTION_POOL_CAPACITY 1024
-
-#define CONNECTION_CONCURRENT_OPS 2
-#define CONNECTION_BUFFER_SIZE 8192
-#define CONNECTION_TIMEOUT_MS -1
-
-/* utilities
- * ===========================================================================
- */
-
-#define NANOS 1000000000ULL
-
-#define KiB 1024ULL
-#define MiB (1024ULL * KiB)
-#define GiB (1024ULL * MiB)
-#define TiB (1024ULL * GiB)
-
-#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
-
-#define MIN(a, b) ((a) < (b) ? (a) : (b))
-#define MAX(a, b) ((a) > (b) ? (a) : (b))
-
-#define TO_PARENT(child, T, member) \
- ((child) ? ((T *) ((uintptr_t) child - offsetof(T, member))) : NULL)
-
-struct str {
- char *ptr;
- size_t len;
-};
-
-inline char *
-strnstr(char *restrict src, size_t len, char const *restrict marker)
-{
- char const *end = src + len;
-
- while (src < end) {
- char *ptr = src;
- char const *cur = marker;
- while (ptr < end && *cur && *ptr++ == *cur)
- cur++;
-
- if (*cur == '\0')
- return src;
-
- if (ptr == end)
- goto end;
-
- src = ptr;
- }
-
-end:
- return NULL;
-}
-
-#define ALIGN_PREV(v, align) ((v) & ~((align) - 1))
-#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + ((align) - 1), (align))
-
-struct arena {
- void *ptr;
- uint64_t cap, len;
-};
-
-inline void
-arena_reset(struct arena *arena)
-{
- arena->len = 0;
-}
-
-inline void *
-arena_alloc(struct arena *arena, uint64_t size, uint64_t align)
-{
- uint64_t aligned_len = ALIGN_NEXT(arena->len, align);
- if (aligned_len + size > arena->cap)
- return NULL;
-
- void *ptr = (void *) ((uintptr_t) arena->ptr + aligned_len);
- arena->len = aligned_len + size;
-
- return ptr;
-}
-
-#define ALLOC_ARRAY(arena, T, n) \
- arena_alloc((arena), sizeof(T) * (n), alignof(T))
-
-#define ALLOC_SIZED(arena, T) ALLOC_ARRAY((arena), T, 1)
-
-struct list_node {
- struct list_node *prev, *next;
-};
-
-#define FROM_NODE(node, T, member) TO_PARENT((node), T, member)
-
-struct list {
- struct list_node *head, *tail;
-};
-
-inline void
-list_push_head(struct list *restrict list, struct list_node *restrict node)
-{
- if (!list->tail)
- list->tail = node;
-
- if (list->head)
- list->head->prev = node;
-
- node->next = list->head;
- list->head = node;
-}
-
-inline void
-list_push_tail(struct list *restrict list, struct list_node *restrict node)
-{
- if (!list->head)
- list->head = node;
-
- if (list->tail)
- list->tail->next = node;
-
- node->prev = list->tail;
- list->tail = node;
-}
-
-inline struct list_node *
-list_pop_head(struct list *list)
-{
- struct list_node *node = list->head;
-
- if (node)
- list->head = list->head->next;
-
- return node;
-}
-
-inline struct list_node *
-list_pop_tail(struct list *list)
-{
- struct list_node *node = list->tail;
-
- if (node)
- list->tail = list->tail->prev;
-
- return node;
-}
-
-inline struct timespec
-get_timeout(struct timespec const *ts, uint64_t timeout_ms)
-{
- return (struct timespec) {
- .tv_sec = ts->tv_sec + (timeout_ms / 1000),
- .tv_nsec = ts->tv_nsec + ((timeout_ms % 1000) * (NANOS / 1000)),
- };
-}
-
-inline uint64_t
-get_timestamp(struct timespec const *ts)
-{
- return (NANOS * ts->tv_sec) + ts->tv_nsec;
-}
-
-inline void
-timespec_diff(struct timespec const *restrict lhs,
- struct timespec const *restrict rhs,
- struct timespec *restrict res)
-{
- res->tv_sec = lhs->tv_sec - rhs->tv_sec;
- res->tv_nsec = lhs->tv_nsec - rhs->tv_nsec;
-
- if (res->tv_nsec < 0) {
- res->tv_sec -= 1;
- res->tv_nsec += NANOS;
- }
-}
-
-/* network interaction
- * ===========================================================================
- */
-
-#define IOREQ_MAX_IOVS 8
-
-struct ioreq {
- enum {
- IOREQ_ACCEPT,
- IOREQ_RECV,
- IOREQ_SEND,
- IOREQ_READ,
- IOREQ_CLOSE,
- } type;
-
- int fd;
-
- union {
- struct {
- struct sockaddr *addr;
- socklen_t *addrlen;
- int flags;
- } accept;
-
- struct {
- void *buf;
- size_t len;
- int flags;
- } recv, send;
-
- struct {
- void *buf;
- size_t len;
- uint64_t off;
- } read;
- };
-};
-
-int
-ioring_init(struct io_uring *ioring);
-
-void
-enqueue_ioreq(struct io_uring *ioring, struct ioreq *ioreq);
-
-inline void
-enqueue_accept(struct io_uring *ioring, struct ioreq *req, int fd,
- struct sockaddr *addr, socklen_t *addrlen, int flags)
-{
- req->type = IOREQ_ACCEPT;
- req->fd = fd;
- req->accept.addr = addr;
- req->accept.addrlen = addrlen;
- req->accept.flags = flags;
-
- enqueue_ioreq(ioring, req);
-}
-
-inline void
-enqueue_recv(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, int flags)
-{
- req->type = IOREQ_RECV;
- req->fd = fd;
- req->recv.buf = buf;
- req->recv.len = len;
- req->recv.flags = flags;
-
- enqueue_ioreq(ioring, req);
-}
-
-inline void
-enqueue_send(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, int flags)
-{
- req->type = IOREQ_SEND;
- req->fd = fd;
- req->send.buf = buf;
- req->send.len = len;
- req->send.flags = flags;
-
- enqueue_ioreq(ioring, req);
-}
-
-inline void
-enqueue_read(struct io_uring *ioring, struct ioreq *req, int fd,
- void *buf, size_t len, uint64_t off)
-{
- req->type = IOREQ_READ;
- req->fd = fd;
- req->read.buf = buf;
- req->read.len = len;
- req->read.off = off;
-
- enqueue_ioreq(ioring, req);
-}
-
-inline void
-enqueue_close(struct io_uring *ioring, struct ioreq *req, int fd)
-{
- req->type = IOREQ_CLOSE;
- req->fd = fd;
-
- enqueue_ioreq(ioring, req);
-}
-
-void
-submit_ioreqs(struct io_uring *ioring);
-
-int
-create_server_socket(char const *restrict host, char const *restrict port);
-
-/* uri parsing and normalisation
- * ===========================================================================
- */
-
-struct urifrag {
- char *ptr;
- int len;
-};
-
-bool
-urifrag_try_normalise(struct urifrag *frag);
-
-void
-urifrag_remove_dot_segments(struct urifrag *frag);
-
-struct uri {
- struct urifrag scheme;
- struct urifrag user;
- struct urifrag host;
- struct urifrag port;
- struct urifrag path;
- struct urifrag query;
- struct urifrag fragment;
-};
-
-struct uri
-uri_parse(char *buf, size_t len);
-
-/* http connection state
- * ===========================================================================
- */
-
-#define HTTP_SEP "\r\n"
-#define HTTP_TERM HTTP_SEP HTTP_SEP
-
-enum http_method {
- UNKNOWN_METHOD,
- HTTP_HEAD,
- HTTP_GET,
-};
-
-inline enum http_method
-method_from_str(char *method)
-{
- if (strcmp(method, "HEAD") == 0)
- return HTTP_HEAD;
-
- if (strcmp(method, "GET") == 0)
- return HTTP_GET;
-
- return UNKNOWN_METHOD;
-}
-
-inline char const *
-method_str(enum http_method method)
-{
- switch (method) {
- case HTTP_HEAD: return "HEAD";
- case HTTP_GET: return "GET";
- default: return NULL;
- }
-}
-
-enum http_version {
- HTTP_09,
- HTTP_10,
- HTTP_11,
- HTTP_20,
- HTTP_30,
-};
-
-inline enum http_version
-version_from_str(char *version)
-{
- if (strcmp(version, "HTTP/0.9") == 0)
- return HTTP_09;
-
- if (strcmp(version, "HTTP/1.0") == 0)
- return HTTP_10;
-
- if (strcmp(version, "HTTP/1.1") == 0)
- return HTTP_11;
-
- if (strcmp(version, "HTTP/2") == 0)
- return HTTP_20;
-
- if (strcmp(version, "HTTP/3") == 0)
- return HTTP_30;
-
- return HTTP_09;
-}
-
-inline char const *
-version_str(enum http_version version)
-{
- switch (version) {
- case HTTP_09: return "HTTP/0.9";
- case HTTP_10: return "HTTP/1.0";
- case HTTP_11: return "HTTP/1.1";
- case HTTP_20: return "HTTP/2";
- case HTTP_30: return "HTTP/3";
- default: return NULL;
- }
-}
-
-enum http_status {
- HTTP_200 = 200, /* OK */
- HTTP_204 = 204, /* No Content */
- HTTP_206 = 206, /* Partial Content */
-
- HTTP_400 = 400, /* Bad Request */
- HTTP_401 = 401, /* Unauthorized */
- HTTP_403 = 403, /* Forbidden */
- HTTP_404 = 404, /* Not Found */
- HTTP_405 = 405, /* Method Not Allowed */
- HTTP_406 = 406, /* Not Acceptable */
- HTTP_408 = 408, /* Request Timeout */
- HTTP_411 = 411, /* Length Required */
- HTTP_413 = 413, /* Payload Too Large */
- HTTP_415 = 415, /* Unsupported Media Type */
- HTTP_416 = 416, /* Range Not Satisfiable */
- HTTP_422 = 422, /* Unprocessable Content */
- HTTP_431 = 431, /* Request Header Fields Too Large */
-
- HTTP_500 = 500, /* Internal Server Error */
- HTTP_501 = 501, /* Not Implemented */
- HTTP_503 = 503, /* Service Unavailable */
- HTTP_505 = 505, /* HTTP Version Not Supported */
-};
-
-inline char const *
-status_str(enum http_status status)
-{
- switch (status) {
- case HTTP_200: return "OK";
- case HTTP_204: return "No Content";
- case HTTP_206: return "Partial Content";
-
- case HTTP_400: return "Bad Request";
- case HTTP_401: return "Unauthorized";
- case HTTP_403: return "Forbidden";
- case HTTP_404: return "Not Found";
- case HTTP_405: return "Method Not Allowed";
- case HTTP_406: return "Not Acceptable";
- case HTTP_408: return "Request Timeout";
- case HTTP_411: return "Length Required";
- case HTTP_413: return "Payload Too Large";
- case HTTP_415: return "Unsupported Media Type";
- case HTTP_416: return "Range Not Satisfiable";
- case HTTP_422: return "Unprocessable Content";
- case HTTP_431: return "Request Header Fields Too Large";
-
- case HTTP_500: return "Internal Server Error";
- case HTTP_501: return "Not Implemented";
- case HTTP_503: return "Service Unavailable";
- case HTTP_505: return "HTTP Version Not Supported";
-
- default: return NULL;
- }
-}
-
-enum content_encoding {
- ENCODING_NONE,
- ENCODING_GZIP,
-};
-
-inline char const *
-content_encoding_str(enum content_encoding encoding)
-{
- switch (encoding) {
- case ENCODING_GZIP: return "gzip";
- default: return NULL;
- }
-}
-
-inline enum content_encoding
-content_encoding_from_list(char *list)
-{
- enum content_encoding best_encoding = ENCODING_NONE;
-
- char *enc, *saveptr;
- for (enc = strtok_r(list, ", ", &saveptr);
- enc;
- enc = strtok_r(NULL, ", ", &saveptr)) {
- if (strcmp(enc, "gzip") == 0)
- best_encoding = MAX(best_encoding, ENCODING_GZIP);
- }
-
- return best_encoding;
-}
-
-enum content_type {
- APPLICATION_OCTET_STREAM,
-
- IMAGE_BMP,
- IMAGE_GIF,
- IMAGE_JPEG,
- IMAGE_PNG,
- IMAGE_SVG,
-
- TEXT_PLAIN,
- TEXT_HTML,
- TEXT_CSS,
- TEXT_JAVASCRIPT,
-
- ANY_CONTENT_TYPE,
-};
-
-inline char const *
-content_type_str(enum content_type type)
-{
- switch (type) {
- case APPLICATION_OCTET_STREAM: return "application/octet-stream";
-
- case IMAGE_BMP: return "image/bmp";
- case IMAGE_GIF: return "image/gif";
- case IMAGE_JPEG: return "image/jpeg";
- case IMAGE_PNG: return "image/png";
- case IMAGE_SVG: return "image/svg+xml";
-
- case TEXT_PLAIN: return "text/plain";
- case TEXT_HTML: return "text/html";
- case TEXT_CSS: return "text/css";
- case TEXT_JAVASCRIPT: return "text/javascript";
-
- default: return NULL;
- }
-}
-
-inline bool
-content_type_is_text(enum content_type type)
-{
- switch (type) {
- case IMAGE_SVG:
- case TEXT_PLAIN:
- case TEXT_HTML:
- case TEXT_CSS:
- case TEXT_JAVASCRIPT:
- return true;
-
- default:
- return false;
- }
-}
-
-inline enum content_type
-content_type_from_mime(char *mime)
-{
- if (strcmp(mime, "*/*") == 0)
- return ANY_CONTENT_TYPE;
-
- if (strcmp(mime, "image/bmp") == 0)
- return IMAGE_BMP;
-
- if (strcmp(mime, "image/gif") == 0)
- return IMAGE_GIF;
-
- if (strcmp(mime, "image/jpeg") == 0)
- return IMAGE_JPEG;
-
- if (strcmp(mime, "image/png") == 0)
- return IMAGE_PNG;
-
- if (strcmp(mime, "image/svg+xml") == 0)
- return IMAGE_SVG;
-
- if (strcmp(mime, "text/plain") == 0)
- return TEXT_PLAIN;
-
- if (strcmp(mime, "text/html") == 0)
- return TEXT_HTML;
-
- if (strcmp(mime, "text/css") == 0)
- return TEXT_CSS;
-
- if (strcmp(mime, "text/javascript") == 0)
- return TEXT_JAVASCRIPT;
-
- return APPLICATION_OCTET_STREAM;
-}
-
-inline enum content_type
-content_type_from_ext(char *ext, size_t len)
-{
- if (!ext)
- return APPLICATION_OCTET_STREAM;
-
- if (len == strlen(".bmp") && strncmp(ext, ".bmp", len) == 0)
- return IMAGE_BMP;
-
- if (len == strlen(".gif") && strncmp(ext, ".gif", len) == 0)
- return IMAGE_GIF;
-
- if ((len == strlen(".jpg") && strncmp(ext, ".jpg", len) == 0) ||
- (len == strlen(".jpeg") && strncmp(ext, ".jpeg", len) == 0))
- return IMAGE_JPEG;
-
- if (len == strlen(".png") && strncmp(ext, ".png", len) == 0)
- return IMAGE_PNG;
-
- if (len == strlen(".svg") && strncmp(ext, ".svg", len) == 0)
- return IMAGE_SVG;
-
- if (len == strlen(".txt") && strncmp(ext, ".txt", len) == 0)
- return TEXT_PLAIN;
-
- if (len == strlen(".html") && strncmp(ext, ".html", len) == 0)
- return TEXT_HTML;
-
- if (len == strlen(".css") && strncmp(ext, ".css", len) == 0)
- return TEXT_CSS;
-
- if (len == strlen(".js") && strncmp(ext, ".js", len) == 0)
- return TEXT_JAVASCRIPT;
-
- return APPLICATION_OCTET_STREAM;
-}
-
-struct http_request {
- enum http_method method;
- enum http_version version;
- struct uri uri;
-};
-
-struct http_headers {
- int keep_alive;
- char *accept;
- enum content_encoding encoding;
- bool have_range;
- struct { uint64_t begin, end; } range;
- enum content_type content_type;
- uint64_t content_length;
-};
-
-void
-parse_http_request(char *src, struct http_request *request, struct http_headers *headers);
-
-struct http_response {
- int fd;
- size_t len;
- uint64_t off;
-};
-
-struct connection {
- int socket;
-
- struct ioreq ioreq;
-
- char buf[CONNECTION_BUFFER_SIZE];
- size_t cap, len, cur;
-
- struct http_request request;
- struct http_headers request_headers;
- struct http_response response;
- struct http_headers response_headers;
-
- struct timespec timeout;
-
- struct list_node list_node;
-};
-
-void
-handle_accept(struct io_uring *ioring, struct connection *conn, int sock);
-
-void
-handle_request_chunk(struct io_uring *ioring, struct connection *conn, int webroot, int res);
-
-void
-handle_request(struct io_uring *ioring, struct connection *conn, int webroot);
-
-void
-handle_response_chunk(struct io_uring *ioring, struct connection *conn, int res);
-
-void
-handle_fileread_chunk(struct io_uring *ioring, struct connection *conn, int res);
-
-void
-error_connection(struct io_uring *ioring, struct connection *conn,
- enum http_status err, bool linger);
-
-void
-close_connection(struct io_uring *ioring, struct connection *conn);
-
-struct connection_pool {
- struct connection *ptr;
- struct list freelist;
-};
-
-inline int
-connection_pool_init(struct connection_pool *pool, size_t capacity)
-{
- size_t cap = capacity * sizeof *pool->ptr;
- int prot = PROT_READ | PROT_WRITE;
- int flags = MAP_PRIVATE | MAP_ANONYMOUS;
-
- if ((pool->ptr = mmap(NULL, cap, prot, flags, -1, 0)) == MAP_FAILED) {
- fprintf(stderr, "Failed to map memory for connection pool\n");
- return -1;
- }
-
- madvise(pool->ptr, cap, MADV_HUGEPAGE);
-
- pool->freelist.head = pool->freelist.tail = NULL;
- for (size_t i = 0; i < capacity; i++)
- list_push_tail(&pool->freelist, &pool->ptr[i].list_node);
-
- return 0;
-}
-
-inline struct connection *
-connection_pool_alloc(struct connection_pool *pool)
-{
- struct list_node *node = list_pop_head(&pool->freelist);
- return FROM_NODE(node, struct connection, list_node);
-}
-
-inline void
-connection_pool_free(struct connection_pool *restrict pool,
- struct connection *restrict conn)
-{
- list_push_tail(&pool->freelist, &conn->list_node);
-}
-
-/* server application
- * ===========================================================================
- */
-
-struct opts {
- int verbose;
- char *host, *port;
- char *webroot;
-};
-
-extern struct opts opts;
-
-extern sig_atomic_t quit;
-extern sig_atomic_t reload_tree;
-
-int
-serve(struct io_uring *ioring, int webroot, int sock, struct connection_pool *pool);
-
-#endif /* WEBSITE_H */