serve

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

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++-
Mbuild.sh | 12+++++-------
Mdebug.sh | 2+-
Rsslterm/main.c -> proxy/main.c | 0
Aserve/connection.c | 433+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve/main.c | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve/net.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve/serve.h | 764+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve/uri.c | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve/utils.c | 33+++++++++++++++++++++++++++++++++
Dwebsite/connection.c | 433-------------------------------------------------------------------------------
Dwebsite/main.c | 294-------------------------------------------------------------------------------
Dwebsite/net.c | 128-------------------------------------------------------------------------------
Dwebsite/uri.c | 229-------------------------------------------------------------------------------
Dwebsite/utils.c | 33---------------------------------
Dwebsite/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 */