httpsrv

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

commit 27fce716699f047217c1de041ccf9f822bb9c92c
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Sun, 31 Mar 2024 18:57:12 +0000

Initial commit.

Diffstat:
A.editorconfig | 17+++++++++++++++++
A.gitignore | 4++++
Abuild.sh | 19+++++++++++++++++++
Aclean.sh | 5+++++
Aextra/index.html | 10++++++++++
Ahttpsrv/http.c | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/http.h | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/httpsrv.c | 679+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/httpsrv.h | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/mime.c | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/mime.h | 24++++++++++++++++++++++++
Ahttpsrv/uri.c | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpsrv/uri.h | 22++++++++++++++++++++++
Ahttpsrv/utils.c | 25+++++++++++++++++++++++++
Atlsterm/tlsterm.c | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
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); +}