serve

serve.git
git clone git://git.lenczewski.org/serve.git
Log | Files | Refs

commit 22845e5f6316831f4b72a6ec8fcd7cf3a5f4713e
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Sat, 11 Jan 2025 23:21:50 +0000

Initial commit

Diffstat:
A.editorconfig | 21+++++++++++++++++++++
A.gitignore | 6++++++
Abuild.sh | 19+++++++++++++++++++
Aclean.sh | 5+++++
Adebug.sh | 5+++++
Aout/.keep | 0
Asslterm/main.c | 5+++++
Awebsite/connection.c | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/main.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/net.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/server.c | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/uri.c | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/utils.c | 28++++++++++++++++++++++++++++
Awebsite/website.h | 696+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 */