commit 843d775d11db3c8819cac0542999f3db47ac9ad7
parent 22845e5f6316831f4b72a6ec8fcd7cf3a5f4713e
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date:   Fri,  7 Feb 2025 17:35:17 +0000
Fix kept-alive connections for http/1.1, flesh out mime types
Diffstat:
6 files changed, 222 insertions(+), 142 deletions(-)
diff --git a/debug.sh b/debug.sh
@@ -2,4 +2,4 @@
 
 set -ex
 
-lldbgui -- bin/website -v out/
+lldbgui -- bin/website -v $@
diff --git a/website/connection.c b/website/connection.c
@@ -54,8 +54,6 @@ parse_http_request(char *src, struct http_request *request, struct http_headers 
 		while (isspace(*val)) /* strip leading whitespace */
 			val++;
 
-		printf("%s: %s\n", key, val);
-
 		for (char *ptr = key; *ptr; ptr++) /* normalise as lowercase */
 			*ptr = isalpha(*ptr) ? tolower(*ptr) : *ptr;
 
@@ -214,6 +212,10 @@ prepare_response_headers(struct connection *conn, enum http_status status,
 	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
@@ -249,6 +251,7 @@ handle_request(struct io_uring *ioring, struct connection *conn, int webroot)
 		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)
@@ -379,6 +382,7 @@ handle_response_chunk(struct io_uring *ioring, struct connection *conn, int res)
 	}
 
 	if (conn->response_headers.keep_alive) {
+		conn->len = conn->cur = 0;
 		enqueue_recv(ioring, &conn->ioreq, conn->socket,
 			     conn->buf, conn->cap, 0);
 		return;
diff --git a/website/main.c b/website/main.c
@@ -6,8 +6,7 @@ struct opts opts = {
 	.port = "8080",
 };
 
-#define OPTSTR "hv"
-
+#define OPTSTR "hva:p:"
 static void
 usage(char *prog)
 {
@@ -79,6 +78,148 @@ reload_handler(int sig, siginfo_t *info, void *uctx)
 }
 
 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) {
@@ -151,4 +292,3 @@ main(int argc, char **argv)
 #include "net.c"
 #include "uri.c"
 #include "connection.c"
-#include "server.c"
diff --git a/website/server.c b/website/server.c
@@ -1,135 +0,0 @@
-#include "website.h"
-
-int
-serve(struct io_uring *ioring, int webroot, int sock, struct connection_pool *pool)
-{
-	listen(sock, SERVER_LISTEN_BACKLOG);
-
-	struct sockaddr_storage client_addr;
-	socklen_t client_addrlen = sizeof client_addr;
-
-	struct ioreq accept_req = {
-		.type = IOREQ_ACCEPT,
-		.fd = sock,
-		.accept.addr = (struct sockaddr *) &client_addr,
-		.accept.addrlen = &client_addrlen,
-		.accept.flags = 0,
-	};
-
-	enqueue_ioreq(ioring, &accept_req);
-	submit_ioreqs(ioring);
-
-	quit = 0;
-	while (!quit) {
-		struct io_uring_cqe *cqe;
-		if (io_uring_wait_cqe(ioring, &cqe) < 0) {
-			fprintf(stderr, "io_uring_wait_cqe() failed\n");
-			return EXIT_FAILURE;
-		}
-
-		unsigned head, seen = 0;
-		io_uring_for_each_cqe(ioring, head, cqe) {
-			struct ioreq *ioreq = io_uring_cqe_get_data(cqe);
-			if (!ioreq) { /* overloaded client error messages */
-				// TODO: do something?
-			}
-
-			int res = cqe->res;
-			switch (ioreq->type) {
-			case IOREQ_ACCEPT: {
-				if (res < 0) { /* multishot failed, restart? */
-					fprintf(stderr, "multishot accept failed\n");
-					return EXIT_FAILURE;
-				}
-
-				struct connection *conn = connection_pool_alloc(pool);
-				if (!conn) { /* overloaded server */
-					close(res);
-					goto next_cqe;
-				}
-
-				char host[NI_MAXHOST];
-				char port[NI_MAXSERV];
-				getnameinfo((struct sockaddr *) &client_addr,
-					    client_addrlen,
-					    host, sizeof host,
-					    port, sizeof port,
-					    NI_NUMERICHOST | NI_NUMERICSERV);
-
-				fprintf(stderr, "[%d] Client connection from %s:%s\n",
-						res, host, port);
-
-				handle_accept(ioring, conn, res);
-			} break;
-
-			case IOREQ_RECV: {
-				struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
-				assert(conn);
-
-				if (res <= 0) {
-					fprintf(stderr, "recv(): %d (%s), closing connection\n", res, strerror(-res));
-					close_connection(ioring, conn);
-					break;
-				}
-
-				fprintf(stderr, "[%d] Client recv: %d bytes\n",
-						conn->socket, res);
-
-				handle_request_chunk(ioring, conn, webroot, res);
-			} break;
-
-			case IOREQ_SEND: {
-				struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
-				assert(conn);
-
-				if (res <= 0) {
-					fprintf(stderr, "send(): %d (%s), closing connection\n", res, strerror(-res));
-					close_connection(ioring, conn);
-					break;
-				}
-
-				fprintf(stderr, "[%d] Client send(): %d bytes\n",
-						conn->socket, res);
-
-				handle_response_chunk(ioring, conn, res);
-			} break;
-
-			case IOREQ_READ: {
-				struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
-				assert(conn);
-
-				if (res <= 0) {
-					fprintf(stderr, "read(): %d (%s), closing connection\n", res, strerror(-res));
-					close_connection(ioring, conn);
-					break;
-				}
-
-
-				fprintf(stderr, "[%d] Client read: %d bytes\n",
-						conn->socket, res);
-
-				handle_fileread_chunk(ioring, conn, res);
-			} break;
-
-			case IOREQ_CLOSE: {
-				struct connection *conn = TO_PARENT(ioreq, struct connection, ioreq);
-				assert(conn);
-
-				fprintf(stderr, "[%d] Client closed connection\n",
-						conn->socket);
-
-				connection_pool_free(pool, conn);
-			} break;
-			}
-
-next_cqe:
-			seen++;
-		}
-
-		io_uring_cq_advance(ioring, seen);
-
-		submit_ioreqs(ioring);
-	}
-
-	return EXIT_SUCCESS;
-}
diff --git a/website/utils.c b/website/utils.c
@@ -26,3 +26,8 @@ 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
@@ -37,7 +37,7 @@
 #define CONNECTION_POOL_CAPACITY 1024
 
 #define CONNECTION_CONCURRENT_OPS 2
-#define CONNECTION_BUFFER_SIZE 4096
+#define CONNECTION_BUFFER_SIZE 8192
 #define CONNECTION_TIMEOUT_MS -1
 
 /* utilities
@@ -193,6 +193,20 @@ 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
  * ===========================================================================
  */
@@ -555,6 +569,33 @@ 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") == 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;
 }
 
@@ -564,9 +605,34 @@ 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;
 }