commit 27fce716699f047217c1de041ccf9f822bb9c92c
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Sun, 31 Mar 2024 18:57:12 +0000
Initial commit.
Diffstat:
15 files changed, 1719 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,17 @@
+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
+
+[*.{md,txt}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+bin/
+docs/
+
+**/.*.swp
diff --git a/build.sh b/build.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+CC="${CC:-cc}"
+PKGCONFIG="${PKGCONFIG:-pkg-config}"
+
+WARNINGS="-Wall -Wextra -Wpedantic -Werror"
+
+CFLAGS="-std=c11 -Og -g"
+CPPFLAGS="-UNDEBUG"
+
+SSL="$($PKGCONFIG --cflags --libs openssl)"
+URING="$($PKGCONFIG --cflags --libs liburing)"
+
+set -ex
+
+mkdir -p bin
+
+$CC -o bin/tlsterm tlsterm/tlsterm.c $WARNINGS $CFLAGS $CPPFLAGS $SSL $URING
+$CC -o bin/httpsrv httpsrv/httpsrv.c $WARNINGS $CFLAGS $CPPFLAGS $URING
diff --git a/clean.sh b/clean.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+rm -rf bin/
diff --git a/extra/index.html b/extra/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Index</title>
+ </head>
+ <body>
+ <h1>Hello, World!</h1>
+ <p>Hello from httpsrv!</p>
+ </body>
+</html>
diff --git a/httpsrv/http.c b/httpsrv/http.c
@@ -0,0 +1,141 @@
+#include "http.h"
+
+enum http_version
+http_get_version_from_str(char const *str)
+{
+ if (!str)
+ return HTTP_UNKNOWN_VERSION;
+
+ if (strcmp(str, "HTTP/0.9") == 0)
+ return HTTP_09;
+
+ if (strcmp(str, "HTTP/1.0") == 0)
+ return HTTP_10;
+
+ if (strcmp(str, "HTTP/1.1") == 0)
+ return HTTP_11;
+
+ return HTTP_UNKNOWN_VERSION;
+}
+
+static char const *http_version_tab[] = {
+ [HTTP_09] = "HTTP/0.9",
+ [HTTP_10] = "HTTP/1.0",
+ [HTTP_11] = "HTTP/1.1",
+};
+
+char const *
+http_version_str(enum http_version version)
+{
+ if (version == HTTP_UNKNOWN_VERSION)
+ return NULL;
+
+ return http_version_tab[version];
+}
+
+enum http_method
+http_get_method_from_str(char const *str)
+{
+ if (!str)
+ return HTTP_UNKNOWN_METHOD;
+
+ if (strcmp(str, "OPTIONS") == 0)
+ return HTTP_OPTIONS;
+
+ if (strcmp(str, "GET") == 0)
+ return HTTP_GET;
+
+ if (strcmp(str, "HEAD") == 0)
+ return HTTP_HEAD;
+
+ if (strcmp(str, "POST") == 0)
+ return HTTP_POST;
+
+ if (strcmp(str, "PUT") == 0)
+ return HTTP_PUT;
+
+ if (strcmp(str, "DELETE") == 0)
+ return HTTP_DELETE;
+
+ if (strcmp(str, "TRACE") == 0)
+ return HTTP_TRACE;
+
+ if (strcmp(str, "CONNECT") == 0)
+ return HTTP_CONNECT;
+
+ return HTTP_UNKNOWN_METHOD;
+}
+
+static char const *http_method_tab[] = {
+ [HTTP_OPTIONS] = "OPTIONS",
+ [HTTP_GET] = "GET",
+ [HTTP_HEAD] = "HEAD",
+ [HTTP_POST] = "POST",
+ [HTTP_PUT] = "PUT",
+ [HTTP_DELETE] = "DELETE",
+ [HTTP_TRACE] = "TRACE",
+ [HTTP_CONNECT] = "CONNECT",
+};
+
+char const *
+http_method_str(enum http_method method)
+{
+ if (method == HTTP_UNKNOWN_METHOD)
+ return NULL;
+
+ return http_method_tab[method];
+}
+
+static char const *http_status_code_tab[] = {
+ [HTTP_STATUS_100] = "100 Continue",
+ [HTTP_STATUS_101] = "101 Switching Protocols",
+
+ [HTTP_STATUS_200] = "200 OK",
+ [HTTP_STATUS_201] = "201 Created",
+ [HTTP_STATUS_202] = "202 Accepted",
+ [HTTP_STATUS_203] = "203 Non-Authoritative Information",
+ [HTTP_STATUS_204] = "204 No Content",
+ [HTTP_STATUS_205] = "205 Reset Content",
+ [HTTP_STATUS_206] = "206 Partial Content",
+
+ [HTTP_STATUS_300] = "300 Multiple Choices",
+ [HTTP_STATUS_301] = "301 Moved Permanently",
+ [HTTP_STATUS_302] = "302 Found",
+ [HTTP_STATUS_304] = "304 Not Modified",
+ [HTTP_STATUS_305] = "305 Use Proxy",
+ [HTTP_STATUS_306] = "306 Switch Proxy",
+ [HTTP_STATUS_307] = "307 Temporary Redirect",
+ [HTTP_STATUS_308] = "308 Permanent Redirect",
+
+ [HTTP_STATUS_400] = "400 Bad Request",
+ [HTTP_STATUS_401] = "401 Unauthorized",
+ [HTTP_STATUS_402] = "402 Payment Required",
+ [HTTP_STATUS_403] = "403 Forbidden",
+ [HTTP_STATUS_404] = "404 Not Found",
+ [HTTP_STATUS_405] = "405 Method Not Allowed",
+ [HTTP_STATUS_406] = "406 Not Acceptable",
+ [HTTP_STATUS_407] = "407 Proxy Authentication Required",
+ [HTTP_STATUS_408] = "408 Request Time-out",
+ [HTTP_STATUS_409] = "409 Conflict",
+ [HTTP_STATUS_410] = "410 Gone",
+ [HTTP_STATUS_411] = "411 Length Required",
+ [HTTP_STATUS_412] = "412 Precondition Failed",
+ [HTTP_STATUS_413] = "413 Request Entity Too Large",
+ [HTTP_STATUS_414] = "414 Request-URI Too Large",
+ [HTTP_STATUS_415] = "415 Unsupported Media Type",
+ [HTTP_STATUS_416] = "416 Requested range not satisfiable",
+ [HTTP_STATUS_417] = "417 Expectation Failed",
+
+ [HTTP_STATUS_500] = "500 Internal Server Error",
+ [HTTP_STATUS_501] = "501 Not Implemented",
+ [HTTP_STATUS_502] = "502 Bad Gateway",
+ [HTTP_STATUS_503] = "503 Service Unavailable",
+ [HTTP_STATUS_504] = "504 Gateway Time-out",
+ [HTTP_STATUS_505] = "505 HTTP Version not supported",
+};
+
+char const *
+http_status_code_str(enum http_status_code status_code)
+{
+ return http_status_code_tab[status_code];
+}
diff --git a/httpsrv/http.h b/httpsrv/http.h
@@ -0,0 +1,92 @@
+#ifndef HTTP_H
+#define HTTP_H
+
+#define HTTP_SEPARATOR " "
+#define HTTP_DELIMITER "\r\n"
+
+enum http_version {
+ HTTP_09,
+ HTTP_10,
+ HTTP_11,
+
+ HTTP_UNKNOWN_VERSION,
+};
+
+extern enum http_version
+http_get_version_from_str(char const *str);
+
+extern char const *
+http_version_str(enum http_version version);
+
+enum http_method {
+ HTTP_OPTIONS,
+ HTTP_GET,
+ HTTP_HEAD,
+ HTTP_POST,
+ HTTP_PUT,
+ HTTP_DELETE,
+ HTTP_TRACE,
+ HTTP_CONNECT,
+
+ HTTP_UNKNOWN_METHOD,
+};
+
+extern enum http_method
+http_get_method_from_str(char const *str);
+
+extern char const *
+http_method_str(enum http_method method);
+
+enum http_status_code {
+ HTTP_STATUS_100, // Continue
+ HTTP_STATUS_101, // Switching Protocols
+
+ HTTP_STATUS_200, // OK
+ HTTP_STATUS_201, // Created
+ HTTP_STATUS_202, // Accepted
+ HTTP_STATUS_203, // Non-Authoritative Information
+ HTTP_STATUS_204, // No Content
+ HTTP_STATUS_205, // Reset Content
+ HTTP_STATUS_206, // Partial Content
+
+ HTTP_STATUS_300, // Multiple Choices
+ HTTP_STATUS_301, // Moved Permanently
+ HTTP_STATUS_302, // Found
+ HTTP_STATUS_303, // See Other
+ HTTP_STATUS_304, // Not Modified
+ HTTP_STATUS_305, // Use Proxy
+ HTTP_STATUS_306, // Switch Proxy
+ HTTP_STATUS_307, // Temporary Redirect
+ HTTP_STATUS_308, // Permanent Redirect
+
+ HTTP_STATUS_400, // Bad Request
+ HTTP_STATUS_401, // Unauthorized
+ HTTP_STATUS_402, // Payment Required
+ HTTP_STATUS_403, // Forbidden
+ HTTP_STATUS_404, // Not Found
+ HTTP_STATUS_405, // Method Not Allowed
+ HTTP_STATUS_406, // Not Acceptable
+ HTTP_STATUS_407, // Proxy Authentication Required
+ HTTP_STATUS_408, // Request Time-out
+ HTTP_STATUS_409, // Conflict
+ HTTP_STATUS_410, // Gone
+ HTTP_STATUS_411, // Length Required
+ HTTP_STATUS_412, // Precondition Failed
+ HTTP_STATUS_413, // Request Entity Too Large
+ HTTP_STATUS_414, // Request-URI Too Large
+ HTTP_STATUS_415, // Unsupported Media Type
+ HTTP_STATUS_416, // Requested range not satisfiable
+ HTTP_STATUS_417, // Expectation Failed
+
+ HTTP_STATUS_500, // Internal Server Error
+ HTTP_STATUS_501, // Not Implemented
+ HTTP_STATUS_502, // Bad Gateway
+ HTTP_STATUS_503, // Service Unavailable
+ HTTP_STATUS_504, // Gateway Time-out
+ HTTP_STATUS_505, // HTTP Version not supported
+};
+
+extern char const *
+http_status_code_str(enum http_status_code status_code);
+
+#endif /* HTTP_H */
diff --git a/httpsrv/httpsrv.c b/httpsrv/httpsrv.c
@@ -0,0 +1,679 @@
+#include "httpsrv.h"
+
+struct opts opts = {
+ .addr = "localhost",
+ .port = "8080",
+ .webroot = NULL,
+ .connection_backlog = 16,
+ .connection_cap = 1024,
+ .connection_arena_sz = 8 * KiB,
+};
+
+static char const *optstr = "ha:p:b:c:m:";
+
+#define POSITIONAL_ARGS 1
+
+static void
+usage(int argc, char **argv)
+{
+ (void) argc;
+
+ fprintf(stderr, "Usage: %s [-h] [-a <addr>] [-p <port>] [-b <conn-backlog>] [-c <conn-cap>] [-m <conn-arena-kib>] <webroot>\n", argv[0]);
+ fprintf(stderr, "\t-h :\n\t\tdisplays this help message\n");
+ fprintf(stderr, "\t-a :\n\t\tthe address the server socket should listen on (default: localhost)\n");
+ fprintf(stderr, "\t-p :\n\t\tthe port the server socket should listen on (default: 8080)\n");
+ fprintf(stderr, "\t-b :\n\t\tthe maximum number of pending connections in the server socket accept backlog (default: 16)\n");
+ fprintf(stderr, "\t-c :\n\t\tthe maximum number of concurrent connections (default: 1024)\n");
+ fprintf(stderr, "\t-m :\n\t\tthe size of each connection's network buffer in KiB (default: 8 KiB)\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\t <webroot> :\n\t\tthe directory out of which to serve content (required)\n");
+}
+
+static bool
+try_parse_opts(int argc, char **argv)
+{
+ int opt;
+ while ((opt = getopt(argc, argv, optstr)) != -1) {
+ switch (opt) {
+ case 'a':
+ opts.addr = optarg;
+ break;
+
+ case 'p':
+ opts.port = optarg;
+ break;
+
+ case 'b':
+ if ((opts.connection_backlog = strtoull(optarg, NULL, 0)) == 0)
+ return false;
+ break;
+
+ case 'c':
+ if ((opts.connection_cap = strtoull(optarg, NULL, 0)) == 0)
+ return false;
+ break;
+
+ case 'm':
+ if ((opts.connection_arena_sz = strtoull(optarg, NULL, 0) * KiB) == 0)
+ return false;
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ if (argc < optind + POSITIONAL_ARGS)
+ return false;
+
+ opts.webroot = argv[optind++];
+
+ assert(optind == argc);
+
+ return true;
+}
+
+static void
+close_handler(int sig, siginfo_t *info, void *uctx)
+{
+ (void) info;
+ (void) uctx;
+
+ fprintf(stderr, "Received signal %d. Closing\n", sig);
+
+ exit(EXIT_SUCCESS);
+}
+
+static int
+bind_server_socket(struct opts const *opts);
+
+static int
+open_webroot(struct opts const *opts);
+
+static struct io_uring uring;
+
+int
+main(int argc, char **argv)
+{
+ if (!try_parse_opts(argc, argv)) {
+ usage(argc, argv);
+ exit(EXIT_FAILURE);
+ }
+
+ struct sigaction onclose = {
+ .sa_sigaction = close_handler, .sa_flags = SA_SIGINFO,
+ };
+
+ sigaction(SIGINT, &onclose, NULL);
+ sigaction(SIGKILL, &onclose, NULL);
+
+ /* NOTE: we need to be able to send 1 accept() multishot, as well as
+ * up to 1 operation per connection simultaneously
+ */
+ unsigned uring_depth = 1 + opts.connection_cap, uring_flags = 0;
+ if (io_uring_queue_init(uring_depth, &uring, uring_flags) < 0) {
+ fprintf(stderr, "Failed to initialise io_uring\n");
+ exit(EXIT_FAILURE);
+ }
+
+ int servfd;
+ if ((servfd = bind_server_socket(&opts)) == -1) {
+ fprintf(stderr, "Failed to bind server socket to %s:%s\n",
+ opts.addr, opts.port);
+ exit(EXIT_FAILURE);
+ }
+
+ int webrootfd;
+ if ((webrootfd = open_webroot(&opts)) == -1) {
+ fprintf(stderr, "Failed to open webroot %s\n", opts.webroot);
+ exit(EXIT_FAILURE);
+ }
+
+ struct connection_pool connection_pool;
+ if (!connection_pool_init(&connection_pool, &opts)) {
+ fprintf(stderr, "Failed to initialise connection pool\n");
+ exit(EXIT_FAILURE);
+ }
+
+ exit(httpsrv_run(&opts, servfd, webrootfd, &connection_pool));
+}
+
+static int
+bind_server_socket(struct opts const *opts)
+{
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ .ai_flags = AI_NUMERICSERV,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo(opts->addr, opts->port, &hints, &addrinfo))) {
+ fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(res));
+ return -1;
+ }
+
+ int sock;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ if ((sock = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol)) == -1)
+ continue;
+
+ if (bind(sock, ptr->ai_addr, ptr->ai_addrlen) == -1) {
+ close(sock);
+ continue;
+ }
+
+ if (listen(sock, opts->connection_backlog) == -1) {
+ close(sock);
+ continue;
+ }
+
+ break;
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr)
+ return -1;
+
+ return sock;
+}
+
+static int
+open_webroot(struct opts const *opts)
+{
+ int fd;
+ if ((fd = open(opts->webroot, O_PATH | O_DIRECTORY | O_RDONLY)) == -1)
+ return -1;
+
+ return fd;
+}
+
+bool
+submit_io_requests(struct io_request *buf, size_t len)
+{
+ for (size_t i = 0; i < len; i++) {
+ struct io_request *req = &buf[i];
+
+ struct io_uring_sqe *sqe = io_uring_get_sqe(&uring);
+ if (!sqe) return false;
+
+ switch (req->type) {
+ case IO_ACCEPT:
+ io_uring_prep_accept(sqe, req->accept.fd, req->accept.addr, req->accept.addrlen, 0);
+ break;
+
+ case IO_RECV:
+ io_uring_prep_recv(sqe, req->recv.fd, req->recv.buf, req->recv.len, 0);
+ break;
+
+ case IO_SEND:
+ io_uring_prep_send(sqe, req->send.fd, req->send.buf, req->send.len, 0);
+ break;
+
+ case IO_READ:
+ io_uring_prep_read(sqe, req->read.fd, req->read.buf, req->read.len, req->read.off);
+ break;
+
+ case IO_WRITEV:
+ io_uring_prep_writev(sqe, req->writev.fd, req->writev.iovs, req->writev.len, 0);
+ break;
+
+ case IO_CLOSE:
+ io_uring_prep_close(sqe, req->close.fd);
+ break;
+ }
+
+ io_uring_sqe_set_data(sqe, req);
+
+ if (i < len - 1)
+ sqe->flags = IOSQE_IO_LINK;
+ else
+ sqe->flags = 0;
+ }
+
+ io_uring_submit(&uring);
+
+ return true;
+}
+
+bool
+connection_pool_init(struct connection_pool *pool, struct opts const *opts)
+{
+ pool->cap = opts->connection_cap;
+ if (!(pool->ptr = malloc(pool->cap * sizeof *pool->ptr)))
+ return false;
+
+ pool->arena.cap = opts->connection_cap * opts->connection_arena_sz;
+ if (!(pool->arena.ptr = malloc(pool->arena.cap)))
+ return false;
+
+ arena_reset(&pool->arena);
+
+ pool->freelist.head = pool->freelist.tail = NULL;
+
+ for (size_t i = 0; i < pool->cap; i++) {
+ struct connection *conn = &pool->ptr[i];
+
+ conn->arena.cap = opts->connection_arena_sz;
+ conn->arena.ptr = PUSH_ARRAY(&pool->arena, u8, conn->arena.cap);
+ arena_reset(&conn->arena);
+
+ connection_pool_free(pool, conn);
+ }
+
+ assert(pool->arena.len == pool->arena.cap);
+
+ return true;
+}
+
+int
+httpsrv_run(struct opts const *opts, int servfd, int webrootfd, struct connection_pool *pool)
+{
+ (void) opts;
+
+ struct sockaddr_storage accept_addr;
+ socklen_t accept_addrlen = sizeof accept_addr;
+
+ struct io_request accept_req = {
+ .type = IO_ACCEPT,
+ .accept.fd = servfd,
+ .accept.addr = (struct sockaddr *) &accept_addr,
+ .accept.addrlen = &accept_addrlen,
+ };
+
+ submit_io_requests(&accept_req, 1);
+
+ int res;
+ while (true) {
+ struct io_uring_cqe *cqe;
+ if ((res = io_uring_wait_cqe(&uring, &cqe)) < 0)
+ continue;
+
+ struct io_request *req = io_uring_cqe_get_data(cqe);
+ switch (req->type) {
+ case IO_ACCEPT: { /* accepted new client */
+ int clientfd = cqe->res;
+ if (clientfd == -1) goto next_accept;
+
+ struct connection *conn = connection_pool_alloc(pool);
+ if (!conn) { /* overloaded */
+ fprintf(stderr, "Overloaded, dropping client %d\n", clientfd);
+
+ /* TODO: send HTTP_STATUS_503 Service Unavailable */
+ close(clientfd);
+ goto next_accept;
+ }
+
+ fprintf(stderr, "[client %d] opened connection\n", conn->sockfd);
+
+ connection_init(conn, clientfd, webrootfd);
+ connection_start_recv(conn);
+
+next_accept:
+ submit_io_requests(req, 1); /* resend accept() */
+ } break;
+
+ case IO_RECV: { /* received http request chunk */
+ struct connection *conn = req->recv.conn;
+ assert(conn);
+
+ int bytes = cqe->res;
+ if (bytes <= 0) {
+ connection_close(conn);
+ goto next_cqe;
+ }
+
+ fprintf(stderr, "[client %d] Received %d bytes\n", conn->sockfd, bytes);
+
+ connection_on_recv_chunk(conn, bytes);
+ } break;
+
+ case IO_SEND: { /* sent http response chunk */
+ struct connection *conn = req->send.conn;
+ assert(conn);
+
+ int bytes = cqe->res;
+ if (bytes <= 0) {
+ connection_close(conn);
+ goto next_cqe;
+ }
+
+ fprintf(stderr, "[client %d] Sent %d bytes\n", conn->sockfd, bytes);
+
+ connection_on_send_chunk(conn, bytes);
+ } break;
+
+ case IO_READ: { /* read file chunk */
+ struct connection *conn = req->read.conn;
+ assert(conn);
+
+ int bytes = cqe->res;
+ if (bytes <= 0) {
+ /* TODO: this should not be a fatal error,
+ * instead we could simply return a
+ * HTTP_STATUS_500 and continue
+ */
+ connection_close(conn);
+ goto next_cqe;
+ }
+
+ fprintf(stderr, "[client %d] Read %d bytes\n", conn->sockfd, bytes);
+
+ connection_on_read_chunk(conn, bytes);
+ } break;
+
+ case IO_WRITEV: { /* sent response to raw client socket */
+ /* TODO: implement me */
+ } break;
+
+ case IO_CLOSE: { /* closed client connection */
+ struct connection *conn = req->close.conn;
+ assert(conn);
+
+ fprintf(stderr, "[client %d] closed connection\n", conn->sockfd);
+
+ connection_pool_free(pool, conn);
+ } break;
+ }
+
+next_cqe:
+ io_uring_cqe_seen(&uring, cqe);
+ }
+
+ return EXIT_SUCCESS;
+}
+
+void
+connection_init(struct connection *conn, int sockfd, int webrootfd)
+{
+ conn->sockfd = sockfd;
+ conn->webrootfd = webrootfd;
+
+ conn->http_version = HTTP_11;
+ conn->linger = true;
+
+ memset(&conn->response, 0, sizeof conn->response);
+ conn->response.body.fd = -1;
+
+ arena_reset(&conn->arena);
+}
+
+void
+connection_start_recv(struct connection *conn)
+{
+ conn->io_request.type = IO_RECV;
+ conn->io_request.recv.fd = conn->sockfd;
+ conn->io_request.recv.buf = conn->arena.ptr;
+ conn->io_request.recv.len = conn->arena.cap;
+ conn->io_request.recv.conn = conn;
+
+ submit_io_requests(&conn->io_request, 1);
+}
+
+void
+connection_on_recv_chunk(struct connection *conn, int bytes)
+{
+ enum http_status_code error_code;
+
+ /* since we don't allocate extra memory once connections fill their
+ * memory arena, we cannot distinguish between the case of an exactly
+ * full buffer, and a buffer with extra data to read. so we error out
+ */
+ if (conn->arena.len + bytes == conn->arena.cap) {
+ error_code = HTTP_STATUS_400;
+ goto error;
+ }
+
+ /* TODO: we assume that the complete http request is sent in the first
+ * received chunk, and error out otherwise. this precludes slow-sending
+ * clients
+ */
+ char *request = conn->arena.ptr;
+ request[bytes] = '\0';
+
+ if (strstr(request, HTTP_DELIMITER HTTP_DELIMITER) == NULL) {
+ error_code = HTTP_STATUS_400;
+ goto error;
+ }
+
+ /* http request-line: METHOD ' ' URI ' ' VERSION '\r\n' */
+ char *saveptr;
+ char *rawmethod = strtok_r(request, HTTP_SEPARATOR, &saveptr);
+ char *rawuri = strtok_r(NULL, HTTP_SEPARATOR, &saveptr);
+ char *rawversion = strtok_r(NULL, HTTP_DELIMITER, &saveptr);
+
+ enum http_version version = http_get_version_from_str(rawversion);
+
+ if (version != HTTP_11) {
+ error_code = HTTP_STATUS_505;
+ goto error;
+ }
+
+ enum http_method method = http_get_method_from_str(rawmethod);
+ struct uri uri = uri_parse(rawuri, strlen(rawuri));
+
+ fprintf(stderr, "[client %d] Request: %s %.*s\n", conn->sockfd,
+ http_method_str(method), uri.path.len, uri.path.ptr);
+
+#if 0
+ fprintf(stderr, "parsed uri:\n");
+ fprintf(stderr, "scheme: len: %d,\tval: %.*s\n", uri.scheme.len, uri.scheme.len, uri.scheme.ptr);
+ fprintf(stderr, "user: len: %d,\tval: %.*s\n", uri.user.len, uri.user.len, uri.user.ptr);
+ fprintf(stderr, "host: len: %d,\tval: %.*s\n", uri.host.len, uri.host.len, uri.host.ptr);
+ fprintf(stderr, "port: len: %d,\tval: %.*s\n", uri.port.len, uri.port.len, uri.port.ptr);
+ fprintf(stderr, "path: len: %d,\tval: %.*s\n", uri.path.len, uri.path.len, uri.path.ptr);
+ fprintf(stderr, "query: len: %d,\tval: %.*s\n", uri.query.len, uri.query.len, uri.query.ptr);
+ fprintf(stderr, "fragment: len: %d,\tval: %.*s\n", uri.fragment.len, uri.fragment.len, uri.fragment.ptr);
+#endif
+
+ /* TODO: handle request headers? e.g. for range requests and for connection linger state */
+
+ connection_handle_request(conn, method, uri);
+ return;
+
+error:
+ prepare_error(&conn->arena, conn->http_version, error_code, conn->linger);
+ connection_start_send_chunk(conn);
+}
+
+void
+connection_handle_request(struct connection *conn, enum http_method method, struct uri uri)
+{
+ enum http_status_code error_code;
+
+ if (method != HTTP_GET) {
+ error_code = HTTP_STATUS_501;
+ goto error;
+ }
+
+ if (uri.path.len == 0 || !urifrag_try_normalise(&uri.path)) {
+ error_code = HTTP_STATUS_400;
+ goto error;
+ }
+
+ urifrag_remove_dot_segments(&uri.path);
+
+ char pathbuf[PATH_MAX];
+ if (ARRLEN(pathbuf) <= (size_t) uri.path.len) {
+ error_code = HTTP_STATUS_414;
+ goto error;
+ }
+
+ char *path = strncpy(pathbuf, uri.path.ptr, ARRLEN(pathbuf));
+ pathbuf[uri.path.len] = '\0';
+
+ if (strcmp(path, "/") == 0) {
+ path = "index.html";
+ } else if (path[0] == '/') {
+ path++;
+ }
+
+ int fd = openat(conn->webrootfd, path, O_RDONLY);
+ if (fd == -1) {
+ int err = errno;
+ fprintf(stderr, "[client %d] openat(): %s: errno %d, %s\n",
+ conn->sockfd, path, err, strerror(err));
+
+ if (err == ENOENT)
+ error_code = HTTP_STATUS_404;
+ else if (err == EACCES)
+ error_code = HTTP_STATUS_403;
+ else
+ error_code = HTTP_STATUS_500;
+
+ goto error;
+ }
+
+ struct stat statbuf;
+ if (fstat(fd, &statbuf) == -1) {
+ error_code = HTTP_STATUS_500;
+ goto file_error;
+
+ }
+
+ if (!S_ISREG(statbuf.st_mode)) {
+ error_code = HTTP_STATUS_400;
+ goto file_error;
+ }
+
+ /* TODO: implement request range support */
+ conn->response.body.fd = fd;
+ conn->response.body.off = 0;
+ conn->response.body.len = statbuf.st_size;
+
+ enum mime_content_type content_type = mime_get_content_type_from_filename(path);
+ size_t content_length = conn->response.body.len;
+
+ prepare_response(&conn->arena, conn->http_version, HTTP_STATUS_200, conn->linger,
+ content_type, content_length);
+
+ if (conn->response.body.len)
+ connection_start_read_chunk(conn);
+ else /* no body to read */
+ connection_start_send_chunk(conn);
+ return;
+
+file_error:
+ close(fd);
+
+error:
+ prepare_error(&conn->arena, conn->http_version, error_code, conn->linger);
+ connection_start_send_chunk(conn);
+}
+
+void
+connection_start_send_chunk(struct connection *conn)
+{
+ conn->io_request.type = IO_SEND;
+ conn->io_request.send.fd = conn->sockfd;
+ conn->io_request.send.buf = conn->arena.ptr;
+ conn->io_request.send.len = conn->arena.len;
+ conn->io_request.send.conn = conn;
+
+ submit_io_requests(&conn->io_request, 1);
+}
+
+void
+connection_on_send_chunk(struct connection *conn, int bytes)
+{
+ (void) bytes;
+
+ /* TODO: we assume all headers are sent in one chunk. is this always the case? */
+
+ if (conn->response.body.len) { /* have remaining body to send */
+ arena_reset(&conn->arena);
+
+ connection_start_read_chunk(conn);
+ return;
+ }
+
+ /* TODO: pool open file descriptors in a fs cache? */
+ if (conn->response.body.fd > -1)
+ close(conn->response.body.fd);
+
+ if (conn->linger)
+ connection_start_recv(conn);
+ else
+ connection_close(conn);
+}
+
+void
+connection_start_read_chunk(struct connection *conn)
+{
+ size_t len = MIN(conn->response.body.len, conn->arena.cap - conn->arena.len);
+
+ conn->io_request.type = IO_READ;
+ conn->io_request.read.fd = conn->response.body.fd;
+ conn->io_request.read.off = conn->response.body.off;
+ conn->io_request.read.buf = (u8 *) conn->arena.ptr + conn->arena.len;
+ conn->io_request.read.len = len;
+ conn->io_request.read.conn = conn;
+
+ submit_io_requests(&conn->io_request, 1);
+}
+
+void
+connection_on_read_chunk(struct connection *conn, int bytes)
+{
+ conn->arena.len += bytes;
+
+ conn->response.body.off += bytes;
+ conn->response.body.len -= bytes;
+
+ connection_start_send_chunk(conn);
+}
+
+void
+connection_close(struct connection *conn)
+{
+ conn->io_request.type = IO_CLOSE;
+ conn->io_request.close.fd = conn->sockfd;
+ conn->io_request.close.conn = conn;
+
+ submit_io_requests(&conn->io_request, 1);
+}
+
+bool
+response_begin(struct arena *arena, enum http_version version, enum http_status_code status_code)
+{
+ char buf[64];
+ size_t len = snprintf(buf, ARRLEN(buf), "%s %s" HTTP_DELIMITER,
+ http_version_str(version), http_status_code_str(status_code));
+
+ void *dst = PUSH_ARRAY(arena, char, len);
+ if (!dst) return false;
+
+ memcpy(dst, buf, len);
+
+ return true;
+}
+
+bool
+response_push_header(struct arena *arena, char *header, size_t len)
+{
+ void *dst = PUSH_ARRAY(arena, char, len);
+ if (!dst) return false;
+
+ memcpy(dst, header, len);
+
+ return true;
+}
+
+bool
+response_end_headers(struct arena *arena)
+{
+ size_t len = strlen(HTTP_DELIMITER);
+ void *dst = PUSH_ARRAY(arena, char, len);
+ if (!dst) return false;
+
+ memcpy(dst, HTTP_DELIMITER, len);
+
+ return true;
+}
+
+#include "http.c"
+#include "mime.c"
+#include "uri.c"
+
+#include "utils.c"
diff --git a/httpsrv/httpsrv.h b/httpsrv/httpsrv.h
@@ -0,0 +1,327 @@
+#ifndef HTTPSRV_H
+#define HTTPSRV_H
+
+#define _XOPEN_SOURCE 700
+#define _DEFAULT_SOURCE
+#define _GNU_SOURCE
+
+#include <assert.h>
+#include <signal.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/uio.h>
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include <liburing.h>
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+#define KiB 1024
+#define MiB (1024 * KiB)
+#define GiB (1024 * MiB)
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define ALIGN_TO_PREV(v, align) ((v) & ~((align) - 1))
+#define ALIGN_TO_NEXT(v, align) ALIGN_TO_PREV((v) + ((align) - 1), (align))
+
+#define IS_ALIGNED_TO(v, align) (((v) & ((align) - 1)) == 0)
+
+struct arena {
+ void *ptr;
+ u64 cap, len;
+};
+
+inline void *
+arena_alloc(struct arena *arena, u64 size, u64 align)
+{
+ assert(size);
+ assert(align);
+ assert((align == 1) || (align % 2) == 0);
+
+ uint64_t aligned_len = ALIGN_TO_NEXT(arena->len, align);
+ if (arena->cap < aligned_len + size) return NULL;
+
+ void *ptr = (uint8_t *) arena->ptr + aligned_len;
+ arena->len = aligned_len + size;
+
+ return ptr;
+}
+
+inline void
+arena_reset(struct arena *arena)
+{
+ arena->len = 0;
+}
+
+#define PUSH_SIZED(arena, T) \
+ arena_alloc(arena, sizeof(T), alignof(T))
+
+#define PUSH_ARRAY(arena, T, n) \
+ arena_alloc(arena, sizeof(T) * n, alignof(T))
+
+struct list_node;
+struct list_node {
+ struct list_node *prev, *next;
+};
+
+#define FROM_LIST_NODE(node, T, member) \
+ ((T *) (((uintptr_t) (node)) - offsetof(T, member)))
+
+struct list {
+ struct list_node *head, *tail;
+};
+
+inline void
+list_push(struct list *list, struct list_node *node)
+{
+ if (!list->head)
+ list->head = node;
+
+ if (list->tail)
+ list->tail->next = node;
+
+ node->next = NULL;
+ node->prev = list->tail;
+ list->tail = node;
+}
+
+inline struct list_node *
+list_pull(struct list *list)
+{
+ if (!list->head)
+ return NULL;
+
+ if (list->head == list->tail)
+ list->tail = NULL;
+
+ struct list_node *node = list->head;
+ list->head = node->next;
+ node->prev = node->next = NULL;
+
+ return node;
+}
+
+#include "http.h"
+#include "mime.h"
+#include "uri.h"
+
+struct opts {
+ char *addr;
+ char *port;
+ char *webroot;
+ u64 connection_backlog;
+ u64 connection_cap;
+ u64 connection_arena_sz;
+};
+
+struct connection;
+
+#define IO_REQUEST_IOVS_MAX 8
+
+struct io_request {
+ enum { IO_ACCEPT, IO_RECV, IO_SEND, IO_READ, IO_WRITEV, IO_CLOSE, } type;
+
+ union {
+ struct {
+ int fd;
+ struct sockaddr *addr;
+ socklen_t *addrlen;
+ } accept;
+
+ struct {
+ int fd;
+ void *buf;
+ size_t len;
+ struct connection *conn;
+ } recv, send;
+
+ struct {
+ int fd;
+ off_t off;
+ void *buf;
+ size_t len;
+ struct connection *conn;
+ } read;
+
+ struct {
+ int fd;
+ struct iovec iovs[IO_REQUEST_IOVS_MAX];
+ size_t len;
+ struct connection *conn;
+ } writev;
+
+ struct {
+ int fd;
+ struct connection *conn;
+ } close;
+ };
+
+ struct list_node freelist_node;
+};
+
+extern bool
+submit_io_requests(struct io_request *buf, size_t len);
+
+struct connection {
+ int sockfd;
+ int webrootfd;
+
+ enum http_version http_version;
+ bool linger;
+
+ struct {
+ struct {
+ int fd;
+ off_t off;
+ size_t len;
+ } body;
+ } response;
+
+ struct arena arena;
+
+ struct io_request io_request;
+
+ struct list_node freelist_node;
+};
+
+extern void
+connection_init(struct connection *conn, int sockfd, int webrootfd);
+
+extern void
+connection_start_recv(struct connection *conn);
+
+extern void
+connection_on_recv_chunk(struct connection *conn, int bytes);
+
+extern void
+connection_handle_request(struct connection *conn, enum http_method method, struct uri uri);
+
+extern void
+connection_start_send_chunk(struct connection *conn);
+
+extern void
+connection_on_send_chunk(struct connection *conn, int bytes);
+
+extern void
+connection_start_read_chunk(struct connection *conn);
+
+extern void
+connection_on_read_chunk(struct connection *conn, int bytes);
+
+extern void
+connection_close(struct connection *conn);
+
+struct connection_pool {
+ struct connection *ptr;
+ u64 cap;
+
+ struct arena arena;
+
+ struct list freelist;
+};
+
+extern bool
+connection_pool_init(struct connection_pool *pool, struct opts const *opts);
+
+inline struct connection *
+connection_pool_alloc(struct connection_pool *pool)
+{
+ return FROM_LIST_NODE(list_pull(&pool->freelist), struct connection, freelist_node);
+}
+
+inline void
+connection_pool_free(struct connection_pool *pool, struct connection *conn)
+{
+ list_push(&pool->freelist, &conn->freelist_node);
+}
+
+extern int
+httpsrv_run(struct opts const *opts, int servfd, int webrootfd, struct connection_pool *pool);
+
+extern bool
+response_begin(struct arena *arena, enum http_version version, enum http_status_code status_code);
+
+extern bool
+response_push_header(struct arena *arena, char *header, size_t len);
+
+extern bool
+response_end_headers(struct arena *arena);
+
+extern bool
+response_push_body(struct arena *arena, int fd, off_t *off, size_t *len);
+
+inline bool
+prepare_error(struct arena *arena, enum http_version version, enum http_status_code status_code,
+ bool linger)
+{
+ arena_reset(arena);
+
+ char connection_buf[64];
+ size_t connection_len = snprintf(connection_buf, ARRLEN(connection_buf),
+ "Connection: close");
+
+ char content_length_buf[64];
+ size_t content_length_len = snprintf(content_length_buf,
+ ARRLEN(content_length_buf),
+ "Content-Length: 0" HTTP_DELIMITER);
+
+ return response_begin(arena, version, status_code) &&
+ (linger || response_push_header(arena, connection_buf, connection_len)) &&
+ response_push_header(arena, content_length_buf, content_length_len) &&
+ response_end_headers(arena);
+}
+
+inline bool
+prepare_response(struct arena *arena, enum http_version version, enum http_status_code status_code,
+ bool linger, enum mime_content_type content_type, size_t content_length)
+{
+ arena_reset(arena);
+
+ char connection_buf[64];
+ size_t connection_len = snprintf(connection_buf, ARRLEN(connection_buf),
+ "Connection: close");
+
+ char content_type_buf[64];
+ size_t content_type_len = snprintf(content_type_buf,
+ ARRLEN(content_type_buf),
+ "Content-Type: %s" HTTP_DELIMITER,
+ mime_content_type_str(content_type));
+
+ char content_length_buf[64];
+ size_t content_length_len = snprintf(content_length_buf,
+ ARRLEN(content_length_buf),
+ "Content-Length: %zu" HTTP_DELIMITER, content_length);
+
+ return response_begin(arena, version, status_code) &&
+ (linger || response_push_header(arena, connection_buf, connection_len)) &&
+ response_push_header(arena, content_type_buf, content_type_len) &&
+ response_push_header(arena, content_length_buf, content_length_len) &&
+ response_end_headers(arena);
+}
+
+#endif /* HTTPSRV_H */
diff --git a/httpsrv/mime.c b/httpsrv/mime.c
@@ -0,0 +1,68 @@
+#include "mime.h"
+
+static char const *mime_content_type_tab[] = {
+ [MIME_APPLICATION_OCTET_STREAM] = "application/octet-stream",
+
+ [MIME_IMAGE_BMP] = "image/bmp",
+ [MIME_IMAGE_GIF] = "image/gif",
+ [MIME_IMAGE_JPEG] = "image/jpeg",
+ [MIME_IMAGE_PNG] = "image/png",
+
+ [MIME_TEXT_PLAIN] = "text/plain",
+ [MIME_TEXT_HTML] = "text/html",
+ [MIME_TEXT_CSS] = "text/css",
+ [MIME_TEXT_JAVASCRIPT] = "text/javascript",
+};
+
+char const *
+mime_content_type_str(enum mime_content_type content_type)
+{
+ return mime_content_type_tab[content_type];
+}
+
+#include <string.h>
+#include <ctype.h>
+
+enum mime_content_type
+mime_get_content_type_from_filename(char const *filename)
+{
+ char const *extension = strrchr(filename, '.');
+ assert(extension);
+
+ char ext[16];
+
+ size_t extlen = strlen(extension);
+ assert(extlen < ARRLEN(ext));
+
+ size_t i;
+ for (i = 0; i < ARRLEN(ext) - 1; i++)
+ ext[i] = tolower(extension[i]);
+ ext[i] = '\0';
+
+ if (strcmp(ext, ".bmp") == 0)
+ return MIME_IMAGE_BMP;
+
+ if (strcmp(ext, ".gif") == 0)
+ return MIME_IMAGE_GIF;
+
+ if (strcmp(ext, ".jpeg") == 0 || strcmp(ext, ".jpg") == 0)
+ return MIME_IMAGE_JPEG;
+
+ if (strcmp(ext, ".png") == 0)
+ return MIME_IMAGE_PNG;
+
+ if (strcmp(ext, ".txt") == 0)
+ return MIME_TEXT_PLAIN;
+
+ if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0)
+ return MIME_TEXT_HTML;
+
+ if (strcmp(ext, ".css") == 0)
+ return MIME_TEXT_CSS;
+
+ if (strcmp(ext, ".js") == 0)
+ return MIME_TEXT_CSS;
+
+ /* unknown media type */
+ return MIME_APPLICATION_OCTET_STREAM;
+}
diff --git a/httpsrv/mime.h b/httpsrv/mime.h
@@ -0,0 +1,24 @@
+#ifndef MIME_H
+#define MIME_H
+
+enum mime_content_type {
+ MIME_APPLICATION_OCTET_STREAM,
+
+ MIME_IMAGE_BMP,
+ MIME_IMAGE_GIF,
+ MIME_IMAGE_JPEG,
+ MIME_IMAGE_PNG,
+
+ MIME_TEXT_PLAIN,
+ MIME_TEXT_HTML,
+ MIME_TEXT_CSS,
+ MIME_TEXT_JAVASCRIPT,
+};
+
+extern char const *
+mime_content_type_str(enum mime_content_type content_type);
+
+extern enum mime_content_type
+mime_get_content_type_from_filename(char const *filename);
+
+#endif /* MIME_H */
diff --git a/httpsrv/uri.c b/httpsrv/uri.c
@@ -0,0 +1,233 @@
+#include "uri.h"
+
+#include <limits.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];
+}
+
+#include <ctype.h>
+
+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/httpsrv/uri.h b/httpsrv/uri.h
@@ -0,0 +1,22 @@
+#ifndef URI_H
+#define URI_H
+
+struct urifrag {
+ char *ptr;
+ int len;
+};
+
+extern bool
+urifrag_try_normalise(struct urifrag *frag);
+
+extern void
+urifrag_remove_dot_segments(struct urifrag *frag);
+
+struct uri {
+ struct urifrag scheme, user, host, port, path, query, fragment;
+};
+
+extern struct uri
+uri_parse(char *buf, size_t len);
+
+#endif /* URI_H */
diff --git a/httpsrv/utils.c b/httpsrv/utils.c
@@ -0,0 +1,25 @@
+extern inline void *
+arena_alloc(struct arena *arena, u64 size, u64 align);
+
+extern inline void
+arena_reset(struct arena *arena);
+
+extern inline void
+list_push(struct list *list, struct list_node *node);
+
+extern inline struct list_node *
+list_pull(struct list *list);
+
+extern inline struct connection *
+connection_pool_alloc(struct connection_pool *pool);
+
+extern inline void
+connection_pool_free(struct connection_pool *pool, struct connection *conn);
+
+extern inline bool
+prepare_error(struct arena *arena, enum http_version version, enum http_status_code status_code,
+ bool linger);
+
+extern inline bool
+prepare_response(struct arena *arena, enum http_version version, enum http_status_code status_code,
+ bool linger, enum mime_content_type content_type, size_t content_length);
diff --git a/tlsterm/tlsterm.c b/tlsterm/tlsterm.c
@@ -0,0 +1,53 @@
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+struct opts {
+ bool verbose;
+};
+
+static struct opts opts;
+static char const *optstr = "hv";
+
+static void
+usage(char *program)
+{
+ fprintf(stderr, "Usage: %s [-h] [-v]\n", program);
+ fprintf(stderr, "\t-h : display this help message\n");
+ fprintf(stderr, "\t-v : enable verbose logging\n");
+}
+
+static bool
+try_parse_args(int argc, char **argv)
+{
+ int chr;
+ while ((chr = getopt(argc, argv, optstr)) != -1) {
+ switch (chr) {
+ case 'v':
+ opts.verbose = true;
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+int
+main(int argc, char **argv)
+{
+ if (!try_parse_args(argc, argv)) {
+ usage(argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Verbose? %d\n", opts.verbose);
+
+ exit(EXIT_SUCCESS);
+}