commit 22845e5f6316831f4b72a6ec8fcd7cf3a5f4713e
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Sat, 11 Jan 2025 23:21:50 +0000
Initial commit
Diffstat:
14 files changed, 1860 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,21 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+guidelines = 80, 120, 160
+
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
+
+[*.{html,css,js,svg}]
+indent_style = space
+indent_size = 2
+
+[*.{md,txt}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+bin/
+out/**
+!out/.keep
+
+imgui.ini
+**/.*.swp
diff --git a/build.sh b/build.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+CC="${CC:-clang}"
+
+WARNINGS="-Wall -Wextra -Wpedantic ${WERROR:+-Werror}"
+
+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/sslterm sslterm/main.c \
+# $WARNINGS $CLFAGS $CPPFLAGS $LDFLAGS $(pkgconf --cflags --libs liburing openssl)
diff --git a/clean.sh b/clean.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+rm -rf bin
diff --git a/debug.sh b/debug.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+lldbgui -- bin/website -v out/
diff --git a/out/.keep b/out/.keep
diff --git a/sslterm/main.c b/sslterm/main.c
@@ -0,0 +1,5 @@
+int
+main(int argc, char **argv)
+{
+ return 0;
+}
diff --git a/website/connection.c b/website/connection.c
@@ -0,0 +1,429 @@
+#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++;
+
+ printf("%s: %s\n", key, 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);
+}
+
+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);
+ }
+
+ 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) {
+ 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
@@ -0,0 +1,154 @@
+#include "website.h"
+
+struct opts opts = {
+ .verbose = 0,
+ .host = "localhost",
+ .port = "8080",
+};
+
+#define OPTSTR "hv"
+
+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
+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"
+#include "server.c"
diff --git a/website/net.c b/website/net.c
@@ -0,0 +1,128 @@
+#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/server.c b/website/server.c
@@ -0,0 +1,135 @@
+#include "website.h"
+
+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;
+ }
+
+ 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) {
+ fprintf(stderr, "recv(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+ 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) {
+ fprintf(stderr, "send(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+ 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) {
+ fprintf(stderr, "read(): %d (%s), closing connection\n", res, strerror(-res));
+ close_connection(ioring, conn);
+ break;
+ }
+
+
+ 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);
+
+ 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;
+}
diff --git a/website/uri.c b/website/uri.c
@@ -0,0 +1,229 @@
+#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
@@ -0,0 +1,28 @@
+#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);
diff --git a/website/website.h b/website/website.h
@@ -0,0 +1,696 @@
+#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 4096
+#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;
+}
+
+/* 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";
+
+ 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;
+
+ 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(".html") && strncmp(ext, ".html", len) == 0)
+ return TEXT_HTML;
+
+ 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 */