From 689a4bcf05f0b81f3bdf680545673bbeb5b8dcc1 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 02:53:58 -0500 Subject: [PATCH 01/13] refactor: extract trie walk from th_router_add_route Split th_router_add_route into th_router_find_or_create_segment (path resolution) and the method-slot write, so websocket route registration can reuse the trie walk without duplicating it. --- src/th_router.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/th_router.c b/src/th_router.c index 1fb5cad..a73af9f 100644 --- a/src/th_router.c +++ b/src/th_router.c @@ -238,8 +238,8 @@ th_route_parse_trail(th_str* trail, th_str* name, th_capture_type* type) return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_router_add_route(th_router* router, th_method method, th_str path, th_handler handler, void* user_data) +TH_LOCAL(th_err) +th_router_find_or_create_segment(th_router* router, th_str path, th_route_segment** out) { if (th_str_empty(path) || path.ptr[0] != '/') return TH_ERR_INVALID_ARG; @@ -278,7 +278,17 @@ th_router_add_route(th_router* router, th_method method, th_str path, th_handler } } } + *out = route; + return TH_ERR_OK; +} +TH_PRIVATE(th_err) +th_router_add_route(th_router* router, th_method method, th_str path, th_handler handler, void* user_data) +{ + th_route_segment* route = NULL; + th_err err = TH_ERR_OK; + if ((err = th_router_find_or_create_segment(router, path, &route)) != TH_ERR_OK) + return err; if (route->handler[TH_METHOD_ANY].handler != NULL || route->handler[method].handler != NULL) return TH_ERR_INVALID_ARG; // Route already exists From 8ccb5a0c1ccdcbaeb164dc929a4be4c92214abda Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 03:39:53 -0500 Subject: [PATCH 02/13] feat: add WebSocket public API, refactor router path resolution - add th_ws_handler/th_ws_send/th_ws_close/th_route_ws public API, TH_ERR_BUSY, TH_CODE_SWITCHING_PROTOCOLS - th_router_resolve/th_route_consume_trail take a th_str path and a capture callback instead of a th_request*, so path resolution no longer depends on a full request - th_router_find_ws_route now takes a th_str path directly, matching resolve's new signature and letting WS route lookups skip building a th_request entirely --- include/th.h | 46 ++++++++++++++++++++++++++++ src/th_error.c | 2 ++ src/th_http_error.h | 3 ++ src/th_router.c | 73 ++++++++++++++++++++++++++++++++++---------- src/th_router.h | 23 ++++++++++++++ src/th_router_test.c | 64 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+), 16 deletions(-) diff --git a/include/th.h b/include/th.h index 83cdc93..43e2357 100644 --- a/include/th.h +++ b/include/th.h @@ -53,6 +53,7 @@ typedef struct th_buffer { #define TH_ERRC_EOF 3 #define TH_ERRC_NOSUPPORT 4 #define TH_ERRC_UNKNOWN 5 +#define TH_ERRC_BUSY 6 /* error pack/unpack macros */ #define TH_ERR_CODE_BITS (sizeof(unsigned) * 8 - 4) @@ -70,6 +71,7 @@ typedef enum th_err { TH_ERR_EOF = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_EOF), TH_ERR_NOSUPPORT = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_NOSUPPORT), TH_ERR_UNKNOWN = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_UNKNOWN), + TH_ERR_BUSY = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_BUSY), } th_err; #define TH_ERR(category, code) (th_err)(TH_ERR_PACK(category, code)) @@ -131,6 +133,7 @@ typedef enum th_method { } th_method; typedef enum th_code { + TH_CODE_SWITCHING_PROTOCOLS = 101, TH_CODE_OK = 200, TH_CODE_MOVED_PERMANENTLY = 301, TH_CODE_BAD_REQUEST = 400, @@ -322,6 +325,39 @@ th_err th_add_cookie(th_response* resp, const char* key, const char* value, th_c */ typedef th_err (*th_handler)(void* userp, const th_request* req, th_response* resp); +/** th_ws_event + * @brief The kind of event delivered to a th_ws_handler. + */ +typedef enum th_ws_event { + TH_WS_EVENT_OPEN, // connection upgraded, ready to send/receive + TH_WS_EVENT_DATA, // one complete message received (data holds its payload) + TH_WS_EVENT_CLOSE, // connection closed, ws is no longer valid after this call returns +} th_ws_event; + +typedef struct th_ws th_ws; + +/** th_ws_handler + * @brief WebSocket event callback. data is empty for TH_WS_EVENT_OPEN/CLOSE, + * and holds one complete message's payload for TH_WS_EVENT_DATA. ws must + * not be used after TH_WS_EVENT_CLOSE has been delivered. + */ +typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data); + +/** th_ws_send + * @brief Sends one WebSocket message. binary selects the opcode (text vs + * binary), it does not otherwise affect encoding - data is sent as-is. + * @return TH_ERR_BUSY if a previous send on this connection hasn't + * finished yet (retry once the next event is delivered), TH_ERR_INVALID_ARG + * if the connection is closing/closed. + */ +th_err th_ws_send(th_ws* ws, th_buffer data, bool binary); + +/** th_ws_close + * @brief Starts closing the connection. TH_WS_EVENT_CLOSE will be + * delivered once the close handshake completes. + */ +th_err th_ws_close(th_ws* ws); + typedef struct th_server th_server; /** th_server_create @@ -345,6 +381,16 @@ th_err th_bind(th_server* server, const char* addr, const char* port, th_bind_op th_err th_route(th_server* server, th_method method, const char* route, th_handler handler, void* userp); +/** th_route_ws + * @brief Registers a WebSocket endpoint at path. Any request to path with + * a valid WebSocket handshake is upgraded, after which handler(userp, ...) + * receives its events. To gate the upgrade (e.g. auth), also register a + * plain th_route on the same path with TH_METHOD_GET: if it returns an + * error, that error is sent as a normal HTTP response and no upgrade + * happens; if it returns TH_ERR_OK (or is absent), the upgrade proceeds. + */ +th_err th_route_ws(th_server* server, const char* path, th_ws_handler handler, void* userp); + /** th_err * @brief Add a directory to the server (for serving or storing files). * @param name Label for the directory. diff --git a/src/th_error.c b/src/th_error.c index bed9fc2..bf817de 100644 --- a/src/th_error.c +++ b/src/th_error.c @@ -19,6 +19,8 @@ th_strerror(th_err err) return "invalid argument"; case TH_ERRC_EOF: return "end of file"; + case TH_ERRC_BUSY: + return "busy"; default: return "unknown error"; } diff --git a/src/th_http_error.h b/src/th_http_error.h index d236b78..2f934d3 100644 --- a/src/th_http_error.h +++ b/src/th_http_error.h @@ -43,6 +43,9 @@ TH_INLINE(const char*) th_http_strerror(int code) { switch (code) { + case TH_CODE_SWITCHING_PROTOCOLS: + return "Switching Protocols"; + break; case TH_CODE_OK: return "OK"; break; diff --git a/src/th_router.c b/src/th_router.c index a73af9f..93feb01 100644 --- a/src/th_router.c +++ b/src/th_router.c @@ -1,6 +1,5 @@ #include "th_router.h" #include "th_allocator.h" -#include "th_log.h" #include "th_request.h" #include "th_str.h" #include "th_url_decode.h" @@ -9,9 +8,6 @@ #include #include -#undef TH_LOG_TAG -#define TH_LOG_TAG "router" - TH_LOCAL(th_err) th_route_init(th_route_segment* route, th_capture_type type, th_str segment, th_allocator* allocator) { @@ -86,7 +82,7 @@ th_router_deinit(th_router* router) } TH_LOCAL(th_err) -th_route_consume_trail(th_route_segment* route, th_request* request, th_str* trail, bool dry, bool* result) +th_route_consume_trail(th_route_segment* route, th_str* trail, th_router_capture_cb on_capture, void* userp, bool* result) { th_str route_name = th_string_view(&route->name); th_str raw_segment = th_str_substr(*trail, 0, th_str_find_first_of(*trail, 0, "/?")); @@ -111,21 +107,21 @@ th_route_consume_trail(th_route_segment* route, th_request* request, th_str* tra break; case TH_CAPTURE_TYPE_INT: if (th_str_is_uint(segment)) { - if (!dry) - (void)th_request_add_pathvar(request, route_name, segment); + if (on_capture) + on_capture(userp, route_name, segment); *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); *result = true; } break; case TH_CAPTURE_TYPE_STRING: - if (!dry) - (void)th_request_add_pathvar(request, route_name, segment); + if (on_capture) + on_capture(userp, route_name, segment); *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); *result = true; break; case TH_CAPTURE_TYPE_PATH: - if (!dry) - (void)th_request_add_pathvar(request, route_name, *trail); + if (on_capture) + on_capture(userp, route_name, *trail); *trail = th_str_make(NULL, 0); *result = true; break; @@ -139,19 +135,18 @@ th_route_consume_trail(th_route_segment* route, th_request* request, th_str* tra } TH_LOCAL(th_err) -th_router_do_handle(th_router* router, th_method method, th_request* request, th_response* response, bool dry) +th_router_resolve(th_router* router, th_str path, th_router_capture_cb on_capture, void* userp, th_route_segment** out) { - TH_LOG_DEBUG("Handling request %p: %s", request, th_string_data(&request->uri_path)); - if (*th_string_at(&request->uri_path, 0) != '/') + if (th_str_empty(path) || *path.ptr != '/') return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - th_str trail = th_str_substr(th_string_view(&request->uri_path), 1, th_str_npos); + th_str trail = th_str_substr(path, 1, th_str_npos); th_route_segment* route = router->routes; while (1) { th_err err = TH_ERR_OK; bool consumed = false; if (route == NULL) { break; - } else if ((err = th_route_consume_trail(route, request, &trail, dry, &consumed)) != TH_ERR_OK + } else if ((err = th_route_consume_trail(route, &trail, on_capture, userp, &consumed)) != TH_ERR_OK || consumed) { if (err != TH_ERR_OK) return err; @@ -165,6 +160,25 @@ th_router_do_handle(th_router* router, th_method method, th_request* request, th if (route == NULL) { return TH_ERR_HTTP(TH_CODE_NOT_FOUND); } + *out = route; + return TH_ERR_OK; +} + +TH_LOCAL(void) +th_router_capture_to_pathvar(void* userp, th_str key, th_str value) +{ + th_request* request = userp; + (void)th_request_add_pathvar(request, key, value); +} + +TH_LOCAL(th_err) +th_router_do_handle(th_router* router, th_method method, th_request* request, th_response* response, bool dry) +{ + th_route_segment* route = NULL; + th_err err = TH_ERR_OK; + th_router_capture_cb on_capture = dry ? NULL : th_router_capture_to_pathvar; + if ((err = th_router_resolve(router, th_string_view(&request->uri_path), on_capture, request, &route)) != TH_ERR_OK) + return err; th_route_handler handler = route->handler[method].handler ? route->handler[method] : route->handler[TH_METHOD_ANY]; if (handler.handler == NULL) { return TH_ERR_HTTP(TH_CODE_METHOD_NOT_ALLOWED); @@ -186,6 +200,19 @@ th_router_would_handle(th_router* router, th_method method, th_request* request) return th_router_do_handle(router, method, request, NULL, true) == TH_ERR_OK; } +TH_PRIVATE(bool) +th_router_find_ws_route(th_router* router, th_str path, th_ws_handler* handler, void** user_data) +{ + th_route_segment* route = NULL; + if (th_router_resolve(router, path, NULL, NULL, &route) != TH_ERR_OK) + return false; + if (route->ws_handler.handler == NULL) + return false; + *handler = route->ws_handler.handler; + *user_data = route->ws_handler.user_data; + return true; +} + // abc < {int} < {string} < {path} TH_LOCAL(bool) th_route_lower(th_route_segment* lh, th_route_segment* rh) @@ -296,3 +323,17 @@ th_router_add_route(th_router* router, th_method method, th_str path, th_handler route->handler[method].user_data = user_data; return TH_ERR_OK; } + +TH_PRIVATE(th_err) +th_router_add_ws_route(th_router* router, th_str path, th_ws_handler handler, void* user_data) +{ + th_route_segment* route = NULL; + th_err err = TH_ERR_OK; + if ((err = th_router_find_or_create_segment(router, path, &route)) != TH_ERR_OK) + return err; + if (route->ws_handler.handler != NULL) + return TH_ERR_INVALID_ARG; // WS route already exists + route->ws_handler.handler = handler; + route->ws_handler.user_data = user_data; + return TH_ERR_OK; +} diff --git a/src/th_router.h b/src/th_router.h index 74dcbb9..18c3a34 100644 --- a/src/th_router.h +++ b/src/th_router.h @@ -19,6 +19,13 @@ typedef struct th_capture { th_str value; } th_capture; +/** th_router_capture_cb + * @brief Called by th_router_resolve for each capture ({name}, {int:name}, + * {path:name}) discovered while resolving a path. NULL means don't report + * captures (a dry run). + */ +typedef void (*th_router_capture_cb)(void* userp, th_str key, th_str value); + typedef enum th_capture_type { TH_CAPTURE_TYPE_NONE = 0, TH_CAPTURE_TYPE_INT, @@ -26,11 +33,17 @@ typedef enum th_capture_type { TH_CAPTURE_TYPE_PATH, } th_capture_type; +typedef struct th_ws_route_handler { + th_ws_handler handler; + void* user_data; +} th_ws_route_handler; + typedef struct th_route_segment th_route_segment; struct th_route_segment { th_capture_type type; th_string name; th_route_handler handler[TH_METHOD_MAX]; + th_ws_route_handler ws_handler; th_route_segment* next; th_route_segment* children; th_allocator* allocator; @@ -60,4 +73,14 @@ th_router_would_handle(th_router* router, th_method method, th_request* request) TH_PRIVATE(th_err) th_router_add_route(th_router* router, th_method method, th_str route, th_handler handler, void* user_data); +TH_PRIVATE(th_err) +th_router_add_ws_route(th_router* router, th_str route, th_ws_handler handler, void* user_data); + +/** th_router_find_ws_route + * @brief Resolves path to a registered WS route (ignoring method). On a + * match, sets handler and user_data and returns true. + */ +TH_PRIVATE(bool) +th_router_find_ws_route(th_router* router, th_str path, th_ws_handler* handler, void** user_data); + #endif diff --git a/src/th_router_test.c b/src/th_router_test.c index 271c2e0..0d7818c 100644 --- a/src/th_router_test.c +++ b/src/th_router_test.c @@ -32,6 +32,16 @@ expect_pathvars_handler(void* user_data, const th_request* req, th_response* res return TH_ERR_OK; } +static th_err +noop_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +{ + (void)userp; + (void)ws; + (void)ev; + (void)data; + return TH_ERR_OK; +} + TH_TEST_BEGIN(router) { TH_TEST_CASE_BEGIN(router_init) @@ -359,5 +369,59 @@ TH_TEST_BEGIN(router) th_router_deinit(&router); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_find_ws_route) + { + th_router router; + th_router_init(&router, NULL); + int userp = 0; + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), noop_ws_handler, &userp) == TH_ERR_OK); + th_ws_handler handler = NULL; + void* user_data = NULL; + TH_EXPECT(th_router_find_ws_route(&router, TH_STR("/ws"), &handler, &user_data)); + TH_EXPECT(handler == noop_ws_handler); + TH_EXPECT(user_data == &userp); + th_router_deinit(&router); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_find_ws_route_not_registered) + { + th_router router; + th_router_init(&router, NULL); + TH_EXPECT(th_router_add_route(&router, TH_METHOD_GET, TH_STR("/plain"), expect_pathvars_handler, NULL) == TH_ERR_OK); + th_ws_handler handler = NULL; + void* user_data = NULL; + TH_EXPECT(!th_router_find_ws_route(&router, TH_STR("/plain"), &handler, &user_data)); + th_router_deinit(&router); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_ws_route_and_http_route_coexist) + { + th_router router; + th_router_init(&router, NULL); + TH_EXPECT(th_router_add_route(&router, TH_METHOD_GET, TH_STR("/chat"), expect_pathvars_handler, NULL) == TH_ERR_OK); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/chat"), noop_ws_handler, NULL) == TH_ERR_OK); + th_request request = {0}; + th_request_init(&request, NULL); + request.method = TH_METHOD_GET; + th_string_set(&request.uri_path, TH_STR("/chat")); + th_response response = {0}; + TH_EXPECT(th_router_handle(&router, &request, &response) == TH_ERR_OK); + th_ws_handler handler = NULL; + void* user_data = NULL; + TH_EXPECT(th_router_find_ws_route(&router, th_string_view(&request.uri_path), &handler, &user_data)); + TH_EXPECT(handler == noop_ws_handler); + th_request_deinit(&request); + th_router_deinit(&router); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_add_ws_route_twice_fails) + { + th_router router; + th_router_init(&router, NULL); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), noop_ws_handler, NULL) == TH_ERR_OK); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), noop_ws_handler, NULL) == TH_ERR_INVALID_ARG); + th_router_deinit(&router); + } + TH_TEST_CASE_END } TH_TEST_END From 8ace8cd91577a3215691c6f8b3c786f79ac20dca Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 08:43:56 -0500 Subject: [PATCH 03/13] feat: implement WebSocket handshake and HTTP upgrade - add SHA-1, Base64, and RFC 6455 handshake/accept-key computation - wire WS upgrade into th_http: 101 response on valid handshake, 426 on non-handshake requests to WS routes, hand connection off to a new th_ws instance - th_router: WS-only routes get a default GET handler so they don't 405; registering a real handler later overrides it - fix Connection header value being discarded during parsing - fix default error body reason phrase always showing "Unknown" --- CMakeLists.txt | 8 ++ include/th.h | 13 ++- src/th_base64.c | 48 ++++++++ src/th_base64.h | 14 +++ src/th_base64_test.c | 68 ++++++++++++ src/th_conn.h | 3 +- src/th_http.c | 99 ++++++++++++++++- src/th_http.h | 4 + src/th_http_error.h | 3 + src/th_http_test.c | 50 +++++++++ src/th_request_parser.c | 2 +- src/th_request_parser_test.c | 11 ++ src/th_response.c | 4 +- src/th_router.c | 18 ++- src/th_router.h | 4 +- src/th_router_test.c | 31 ++++++ src/th_server.c | 12 ++ src/th_sha1.c | 86 +++++++++++++++ src/th_sha1.h | 16 +++ src/th_sha1_test.c | 51 +++++++++ src/th_str.c | 14 +++ src/th_str.h | 7 ++ src/th_ws.c | 87 +++++++++++++++ src/th_ws.h | 35 ++++++ src/th_ws_handshake.c | 60 ++++++++++ src/th_ws_handshake.h | 24 ++++ src/th_ws_handshake_test.c | 112 +++++++++++++++++++ src/th_ws_test.c | 205 +++++++++++++++++++++++++++++++++++ 28 files changed, 1074 insertions(+), 15 deletions(-) create mode 100644 src/th_base64.c create mode 100644 src/th_base64.h create mode 100644 src/th_base64_test.c create mode 100644 src/th_sha1.c create mode 100644 src/th_sha1.h create mode 100644 src/th_sha1_test.c create mode 100644 src/th_ws.c create mode 100644 src/th_ws.h create mode 100644 src/th_ws_handshake.c create mode 100644 src/th_ws_handshake.h create mode 100644 src/th_ws_handshake_test.c create mode 100644 src/th_ws_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4011919..86af4df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,10 @@ SET(TH_CORE_SRC src/th_timer.c src/th_conn_tracker.c src/th_url_decode.c + src/th_sha1.c + src/th_base64.c + src/th_ws_handshake.c + src/th_ws.c # SSL (compiled out via TH_WITH_SSL=0 when OpenSSL is not found) src/th_ssl_smem_bio.c src/th_ssl_context.c @@ -236,8 +240,12 @@ if (NOT TH_DISABLE_TESTS) src/th_cookie_parser_test.c src/th_multipart_parser_test.c src/th_url_decode_test.c + src/th_sha1_test.c + src/th_base64_test.c + src/th_ws_handshake_test.c src/th_response_test.c src/th_http_test.c + src/th_ws_test.c src/th_fcache_test.c src/th_filepath_test.c src/th_file_test.c diff --git a/include/th.h b/include/th.h index 43e2357..30ddc3a 100644 --- a/include/th.h +++ b/include/th.h @@ -146,6 +146,7 @@ typedef enum th_code { TH_CODE_URI_TOO_LONG = 414, TH_CODE_UNSUPPORTED_MEDIA_TYPE = 415, TH_CODE_RANGE_NOT_SATISFIABLE = 416, + TH_CODE_UPGRADE_REQUIRED = 426, TH_CODE_TOO_MANY_REQUESTS = 429, TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431, TH_CODE_INTERNAL_SERVER_ERROR = 500, @@ -343,14 +344,22 @@ typedef struct th_ws th_ws; */ typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data); +/** th_ws_msg_type + * @brief Selects the opcode a th_ws_send message goes out as. + */ +typedef enum th_ws_msg_type { + TH_WS_MSG_TEXT, + TH_WS_MSG_BINARY, +} th_ws_msg_type; + /** th_ws_send - * @brief Sends one WebSocket message. binary selects the opcode (text vs + * @brief Sends one WebSocket message. type selects the opcode (text vs * binary), it does not otherwise affect encoding - data is sent as-is. * @return TH_ERR_BUSY if a previous send on this connection hasn't * finished yet (retry once the next event is delivered), TH_ERR_INVALID_ARG * if the connection is closing/closed. */ -th_err th_ws_send(th_ws* ws, th_buffer data, bool binary); +th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_msg_type type); /** th_ws_close * @brief Starts closing the connection. TH_WS_EVENT_CLOSE will be diff --git a/src/th_base64.c b/src/th_base64.c new file mode 100644 index 0000000..9f15254 --- /dev/null +++ b/src/th_base64.c @@ -0,0 +1,48 @@ +#include "th_base64.h" + +#include + +static const char th_base64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +TH_LOCAL(size_t) +th_base64_encoded_len(size_t len) +{ + return ((len + 2) / 3) * 4; +} + +TH_PRIVATE(th_err) +th_base64_encode(th_str input, th_string* output) +{ + th_err err = th_string_resize(output, th_base64_encoded_len(input.len), '\0'); + if (err != TH_ERR_OK) + return err; + if (input.len == 0) + return TH_ERR_OK; + + const unsigned char* bytes = (const unsigned char*)input.ptr; + char* out = th_string_at(output, 0); + size_t i = 0; + size_t o = 0; + for (; i + 3 <= input.len; i += 3) { + uint32_t n = ((uint32_t)bytes[i] << 16) | ((uint32_t)bytes[i + 1] << 8) | (uint32_t)bytes[i + 2]; + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 6) & 0x3F]; + out[o++] = th_base64_alphabet[n & 0x3F]; + } + size_t remaining = input.len - i; + if (remaining == 1) { + uint32_t n = (uint32_t)bytes[i] << 16; + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = '='; + out[o++] = '='; + } else if (remaining == 2) { + uint32_t n = ((uint32_t)bytes[i] << 16) | ((uint32_t)bytes[i + 1] << 8); + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 6) & 0x3F]; + out[o++] = '='; + } + return TH_ERR_OK; +} diff --git a/src/th_base64.h b/src/th_base64.h new file mode 100644 index 0000000..546fac4 --- /dev/null +++ b/src/th_base64.h @@ -0,0 +1,14 @@ +#ifndef TH_BASE64_H +#define TH_BASE64_H + +#include "th_config.h" +#include "th_str.h" +#include "th_string.h" + +/** th_base64_encode + * @brief Base64-encodes input, overwriting output with the result. + */ +TH_PRIVATE(th_err) +th_base64_encode(th_str input, th_string* output); + +#endif diff --git a/src/th_base64_test.c b/src/th_base64_test.c new file mode 100644 index 0000000..de8d61f --- /dev/null +++ b/src/th_base64_test.c @@ -0,0 +1,68 @@ +#include "th_base64.h" +#include "th_test.h" + +#include + +static bool +encodes_to(const char* input, const char* expected) +{ + th_string out; + th_string_init(&out, th_default_allocator_get()); + bool ok = th_base64_encode(th_str_from_cstr(input), &out) == TH_ERR_OK + && th_str_eq(th_string_view(&out), th_str_from_cstr(expected)); + th_string_deinit(&out); + return ok; +} + +TH_TEST_BEGIN(base64) +{ + TH_TEST_CASE_BEGIN(base64_empty) + { + TH_EXPECT(encodes_to("", "")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_one_byte_needs_two_padding_chars) + { + TH_EXPECT(encodes_to("f", "Zg==")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_two_bytes_needs_one_padding_char) + { + TH_EXPECT(encodes_to("fo", "Zm8=")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_three_bytes_needs_no_padding) + { + TH_EXPECT(encodes_to("foo", "Zm9v")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_multiple_groups_with_padding) + { + TH_EXPECT(encodes_to("foobar", "Zm9vYmFy")); + TH_EXPECT(encodes_to("foob", "Zm9vYg==")); + TH_EXPECT(encodes_to("fooba", "Zm9vYmE=")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_sha1_digest_length) + { + // Sec-WebSocket-Accept encodes a 20-byte SHA-1 digest into 28 chars. + unsigned char digest[20] = {0}; + th_string out; + th_string_init(&out, th_default_allocator_get()); + TH_EXPECT(th_base64_encode(th_str_make((const char*)digest, sizeof(digest)), &out) == TH_ERR_OK); + TH_EXPECT(th_string_len(&out) == 28); + th_string_deinit(&out); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(base64_reuses_existing_output_string) + { + th_string out; + th_string_init(&out, th_default_allocator_get()); + TH_EXPECT(th_string_set(&out, TH_STR("leftover content")) == TH_ERR_OK); + TH_EXPECT(th_base64_encode(th_str_from_cstr("foo"), &out) == TH_ERR_OK); + TH_EXPECT(th_str_eq(th_string_view(&out), TH_STR("Zm9v"))); + th_string_deinit(&out); + } + TH_TEST_CASE_END +} +TH_TEST_END diff --git a/src/th_conn.h b/src/th_conn.h index f464053..c257bac 100644 --- a/src/th_conn.h +++ b/src/th_conn.h @@ -87,7 +87,8 @@ th_conn_cancel(th_conn* conn) TH_INLINE(void) th_conn_destroy(th_conn* conn) { - conn->methods->destroy(conn); + if (conn) + conn->methods->destroy(conn); } /* th_conn interface end */ diff --git a/src/th_http.c b/src/th_http.c index 1d01942..6bbd59d 100644 --- a/src/th_http.c +++ b/src/th_http.c @@ -1,6 +1,9 @@ #include "th_http.h" #include "th_fmt.h" #include "th_http_error.h" +#include "th_utility.h" +#include "th_ws.h" +#include "th_ws_handshake.h" #include @@ -22,12 +25,26 @@ th_http_destroy(void* self) th_allocator_free(http->allocator, http); } +// Moves conn out of http (leaving it destroy-safe with a NULL conn) and +// destroys everything else - used to hand conn off to upgrade it to +// another protocol without tearing it down. +TH_LOCAL(th_conn*) +th_http_detach_conn(th_http* http) +{ + th_conn* conn = TH_MOVE_PTR(http->conn); + th_http_destroy(http); + return conn; +} + TH_LOCAL(void) th_http_handle_read_request(void* user_data, size_t len, th_err err); TH_LOCAL(void) th_http_handle_write_response(void* user_data, size_t len, th_err err); +TH_LOCAL(void) +th_http_handle_error(th_http* http, th_err err); + TH_LOCAL(void) th_http_restart(th_http* http) { @@ -50,24 +67,90 @@ th_http_complete(th_http* http) } TH_LOCAL(void) -th_http_write_response(th_http* http) +th_http_write_response_cb(th_http* http, th_send_cb callback) { th_response_write_plan plan; th_err err = th_response_prepare_write(&http->response, &plan); if (err != TH_ERR_OK) { - th_http_handle_write_response(http, 0, err); + callback(http, 0, err); + return; + } + th_conn_send(http->conn, plan.iov, plan.iovcnt, plan.file, plan.offset, plan.len, callback, http); +} + +TH_LOCAL(void) +th_http_write_response(th_http* http) +{ + th_http_write_response_cb(http, th_http_handle_write_response); +} + +TH_LOCAL(void) +th_http_handle_ws_upgrade_written(void* user_data, size_t len, th_err err) +{ + th_http* http = user_data; + (void)len; + if (err != TH_ERR_OK) { + TH_LOG_ERROR("%p: Failed to write WS upgrade response: %s", (void*)http, th_strerror(err)); + th_http_destroy(http); + return; + } + th_ws_handler handler = http->ws_handler; + void* ws_user_data = http->ws_user_data; + th_conn* conn = th_http_detach_conn(http); + th_ws* ws = NULL; + if ((err = th_ws_create(&ws, conn, handler, ws_user_data, NULL)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to create ws instance: %s", th_strerror(err)); + th_conn_destroy(conn); + return; + } + th_ws_start(ws); +} + +// Sends the 101 response and, on success, hands conn off to a new th_ws. +// If request wasn't actually a WS handshake, sends a 426 Upgrade Required +// instead. +TH_LOCAL(void) +th_http_try_upgrade_ws(th_http* http) +{ + th_ws_handler handler = NULL; + void* user_data = NULL; + bool is_ws_route = th_router_find_ws_route(http->router, th_string_view(&http->request.uri_path), &handler, &user_data); + if (!is_ws_route || !th_ws_is_handshake(&http->request)) { + th_response_add_header(&http->response, TH_STR("Upgrade"), TH_STR("websocket")); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_UPGRADE_REQUIRED)); return; } - th_conn_send(http->conn, plan.iov, plan.iovcnt, plan.file, plan.offset, plan.len, th_http_handle_write_response, http); + + th_err err = TH_ERR_OK; + if ((err = th_response_add_header(&http->response, TH_STR("Upgrade"), TH_STR("websocket"))) != TH_ERR_OK) + goto fail; + if ((err = th_response_add_header(&http->response, TH_STR("Connection"), TH_STR("Upgrade"))) != TH_ERR_OK) + goto fail; + th_string accept_key; + th_string_init(&accept_key, http->allocator); + err = th_ws_handshake_accept_key(th_request_get_header(&http->request, TH_STR("sec-websocket-key")), &accept_key); + if (err == TH_ERR_OK) + err = th_response_add_header(&http->response, TH_STR("Sec-WebSocket-Accept"), th_string_view(&accept_key)); + th_string_deinit(&accept_key); + if (err != TH_ERR_OK) + goto fail; + + http->ws_handler = handler; + http->ws_user_data = user_data; + th_http_write_response_cb(http, th_http_handle_ws_upgrade_written); + return; +fail: + TH_LOG_ERROR("%p: Failed to prepare WS upgrade response: %s", (void*)http, th_strerror(err)); + th_response_reset(&http->response); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR)); } TH_LOCAL(void) th_http_write_error_response(th_http* http, th_err err) { th_response_set_code(&http->response, TH_ERR_CODE(err)); - if (th_string_len(&http->request.uri_path) == 0) { - // Set default error message - th_printf_body(&http->response, "%d %s", TH_ERR_CODE(err), th_http_strerror((int)err)); + if (!http->response.is_file && th_string_len(&http->response.body) == 0) { + th_printf_body(&http->response, "%d %s", TH_ERR_CODE(err), th_http_strerror((int)TH_ERR_CODE(err))); } if (http->close) { th_response_add_header(&http->response, TH_STR("Connection"), TH_STR("close")); @@ -173,6 +256,10 @@ th_http_handle_request_and_write_response(th_http* http) th_http_handle_require_1_1(http); return; } + if (TH_ERR_CODE(err) == TH_CODE_SWITCHING_PROTOCOLS) { + th_http_try_upgrade_ws(http); + return; + } break; case TH_HTTP_CODE_TYPE_SERVER_ERROR: case TH_HTTP_CODE_TYPE_CLIENT_ERROR: diff --git a/src/th_http.h b/src/th_http.h index 5decbfa..f180e9d 100644 --- a/src/th_http.h +++ b/src/th_http.h @@ -31,6 +31,10 @@ struct th_http { // true if the connection should be closed bool close; + + // set by th_http_try_upgrade_ws, read once the 101 response is written + th_ws_handler ws_handler; + void* ws_user_data; }; typedef struct th_http_upgrader { diff --git a/src/th_http_error.h b/src/th_http_error.h index 2f934d3..433a778 100644 --- a/src/th_http_error.h +++ b/src/th_http_error.h @@ -88,6 +88,9 @@ th_http_strerror(int code) case TH_CODE_RANGE_NOT_SATISFIABLE: return "Range Not Satisfiable"; break; + case TH_CODE_UPGRADE_REQUIRED: + return "Upgrade Required"; + break; case TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE: return "Request Header Fields Too Large"; break; diff --git a/src/th_http_test.c b/src/th_http_test.c index 2040a07..f448a6c 100644 --- a/src/th_http_test.c +++ b/src/th_http_test.c @@ -194,6 +194,16 @@ th_test_system_error_handler(void* user_data, const th_request* req, th_response return TH_ERR_SYSTEM(TH_ENOENT); } +static th_err +th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +{ + (void)userp; + (void)ws; + (void)ev; + (void)data; + return TH_ERR_OK; +} + TH_TEST_BEGIN(http) { th_conn_tracker tracker; @@ -204,6 +214,7 @@ TH_TEST_BEGIN(http) TH_EXPECT(th_router_add_route(&router, TH_METHOD_POST, TH_STR("/test"), th_test_handler, NULL) == TH_ERR_OK); TH_EXPECT(th_router_add_route(&router, TH_METHOD_GET, TH_STR("/informational"), th_test_informational_handler, NULL) == TH_ERR_OK); TH_EXPECT(th_router_add_route(&router, TH_METHOD_GET, TH_STR("/system-error"), th_test_system_error_handler, NULL) == TH_ERR_OK); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), th_test_ws_handler, NULL) == TH_ERR_OK); th_http_upgrader upgrader; th_http_upgrader_init(&upgrader, &tracker, &router, NULL, NULL, th_default_allocator_get()); th_fake_conn conn; @@ -233,6 +244,7 @@ TH_TEST_BEGIN(http) th_fake_conn_run(&conn); TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 404 Not Found\r\n")); + TH_EXPECT(th_buf_ends_with(conn.written, conn.written_len, "404 Not Found")); } TH_TEST_CASE_END TH_TEST_CASE_BEGIN(http_writes_400_for_bad_request) @@ -469,6 +481,44 @@ TH_TEST_BEGIN(http) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(http_upgrades_valid_ws_handshake) + { + th_fake_conn_set_request( + &conn, + TH_STR("GET /ws HTTP/1.1\r\nHost: example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n")); + + th_conn_upgrader_upgrade(&upgrader.base, &conn.base); + while (conn.written_len == 0) + th_fake_conn_run(&conn); + + TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 101 Switching Protocols\r\n")); + TH_EXPECT(th_buf_has_header(conn.written, conn.written_len, "Upgrade", "websocket")); + TH_EXPECT(th_buf_has_header(conn.written, conn.written_len, "Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")); + // conn is handed off to th_ws, not destroyed by th_http. + TH_EXPECT(!conn.destroyed); + + // Drives the send completion (creates+starts th_ws) and then + // th_ws_start's recv, which sees the fully-consumed request as + // EOF and cleans up. + th_fake_conn_run(&conn); + th_fake_conn_run(&conn); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(http_rejects_non_handshake_request_to_ws_route) + { + th_fake_conn_set_request(&conn, TH_STR("GET /ws HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")); + + th_conn_upgrader_upgrade(&upgrader.base, &conn.base); + while (!conn.destroyed && conn.callback != NULL) + th_fake_conn_run(&conn); + + TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 426 Upgrade Required\r\n")); + TH_EXPECT(th_buf_has_header(conn.written, conn.written_len, "Upgrade", "websocket")); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END th_router_deinit(&router); th_conn_tracker_deinit(&tracker); diff --git a/src/th_request_parser.c b/src/th_request_parser.c index c4423f1..eec0397 100644 --- a/src/th_request_parser.c +++ b/src/th_request_parser.c @@ -291,7 +291,7 @@ th_request_parse_handle_header(th_request_parser* parser, th_request* request, t } else if (th_str_eq(value, TH_STR("keep-alive"))) { request->close = false; } - return TH_ERR_OK; + break; case TH_HEADER_ID_CONTENT_TYPE: if (th_str_eq(value, TH_STR("application/x-www-form-urlencoded"))) { parser->body_encoding = TH_REQUEST_BODY_ENCODING_FORM_URL_ENCODED; diff --git a/src/th_request_parser_test.c b/src/th_request_parser_test.c index 3f70758..f80c84d 100644 --- a/src/th_request_parser_test.c +++ b/src/th_request_parser_test.c @@ -23,6 +23,17 @@ TH_TEST_BEGIN(request_parser) TH_EXPECT(TH_STR_EQ(request.body, "")); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(parse_connection_header_is_stored_verbatim) + { + // th_request_close is derived from this header, but the raw value + // must still be stored and retrievable, whatever it is. + th_str data = TH_STR("GET /test HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\n\r\n"); + size_t parsed = 0; + TH_EXPECT(th_request_parser_parse(&parser, &request, data, &parsed) == TH_ERR_OK); + TH_EXPECT(TH_STR_EQ(th_request_get_header(&request, TH_STR("connection")), "Upgrade")); + TH_EXPECT(request.close == false); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(parse_path_and_query) { th_str data = TH_STR("GET /test?key1=value1&key2=value2 HTTP/1.1\r\nHost: example.com\r\n\r\n"); diff --git a/src/th_response.c b/src/th_response.c index 0b2d700..2789b7c 100644 --- a/src/th_response.c +++ b/src/th_response.c @@ -214,7 +214,9 @@ th_response_set_default_headers(th_response* response) { th_err err = TH_ERR_OK; char buffer[256]; - if (response->is_file) { + if (response->code == TH_CODE_SWITCHING_PROTOCOLS) { + // No body, and RFC 7230 forbids Content-Length framing here. + } else if (response->is_file) { size_t len = 0; const char* content_len = th_fmt_uint_to_str_ex(buffer, sizeof(buffer), (unsigned int)response->file_len, &len); if ((err = th_response_add_header(response, TH_STR("Content-Length"), th_str_make(content_len, len))) != TH_ERR_OK) diff --git a/src/th_router.c b/src/th_router.c index 93feb01..a9c61c4 100644 --- a/src/th_router.c +++ b/src/th_router.c @@ -309,6 +309,15 @@ th_router_find_or_create_segment(th_router* router, th_str path, th_route_segmen return TH_ERR_OK; } +TH_LOCAL(th_err) +th_router_ws_default_handler(void* user_data, const th_request* request, th_response* response) +{ + (void)user_data; + (void)request; + (void)response; + return TH_ERR_HTTP(TH_CODE_SWITCHING_PROTOCOLS); +} + TH_PRIVATE(th_err) th_router_add_route(th_router* router, th_method method, th_str path, th_handler handler, void* user_data) { @@ -316,8 +325,9 @@ th_router_add_route(th_router* router, th_method method, th_str path, th_handler th_err err = TH_ERR_OK; if ((err = th_router_find_or_create_segment(router, path, &route)) != TH_ERR_OK) return err; + bool is_ws_default = route->handler[method].handler == th_router_ws_default_handler; if (route->handler[TH_METHOD_ANY].handler != NULL - || route->handler[method].handler != NULL) + || (route->handler[method].handler != NULL && !is_ws_default)) return TH_ERR_INVALID_ARG; // Route already exists route->handler[method].handler = handler; route->handler[method].user_data = user_data; @@ -335,5 +345,11 @@ th_router_add_ws_route(th_router* router, th_str path, th_ws_handler handler, vo return TH_ERR_INVALID_ARG; // WS route already exists route->ws_handler.handler = handler; route->ws_handler.user_data = user_data; + // No gating th_route registered for this path/method yet: default to + // allowing the upgrade, so a WS-only route doesn't 405. + if (route->handler[TH_METHOD_GET].handler == NULL && route->handler[TH_METHOD_ANY].handler == NULL) { + route->handler[TH_METHOD_GET].handler = th_router_ws_default_handler; + route->handler[TH_METHOD_GET].user_data = NULL; + } return TH_ERR_OK; } diff --git a/src/th_router.h b/src/th_router.h index 18c3a34..a6600cd 100644 --- a/src/th_router.h +++ b/src/th_router.h @@ -20,9 +20,7 @@ typedef struct th_capture { } th_capture; /** th_router_capture_cb - * @brief Called by th_router_resolve for each capture ({name}, {int:name}, - * {path:name}) discovered while resolving a path. NULL means don't report - * captures (a dry run). + * @brief Called per capture found while resolving a path. NULL = dry run. */ typedef void (*th_router_capture_cb)(void* userp, th_str key, th_str value); diff --git a/src/th_router_test.c b/src/th_router_test.c index 0d7818c..51bb69a 100644 --- a/src/th_router_test.c +++ b/src/th_router_test.c @@ -423,5 +423,36 @@ TH_TEST_BEGIN(router) th_router_deinit(&router); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_ws_only_route_returns_switching_protocols_instead_of_405) + { + th_router router; + th_router_init(&router, NULL); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), noop_ws_handler, NULL) == TH_ERR_OK); + th_request request = {0}; + th_request_init(&request, NULL); + request.method = TH_METHOD_GET; + th_string_set(&request.uri_path, TH_STR("/ws")); + th_response response = {0}; + TH_EXPECT(th_router_handle(&router, &request, &response) == TH_ERR_HTTP(TH_CODE_SWITCHING_PROTOCOLS)); + th_request_deinit(&request); + th_router_deinit(&router); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(router_add_route_after_ws_route_overrides_default_gate) + { + th_router router; + th_router_init(&router, NULL); + TH_EXPECT(th_router_add_ws_route(&router, TH_STR("/ws"), noop_ws_handler, NULL) == TH_ERR_OK); + TH_EXPECT(th_router_add_route(&router, TH_METHOD_GET, TH_STR("/ws"), expect_pathvars_handler, NULL) == TH_ERR_OK); + th_request request = {0}; + th_request_init(&request, NULL); + request.method = TH_METHOD_GET; + th_string_set(&request.uri_path, TH_STR("/ws")); + th_response response = {0}; + TH_EXPECT(th_router_handle(&router, &request, &response) == TH_ERR_OK); + th_request_deinit(&request); + th_router_deinit(&router); + } + TH_TEST_CASE_END } TH_TEST_END diff --git a/src/th_server.c b/src/th_server.c index b8d1bc8..090405c 100644 --- a/src/th_server.c +++ b/src/th_server.c @@ -93,6 +93,12 @@ th_server_route(th_server* server, th_method method, const char* path, th_handle return th_router_add_route(&server->router, method, th_str_from_cstr(path), handler, user_data); } +TH_LOCAL(th_err) +th_server_route_ws(th_server* server, const char* path, th_ws_handler handler, void* user_data) +{ + return th_router_add_ws_route(&server->router, th_str_from_cstr(path), handler, user_data); +} + TH_LOCAL(th_err) th_server_add_dir(th_server* server, const char* name, const char* path) { @@ -179,6 +185,12 @@ th_route(th_server* server, th_method method, const char* route, th_handler hand return th_server_route(server, method, route, handler, userp); } +TH_PUBLIC(th_err) +th_route_ws(th_server* server, const char* path, th_ws_handler handler, void* userp) +{ + return th_server_route_ws(server, path, handler, userp); +} + TH_PUBLIC(th_err) th_add_dir(th_server* server, const char* name, const char* path) { diff --git a/src/th_sha1.c b/src/th_sha1.c new file mode 100644 index 0000000..f2f0f41 --- /dev/null +++ b/src/th_sha1.c @@ -0,0 +1,86 @@ +#include "th_sha1.h" + +#include +#include + +TH_LOCAL(uint32_t) +th_sha1_rotl(uint32_t x, int n) +{ + return (x << n) | (x >> (32 - n)); +} + +TH_LOCAL(void) +th_sha1_process_block(uint32_t state[5], const unsigned char block[64]) +{ + uint32_t w[80]; + for (int i = 0; i < 16; ++i) { + w[i] = ((uint32_t)block[i * 4] << 24) | ((uint32_t)block[i * 4 + 1] << 16) + | ((uint32_t)block[i * 4 + 2] << 8) | (uint32_t)block[i * 4 + 3]; + } + for (int i = 16; i < 80; ++i) { + w[i] = th_sha1_rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + uint32_t a = state[0], b = state[1], c = state[2], d = state[3], e = state[4]; + for (int i = 0; i < 80; ++i) { + uint32_t f, k; + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + uint32_t temp = th_sha1_rotl(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = th_sha1_rotl(b, 30); + b = a; + a = temp; + } + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; +} + +TH_PRIVATE(void) +th_sha1(th_buffer data, unsigned char digest[TH_SHA1_DIGEST_LEN]) +{ + uint32_t state[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u}; + const unsigned char* bytes = (const unsigned char*)data.ptr; + size_t len = data.len; + size_t full_blocks = len / 64; + for (size_t i = 0; i < full_blocks; ++i) { + th_sha1_process_block(state, bytes + i * 64); + } + + unsigned char tail[128] = {0}; + size_t tail_len = len - full_blocks * 64; + memcpy(tail, bytes + full_blocks * 64, tail_len); + tail[tail_len] = 0x80; + size_t padded_len = tail_len < 56 ? 64 : 128; + uint64_t bit_len = (uint64_t)len * 8; + for (size_t i = 0; i < 8; ++i) { + tail[padded_len - 1 - i] = (unsigned char)(bit_len >> (8 * i)); + } + th_sha1_process_block(state, tail); + if (padded_len == 128) { + th_sha1_process_block(state, tail + 64); + } + + for (int i = 0; i < 5; ++i) { + digest[i * 4] = (unsigned char)(state[i] >> 24); + digest[i * 4 + 1] = (unsigned char)(state[i] >> 16); + digest[i * 4 + 2] = (unsigned char)(state[i] >> 8); + digest[i * 4 + 3] = (unsigned char)state[i]; + } +} diff --git a/src/th_sha1.h b/src/th_sha1.h new file mode 100644 index 0000000..daa82eb --- /dev/null +++ b/src/th_sha1.h @@ -0,0 +1,16 @@ +#ifndef TH_SHA1_H +#define TH_SHA1_H + +#include + +#include "th_config.h" + +#define TH_SHA1_DIGEST_LEN 20 + +/** th_sha1 + * @brief Computes the SHA-1 digest of data into digest[TH_SHA1_DIGEST_LEN]. + */ +TH_PRIVATE(void) +th_sha1(th_buffer data, unsigned char digest[TH_SHA1_DIGEST_LEN]); + +#endif diff --git a/src/th_sha1_test.c b/src/th_sha1_test.c new file mode 100644 index 0000000..e971b77 --- /dev/null +++ b/src/th_sha1_test.c @@ -0,0 +1,51 @@ +#include "th_sha1.h" +#include "th_test.h" + +#include + +static bool +digest_hex_eq(const unsigned char digest[TH_SHA1_DIGEST_LEN], const char* hex) +{ + char buf[TH_SHA1_DIGEST_LEN * 2 + 1]; + for (size_t i = 0; i < TH_SHA1_DIGEST_LEN; i++) + snprintf(buf + i * 2, 3, "%02x", digest[i]); + return strcmp(buf, hex) == 0; +} + +TH_TEST_BEGIN(sha1) +{ + TH_TEST_CASE_BEGIN(sha1_empty) + { + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){"", 0}, digest); + TH_EXPECT(digest_hex_eq(digest, "da39a3ee5e6b4b0d3255bfef95601890afd80709")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(sha1_short_single_block) + { + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){"abc", 3}, digest); + TH_EXPECT(digest_hex_eq(digest, "a9993e364706816aba3e25717850c26c9cd0d89d")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(sha1_message_spanning_two_blocks) + { + // 56 bytes: no room for padding in the first block. + const char* msg = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){msg, strlen(msg)}, digest); + TH_EXPECT(digest_hex_eq(digest, "84983e441c3bd26ebaae4aa1f95129e5e54670f1")); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(sha1_exactly_one_block) + { + // Full block, padding must spill into a second block. + char msg[64]; + memset(msg, 'a', sizeof(msg)); + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){msg, sizeof(msg)}, digest); + TH_EXPECT(digest_hex_eq(digest, "0098ba824b5c16427bd7a1122a5a442a25ec644d")); + } + TH_TEST_CASE_END +} +TH_TEST_END diff --git a/src/th_str.c b/src/th_str.c index f58d734..3cfb255 100644 --- a/src/th_str.c +++ b/src/th_str.c @@ -1,5 +1,6 @@ #include +#include #include #include @@ -41,6 +42,19 @@ th_str_eq(th_str a, th_str b) return memcmp(a.ptr, b.ptr, a.len) == 0; } +TH_PRIVATE(bool) +th_str_ieq(th_str a, th_str b) +{ + if (a.len != b.len) { + return 0; + } + for (size_t i = 0; i < a.len; i++) { + if (tolower((unsigned char)a.ptr[i]) != tolower((unsigned char)b.ptr[i])) + return 0; + } + return 1; +} + TH_PRIVATE(size_t) th_str_find_first(th_str str, size_t start, char c) { diff --git a/src/th_str.h b/src/th_str.h index be2616a..dc9be6e 100644 --- a/src/th_str.h +++ b/src/th_str.h @@ -49,6 +49,13 @@ th_str_from_cstr(const char* str) TH_PRIVATE(bool) th_str_eq(th_str a, th_str b); +/** th_str_ieq + * @brief Case-insensitive version of th_str_eq. + * @return 1 if the strings are equal ignoring case, 0 otherwise. + */ +TH_PRIVATE(bool) +th_str_ieq(th_str a, th_str b); + /** th_str_empty * @brief Helper function to check if a th_str is empty. * @return true if the string is empty, false otherwise. diff --git a/src/th_ws.c b/src/th_ws.c new file mode 100644 index 0000000..61455f3 --- /dev/null +++ b/src/th_ws.c @@ -0,0 +1,87 @@ +#include "th_ws.h" + +#include "th_log.h" + +#undef TH_LOG_TAG +#define TH_LOG_TAG "ws" + +TH_PRIVATE(void) +th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator) +{ + ws->conn = conn; + ws->handler = handler; + ws->user_data = user_data; + ws->allocator = allocator ? allocator : th_default_allocator_get(); +} + +TH_PRIVATE(void) +th_ws_deinit(th_ws* ws) +{ + th_conn_destroy(ws->conn); +} + +TH_PRIVATE(th_err) +th_ws_create(th_ws** out, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator) +{ + allocator = allocator ? allocator : th_default_allocator_get(); + th_ws* ws = th_allocator_alloc(allocator, sizeof(th_ws)); + if (!ws) + return TH_ERR_BAD_ALLOC; + th_ws_init(ws, conn, handler, user_data, allocator); + *out = ws; + return TH_ERR_OK; +} + +TH_LOCAL(void) +th_ws_destroy(th_ws* ws) +{ + th_allocator* allocator = ws->allocator; + th_ws_deinit(ws); + th_allocator_free(allocator, ws); +} + +TH_LOCAL(void) +th_ws_handle_recv(void* user_data, size_t len, th_err err) +{ + th_ws* ws = user_data; + (void)len; + // Frame parsing isn't implemented yet: any successfully received + // bytes are discarded, we only care about detecting EOF/errors. + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Connection closed: %s", (void*)ws, th_strerror(err)); + (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}); + th_ws_destroy(ws); + return; + } + th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); +} + +TH_PRIVATE(void) +th_ws_start(th_ws* ws) +{ + th_err err = ws->handler(ws->user_data, ws, TH_WS_EVENT_OPEN, (th_buffer){0}); + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: OPEN handler returned %s, closing", (void*)ws, th_strerror(err)); + th_ws_destroy(ws); + return; + } + th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); +} + +TH_PUBLIC(th_err) +th_ws_send(th_ws* ws, th_buffer data, th_ws_msg_type type) +{ + (void)ws; + (void)data; + (void)type; + TH_LOG_ERROR("WebSocket frame sending is not implemented yet."); + return TH_ERR_NOSUPPORT; +} + +TH_PUBLIC(th_err) +th_ws_close(th_ws* ws) +{ + (void)ws; + TH_LOG_ERROR("WebSocket close handshake is not implemented yet."); + return TH_ERR_NOSUPPORT; +} diff --git a/src/th_ws.h b/src/th_ws.h new file mode 100644 index 0000000..f6815c1 --- /dev/null +++ b/src/th_ws.h @@ -0,0 +1,35 @@ +#ifndef TH_WS_H +#define TH_WS_H + +#include + +#include "th_conn.h" + +// Frame parsing isn't implemented yet; received bytes are discarded here. +#define TH_WS_SCRATCH_RECV_LEN 8192 + +struct th_ws { + th_conn* conn; + th_ws_handler handler; + void* user_data; + th_allocator* allocator; + char scratch[TH_WS_SCRATCH_RECV_LEN]; +}; + +TH_PRIVATE(void) +th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator); + +TH_PRIVATE(void) +th_ws_deinit(th_ws* ws); + +TH_PRIVATE(th_err) +th_ws_create(th_ws** out, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator); + +/** th_ws_start + * @brief Fires TH_WS_EVENT_OPEN. If the handler returns an error, the + * connection is torn down immediately without ever reading a frame. + */ +TH_PRIVATE(void) +th_ws_start(th_ws* ws); + +#endif diff --git a/src/th_ws_handshake.c b/src/th_ws_handshake.c new file mode 100644 index 0000000..782acef --- /dev/null +++ b/src/th_ws_handshake.c @@ -0,0 +1,60 @@ +#include "th_ws_handshake.h" + +#include "th_base64.h" +#include "th_sha1.h" + +#include + +// Max length of a base64-encoded 16-byte nonce, per RFC 6455. +#define TH_WS_HANDSHAKE_KEY_MAX_LEN 24 +#define TH_WS_HANDSHAKE_GUID_LEN 36 +#define TH_WS_HANDSHAKE_GUID TH_STR("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +TH_LOCAL(bool) +th_ws_connection_has_upgrade_token(th_str value) +{ + size_t pos = 0; + while (pos <= value.len) { + size_t comma = th_str_find_first(value, pos, ','); + size_t end = comma == th_str_npos ? value.len : comma; + th_str token = th_str_trim(th_str_substr(value, pos, end - pos)); + if (th_str_ieq(token, TH_STR("upgrade"))) + return true; + if (comma == th_str_npos) + break; + pos = comma + 1; + } + return false; +} + +TH_PRIVATE(bool) +th_ws_is_handshake(th_request* request) +{ + if (request->method != TH_METHOD_GET) + return false; + if (!th_str_ieq(th_request_get_header(request, TH_STR("upgrade")), TH_STR("websocket"))) + return false; + if (!th_ws_connection_has_upgrade_token(th_request_get_header(request, TH_STR("connection")))) + return false; + if (th_str_empty(th_request_get_header(request, TH_STR("sec-websocket-key")))) + return false; + if (!th_str_eq(th_request_get_header(request, TH_STR("sec-websocket-version")), TH_STR("13"))) + return false; + return true; +} + +TH_PRIVATE(th_err) +th_ws_handshake_accept_key(th_str key, th_string* out) +{ + if (key.len > TH_WS_HANDSHAKE_KEY_MAX_LEN) + return TH_ERR_INVALID_ARG; + th_str guid = TH_WS_HANDSHAKE_GUID; + char concat[TH_WS_HANDSHAKE_KEY_MAX_LEN + TH_WS_HANDSHAKE_GUID_LEN]; + memcpy(concat, key.ptr, key.len); + memcpy(concat + key.len, guid.ptr, guid.len); + + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){concat, key.len + guid.len}, digest); + + return th_base64_encode(th_str_make((const char*)digest, TH_SHA1_DIGEST_LEN), out); +} diff --git a/src/th_ws_handshake.h b/src/th_ws_handshake.h new file mode 100644 index 0000000..0807825 --- /dev/null +++ b/src/th_ws_handshake.h @@ -0,0 +1,24 @@ +#ifndef TH_WS_HANDSHAKE_H +#define TH_WS_HANDSHAKE_H + +#include + +#include "th_config.h" +#include "th_request.h" +#include "th_str.h" +#include "th_string.h" + +/** th_ws_is_handshake + * @brief Checks whether request is a valid RFC 6455 upgrade request. + */ +TH_PRIVATE(bool) +th_ws_is_handshake(th_request* request); + +/** th_ws_handshake_accept_key + * @brief Computes Sec-WebSocket-Accept for a Sec-WebSocket-Key value. + * @return TH_ERR_INVALID_ARG if key is too long, TH_ERR_OK otherwise. + */ +TH_PRIVATE(th_err) +th_ws_handshake_accept_key(th_str key, th_string* out); + +#endif diff --git a/src/th_ws_handshake_test.c b/src/th_ws_handshake_test.c new file mode 100644 index 0000000..b4c262f --- /dev/null +++ b/src/th_ws_handshake_test.c @@ -0,0 +1,112 @@ +#include "th_request.h" +#include "th_test.h" +#include "th_ws_handshake.h" + +#include + +TH_TEST_BEGIN(ws_handshake) +{ + th_request request; + th_request_init(&request, th_default_allocator_get()); + + TH_TEST_CASE_BEGIN(ws_handshake_accept_key_matches_rfc6455_example) + { + th_string out; + th_string_init(&out, th_default_allocator_get()); + TH_EXPECT(th_ws_handshake_accept_key(TH_STR("dGhlIHNhbXBsZSBub25jZQ=="), &out) == TH_ERR_OK); + TH_EXPECT(th_str_eq(th_string_view(&out), TH_STR("s3pPLMBiTxaQ9kYGzzhZRbK+xOo="))); + th_string_deinit(&out); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_handshake_accept_key_rejects_oversized_key) + { + char oversized[64]; + memset(oversized, 'a', sizeof(oversized)); + th_string out; + th_string_init(&out, th_default_allocator_get()); + TH_EXPECT(th_ws_handshake_accept_key(th_str_make(oversized, sizeof(oversized)), &out) == TH_ERR_INVALID_ARG); + th_string_deinit(&out); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_accepts_valid_request) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("websocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("keep-alive, Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_accepts_case_insensitive_upgrade_value) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("WebSocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_non_get_method) + { + request.method = TH_METHOD_POST; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("websocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_missing_upgrade_header) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_wrong_upgrade_value) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("h2c")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_connection_header_without_upgrade_token) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("websocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("keep-alive")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_missing_key) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("websocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("13")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_is_handshake_rejects_wrong_version) + { + request.method = TH_METHOD_GET; + TH_EXPECT(th_request_add_header(&request, TH_STR("upgrade"), TH_STR("websocket")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("connection"), TH_STR("Upgrade")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-key"), TH_STR("dGhlIHNhbXBsZSBub25jZQ==")) == TH_ERR_OK); + TH_EXPECT(th_request_add_header(&request, TH_STR("sec-websocket-version"), TH_STR("8")) == TH_ERR_OK); + TH_EXPECT(!th_ws_is_handshake(&request)); + } + TH_TEST_CASE_END + + th_request_deinit(&request); +} +TH_TEST_END diff --git a/src/th_ws_test.c b/src/th_ws_test.c new file mode 100644 index 0000000..60be194 --- /dev/null +++ b/src/th_ws_test.c @@ -0,0 +1,205 @@ +#include "th_system_error.h" +#include "th_test.h" +#include "th_ws.h" + +typedef struct th_fake_conn { + th_conn base; + bool destroyed; + void (*callback)(void* user_data, size_t size, th_err err); + void* user_data; + th_err next_recv_err; +} th_fake_conn; + +static th_address* +th_fake_conn_get_address(void* self) +{ + (void)self; + return NULL; +} + +static void +th_fake_conn_start(void* self) +{ + (void)self; +} + +static void +th_fake_conn_recv(void* self, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data) +{ + (void)addr; + (void)len; + (void)exact; + th_fake_conn* conn = self; + TH_ASSERT(conn->callback == NULL); + conn->callback = callback; + conn->user_data = user_data; +} + +static void +th_fake_conn_send(void* self, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) +{ + (void)self; + (void)iov; + (void)iovcnt; + (void)file; + (void)offset; + (void)len; + (void)callback; + (void)user_data; + TH_ASSERT(0 && "not expected to be called in this slice"); +} + +static void +th_fake_conn_cancel(void* self) +{ + (void)self; +} + +static void +th_fake_conn_destroy(void* self) +{ + th_fake_conn* conn = self; + conn->destroyed = true; +} + +static const th_conn_methods th_fake_conn_methods = { + .get_address = th_fake_conn_get_address, + .start = th_fake_conn_start, + .recv = th_fake_conn_recv, + .send = th_fake_conn_send, + .cancel = th_fake_conn_cancel, + .destroy = th_fake_conn_destroy, +}; + +static void +th_fake_conn_init(th_fake_conn* conn) +{ + conn->base.methods = &th_fake_conn_methods; + conn->destroyed = false; + conn->callback = NULL; + conn->user_data = NULL; + conn->next_recv_err = TH_ERR_EOF; +} + +static void +th_fake_conn_run(th_fake_conn* conn) +{ + TH_ASSERT(conn->callback != NULL); + void (*callback)(void*, size_t, th_err) = conn->callback; + void* user_data = conn->user_data; + conn->callback = NULL; + conn->user_data = NULL; + callback(user_data, 0, conn->next_recv_err); +} + +struct handler_calls { + int open_count; + int close_count; + th_err return_on_open; +}; + +static th_err +th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +{ + (void)ws; + (void)data; + struct handler_calls* calls = userp; + switch (ev) { + case TH_WS_EVENT_OPEN: + calls->open_count++; + return calls->return_on_open; + case TH_WS_EVENT_CLOSE: + calls->close_count++; + return TH_ERR_OK; + default: + return TH_ERR_OK; + } +} + +TH_TEST_BEGIN(ws) +{ + th_fake_conn conn; + th_fake_conn_init(&conn); + struct handler_calls calls = {0, 0, TH_ERR_OK}; + + TH_TEST_CASE_BEGIN(ws_start_fires_open) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + TH_EXPECT(calls.open_count == 1); + TH_EXPECT(calls.close_count == 0); + TH_EXPECT(!conn.destroyed); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_open_error_closes_without_recv) + { + calls.return_on_open = TH_ERR_INVALID_ARG; + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + TH_EXPECT(calls.open_count == 1); + TH_EXPECT(calls.close_count == 0); // CLOSE isn't fired for a handler-rejected OPEN + TH_EXPECT(conn.destroyed); + TH_EXPECT(conn.callback == NULL); // never issued a recv + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_error_fires_close_and_destroys) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + conn.next_recv_err = TH_ERR_SYSTEM(TH_EIO); + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_success_keeps_reading) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + conn.next_recv_err = TH_ERR_OK; + th_fake_conn_run(&conn); // successful recv, discarded, issues another recv + TH_EXPECT(calls.close_count == 0); + TH_EXPECT(!conn.destroyed); + TH_EXPECT(conn.callback != NULL); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_returns_nosupport) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_MSG_TEXT) == TH_ERR_NOSUPPORT); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_close_returns_nosupport) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + TH_EXPECT(th_ws_close(ws) == TH_ERR_NOSUPPORT); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END +} +TH_TEST_END From 788dd1bba6046e35310f33e6254bf221d9de78da Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 13:20:59 -0500 Subject: [PATCH 04/13] feat: implement WebSocket frame receiving - th_ws_frame_parser: incremental RFC 6455 frame parser, mirrors th_request_parser's switch-based per-state dispatch - th_ws now accumulates message payloads and dispatches DATA/CLOSE events; ping/pong are discarded without reaching the handler - add TH_EPROTO system error code for protocol violations - fix: th_route_init left ws_handler uninitialized, causing th_router_add_ws_route to spuriously fail on a fresh route Sending frames is not implemented yet (th_ws_send/th_ws_close remain TH_ERR_NOSUPPORT stubs). --- CMakeLists.txt | 2 + src/th_config.h | 4 + src/th_router.c | 1 + src/th_system_error.h | 2 + src/th_ws.c | 56 +++++++- src/th_ws.h | 5 +- src/th_ws_frame.h | 15 +++ src/th_ws_frame_parser.c | 207 ++++++++++++++++++++++++++++ src/th_ws_frame_parser.h | 48 +++++++ src/th_ws_frame_parser_test.c | 244 ++++++++++++++++++++++++++++++++++ src/th_ws_test.c | 105 ++++++++++++++- 11 files changed, 681 insertions(+), 8 deletions(-) create mode 100644 src/th_ws_frame.h create mode 100644 src/th_ws_frame_parser.c create mode 100644 src/th_ws_frame_parser.h create mode 100644 src/th_ws_frame_parser_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 86af4df..9cfaf0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ SET(TH_CORE_SRC src/th_sha1.c src/th_base64.c src/th_ws_handshake.c + src/th_ws_frame_parser.c src/th_ws.c # SSL (compiled out via TH_WITH_SSL=0 when OpenSSL is not found) src/th_ssl_smem_bio.c @@ -243,6 +244,7 @@ if (NOT TH_DISABLE_TESTS) src/th_sha1_test.c src/th_base64_test.c src/th_ws_handshake_test.c + src/th_ws_frame_parser_test.c src/th_response_test.c src/th_http_test.c src/th_ws_test.c diff --git a/src/th_config.h b/src/th_config.h index 146e03c..c2c974d 100644 --- a/src/th_config.h +++ b/src/th_config.h @@ -30,6 +30,10 @@ #define TH_MAX_BODY_LEN (4 * 1024 * 1024) #endif +#ifndef TH_CONFIG_WS_MAX_MESSAGE_LEN +#define TH_CONFIG_WS_MAX_MESSAGE_LEN (4 * 1024 * 1024) +#endif + /* feature configuration end */ #if defined(__APPLE__) diff --git a/src/th_router.c b/src/th_router.c index a9c61c4..9bc8064 100644 --- a/src/th_router.c +++ b/src/th_router.c @@ -23,6 +23,7 @@ th_route_init(th_route_segment* route, th_capture_type type, th_str segment, th_ route->allocator = allocator; for (size_t i = 0; i < TH_METHOD_MAX; ++i) route->handler[i] = (th_route_handler){NULL, NULL}; + route->ws_handler = (th_ws_route_handler){NULL, NULL}; return TH_ERR_OK; } diff --git a/src/th_system_error.h b/src/th_system_error.h index cc2a79c..6ee53d6 100644 --- a/src/th_system_error.h +++ b/src/th_system_error.h @@ -38,6 +38,7 @@ th_system_strerror(int errc) #define TH_ENOSYS ENOSYS #define TH_ETIMEDOUT ETIMEDOUT #define TH_ECANCELED ECANCELED +#define TH_EPROTO EPROTO #elif defined(TH_CONFIG_OS_WIN) #define TH_ENOENT ERROR_FILE_NOT_FOUND #define TH_EINTR ERROR_INTERRUPT @@ -50,6 +51,7 @@ th_system_strerror(int errc) #define TH_ENOSYS ERROR_NOT_SUPPORTED #define TH_ETIMEDOUT ERROR_TIMEOUT #define TH_ECANCELED ERROR_CANCELLED +#define TH_EPROTO ERROR_INVALID_DATA #endif #endif diff --git a/src/th_ws.c b/src/th_ws.c index 61455f3..615349f 100644 --- a/src/th_ws.c +++ b/src/th_ws.c @@ -1,6 +1,7 @@ #include "th_ws.h" #include "th_log.h" +#include "th_system_error.h" #undef TH_LOG_TAG #define TH_LOG_TAG "ws" @@ -12,11 +13,14 @@ th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_ ws->handler = handler; ws->user_data = user_data; ws->allocator = allocator ? allocator : th_default_allocator_get(); + ws->parser = (th_ws_frame_parser){0}; + th_buf_vec_init(&ws->payload, ws->allocator); } TH_PRIVATE(void) th_ws_deinit(th_ws* ws) { + th_buf_vec_deinit(&ws->payload); th_conn_destroy(ws->conn); } @@ -40,17 +44,59 @@ th_ws_destroy(th_ws* ws) th_allocator_free(allocator, ws); } +TH_LOCAL(void) +th_ws_close_and_destroy(th_ws* ws) +{ + (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}); + th_ws_destroy(ws); +} + +TH_LOCAL(void) +th_ws_handle_recv(void* user_data, size_t len, th_err err); + +TH_LOCAL(bool) +th_ws_consume(th_ws* ws, char* data, size_t len) +{ + while (len > 0) { + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&ws->parser, data, len, &ws->payload, &parsed, &type); + data += parsed; + len -= parsed; + if (err == TH_ERR_SYSTEM(TH_EAGAIN)) + return true; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Invalid frame: %s", (void*)ws, th_strerror(err)); + return false; + } + + if (type == TH_WS_FRAME_CLOSE) + return false; + if (type == TH_WS_FRAME_PING || type == TH_WS_FRAME_PONG) + continue; + + th_buffer message = {th_buf_vec_begin(&ws->payload), th_buf_vec_size(&ws->payload)}; + err = ws->handler(ws->user_data, ws, TH_WS_EVENT_DATA, message); + th_buf_vec_clear(&ws->payload); + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: DATA handler returned %s, closing", (void*)ws, th_strerror(err)); + return false; + } + } + return true; +} + TH_LOCAL(void) th_ws_handle_recv(void* user_data, size_t len, th_err err) { th_ws* ws = user_data; - (void)len; - // Frame parsing isn't implemented yet: any successfully received - // bytes are discarded, we only care about detecting EOF/errors. if (err != TH_ERR_OK) { TH_LOG_DEBUG("%p: Connection closed: %s", (void*)ws, th_strerror(err)); - (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}); - th_ws_destroy(ws); + th_ws_close_and_destroy(ws); + return; + } + if (!th_ws_consume(ws, ws->scratch, len)) { + th_ws_close_and_destroy(ws); return; } th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); diff --git a/src/th_ws.h b/src/th_ws.h index f6815c1..3372aeb 100644 --- a/src/th_ws.h +++ b/src/th_ws.h @@ -4,8 +4,9 @@ #include #include "th_conn.h" +#include "th_vec.h" +#include "th_ws_frame_parser.h" -// Frame parsing isn't implemented yet; received bytes are discarded here. #define TH_WS_SCRATCH_RECV_LEN 8192 struct th_ws { @@ -13,6 +14,8 @@ struct th_ws { th_ws_handler handler; void* user_data; th_allocator* allocator; + th_ws_frame_parser parser; + th_buf_vec payload; // accumulates a message's payload across fragments/calls char scratch[TH_WS_SCRATCH_RECV_LEN]; }; diff --git a/src/th_ws_frame.h b/src/th_ws_frame.h new file mode 100644 index 0000000..a4249c0 --- /dev/null +++ b/src/th_ws_frame.h @@ -0,0 +1,15 @@ +#ifndef TH_WS_FRAME_H +#define TH_WS_FRAME_H + +#include + +#include "th_config.h" + +typedef enum th_ws_frame_type { + TH_WS_FRAME_DATA, + TH_WS_FRAME_PING, + TH_WS_FRAME_PONG, + TH_WS_FRAME_CLOSE, +} th_ws_frame_type; + +#endif diff --git a/src/th_ws_frame_parser.c b/src/th_ws_frame_parser.c new file mode 100644 index 0000000..43237b1 --- /dev/null +++ b/src/th_ws_frame_parser.c @@ -0,0 +1,207 @@ +#include "th_ws_frame_parser.h" + +#include "th_system_error.h" + +#include + +#define TH_WS_OPCODE_CONTINUATION 0x0 +#define TH_WS_OPCODE_TEXT 0x1 +#define TH_WS_OPCODE_BINARY 0x2 +#define TH_WS_OPCODE_CLOSE 0x8 +#define TH_WS_OPCODE_PING 0x9 +#define TH_WS_OPCODE_PONG 0xA + +TH_LOCAL(bool) +th_ws_opcode_is_control(unsigned char opcode) +{ + return opcode >= TH_WS_OPCODE_CLOSE; +} + +TH_LOCAL(size_t) +th_ws_frame_parser_fill_header(th_ws_frame_parser* parser, const char* data, size_t len, size_t needed) +{ + size_t remaining = needed - parser->header_len; + size_t n = len < remaining ? len : remaining; + memcpy(parser->header_buf + parser->header_len, data, n); + parser->header_len += n; + return n; +} + +TH_LOCAL(th_err) +th_ws_frame_parser_validate_base_header(th_ws_frame_parser* parser, size_t* header_len) +{ + unsigned char byte0 = parser->header_buf[0]; + unsigned char byte1 = parser->header_buf[1]; + if ((byte0 & 0x70) != 0) // RSV1-3 must be 0, no extensions negotiated + return TH_ERR_SYSTEM(TH_EPROTO); + if ((byte1 & 0x80) == 0) // client frames must be masked + return TH_ERR_SYSTEM(TH_EPROTO); + + unsigned char opcode = byte0 & 0x0F; + bool fin = (byte0 & 0x80) != 0; + unsigned char len7 = byte1 & 0x7F; + + bool known_opcode = opcode == TH_WS_OPCODE_CONTINUATION || opcode == TH_WS_OPCODE_TEXT + || opcode == TH_WS_OPCODE_BINARY || opcode == TH_WS_OPCODE_CLOSE || opcode == TH_WS_OPCODE_PING + || opcode == TH_WS_OPCODE_PONG; + if (!known_opcode) + return TH_ERR_SYSTEM(TH_EPROTO); + if (th_ws_opcode_is_control(opcode) && !fin) // control frames can't be fragmented + return TH_ERR_SYSTEM(TH_EPROTO); + if (th_ws_opcode_is_control(opcode) && len7 > 125) // RFC 6455 5.5 + return TH_ERR_SYSTEM(TH_EPROTO); + if (opcode == TH_WS_OPCODE_CONTINUATION && parser->message_opcode == 0) // nothing to continue + return TH_ERR_SYSTEM(TH_EPROTO); + if ((opcode == TH_WS_OPCODE_TEXT || opcode == TH_WS_OPCODE_BINARY) && parser->message_opcode != 0) + return TH_ERR_SYSTEM(TH_EPROTO); // data frame while a fragmented message is still in progress + + size_t ext_len_bytes = len7 == 126 ? 2 : len7 == 127 ? 8 : 0; + *header_len = 2 + ext_len_bytes + 4; + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_ws_frame_parser_finish_header(th_ws_frame_parser* parser, th_buf_vec* payload) +{ + unsigned char byte1 = parser->header_buf[1]; + unsigned char len7 = byte1 & 0x7F; + size_t ext_len_bytes = len7 == 126 ? 2 : len7 == 127 ? 8 : 0; + + parser->fin = (parser->header_buf[0] & 0x80) != 0; + parser->opcode = parser->header_buf[0] & 0x0F; + + uint64_t payload_len = len7; + if (ext_len_bytes > 0) { + payload_len = 0; + for (size_t i = 0; i < ext_len_bytes; ++i) + payload_len = (payload_len << 8) | parser->header_buf[2 + i]; + } + if (!th_ws_opcode_is_control(parser->opcode) && th_buf_vec_size(payload) + payload_len > TH_CONFIG_WS_MAX_MESSAGE_LEN) + return TH_ERR_SYSTEM(TH_EPROTO); + + memcpy(parser->mask_key, parser->header_buf + 2 + ext_len_bytes, 4); + parser->payload_len = payload_len; + parser->payload_read = 0; + parser->state = TH_WS_FRAME_PARSER_STATE_PAYLOAD; + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_ws_frame_parser_do_header(th_ws_frame_parser* parser, th_buf_vec* payload, const char* data, size_t len, + size_t* parsed) +{ + *parsed = th_ws_frame_parser_fill_header(parser, data, len, 2); + if (parser->header_len < 2) + return TH_ERR_OK; + + size_t header_len = 0; + th_err err = th_ws_frame_parser_validate_base_header(parser, &header_len); + if (err != TH_ERR_OK) + return err; + + *parsed += th_ws_frame_parser_fill_header(parser, data + *parsed, len - *parsed, header_len); + if (parser->header_len < header_len) + return TH_ERR_OK; + + return th_ws_frame_parser_finish_header(parser, payload); +} + +TH_LOCAL(th_err) +th_ws_frame_parser_append_payload(th_buf_vec* payload, const unsigned char* data, size_t len) +{ + if (len == 0) + return TH_ERR_OK; + size_t start = th_buf_vec_size(payload); + th_err err = th_buf_vec_resize(payload, start + len); + if (err != TH_ERR_OK) + return err; + memcpy(th_buf_vec_at(payload, start), data, len); + return TH_ERR_OK; +} + +TH_LOCAL(void) +th_ws_frame_parser_frame_done(th_ws_frame_parser* parser, bool* message_done, th_ws_frame_type* type) +{ + if (th_ws_opcode_is_control(parser->opcode)) { + *type = parser->opcode == TH_WS_OPCODE_CLOSE ? TH_WS_FRAME_CLOSE + : parser->opcode == TH_WS_OPCODE_PING ? TH_WS_FRAME_PING + : TH_WS_FRAME_PONG; + *message_done = true; + } else { + if (parser->opcode != TH_WS_OPCODE_CONTINUATION) + parser->message_opcode = parser->opcode; + if (parser->fin) { + parser->message_opcode = 0; + *type = TH_WS_FRAME_DATA; + *message_done = true; + } + } + parser->state = TH_WS_FRAME_PARSER_STATE_HEADER; + parser->header_len = 0; +} + +// mask_key indexing uses payload_read so it stays correct across chunks. +TH_LOCAL(th_err) +th_ws_frame_parser_do_payload(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + bool* message_done, th_ws_frame_type* type) +{ + uint64_t remaining = parser->payload_len - parser->payload_read; + size_t n = (uint64_t)len < remaining ? len : (size_t)remaining; + *parsed = n; + + for (size_t i = 0; i < n; ++i) + data[i] = (char)((unsigned char)data[i] ^ parser->mask_key[(parser->payload_read + i) % 4]); + + th_err err = TH_ERR_OK; + if (!th_ws_opcode_is_control(parser->opcode)) + err = th_ws_frame_parser_append_payload(payload, (const unsigned char*)data, n); + parser->payload_read += n; + if (err != TH_ERR_OK) + return err; + + *message_done = false; + if (parser->payload_read == parser->payload_len) + th_ws_frame_parser_frame_done(parser, message_done, type); + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_ws_frame_parser_parse_next(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + bool* message_done, th_ws_frame_type* type) +{ + switch (parser->state) { + case TH_WS_FRAME_PARSER_STATE_HEADER: + *message_done = false; + return th_ws_frame_parser_do_header(parser, payload, data, len, parsed); + case TH_WS_FRAME_PARSER_STATE_PAYLOAD: + return th_ws_frame_parser_do_payload(parser, data, len, payload, parsed, message_done, type); + default: + *parsed = 0; + *message_done = false; + return TH_ERR_OK; + } +} + +TH_PRIVATE(th_err) +th_ws_frame_parser_parse(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + th_ws_frame_type* type) +{ + th_err err = TH_ERR_OK; + *parsed = 0; + for (;;) { + size_t p = 0; + bool message_done = false; + if ((err = th_ws_frame_parser_parse_next(parser, data, len, payload, &p, &message_done, type)) != TH_ERR_OK) { + *parsed += p; + return err; + } + data += p; + len -= p; + *parsed += p; + if (message_done) + return TH_ERR_OK; + // check message_done first: a zero-length payload also has p == 0 + if (p == 0 && len == 0) + return TH_ERR_SYSTEM(TH_EAGAIN); + } +} diff --git a/src/th_ws_frame_parser.h b/src/th_ws_frame_parser.h new file mode 100644 index 0000000..9650c96 --- /dev/null +++ b/src/th_ws_frame_parser.h @@ -0,0 +1,48 @@ +#ifndef TH_WS_FRAME_PARSER_H +#define TH_WS_FRAME_PARSER_H + +#include + +#include "th_config.h" +#include "th_vec.h" +#include "th_ws_frame.h" + +#include + +typedef enum th_ws_frame_parser_state { + TH_WS_FRAME_PARSER_STATE_HEADER, + TH_WS_FRAME_PARSER_STATE_PAYLOAD, +} th_ws_frame_parser_state; + +// Largest a header can be: 2 fixed bytes + 8 byte extended length + 4 byte mask key. +#define TH_WS_FRAME_PARSER_HEADER_MAX_LEN 14 + +typedef struct th_ws_frame_parser { + th_ws_frame_parser_state state; + + // Header bytes seen so far, for a header split across recv() calls. + unsigned char header_buf[TH_WS_FRAME_PARSER_HEADER_MAX_LEN]; + size_t header_len; + + // Current frame, once its header is fully parsed. + bool fin; + unsigned char opcode; + unsigned char mask_key[4]; + uint64_t payload_len; + uint64_t payload_read; // bytes of this frame's payload consumed so far + + unsigned char message_opcode; // opcode of a fragmented message still in progress, 0 if none +} th_ws_frame_parser; + +// data must be mutable - payloads are unmasked in place. +// *type is only set when this returns TH_ERR_OK. +// +// - TH_ERR_OK: one full message is in payload (empty for TH_WS_FRAME_CLOSE) +// - TH_ERR_SYSTEM(TH_EAGAIN): need more data; *parsed still reflects bytes +// consumed so far - keep the remainder and retry once more bytes arrive +// - TH_ERR_SYSTEM(TH_EPROTO): protocol violation +TH_PRIVATE(th_err) +th_ws_frame_parser_parse(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + th_ws_frame_type* type); + +#endif diff --git a/src/th_ws_frame_parser_test.c b/src/th_ws_frame_parser_test.c new file mode 100644 index 0000000..867228f --- /dev/null +++ b/src/th_ws_frame_parser_test.c @@ -0,0 +1,244 @@ +#include "th_system_error.h" +#include "th_test.h" +#include "th_ws_frame_parser.h" + +#include + +/* Frame byte layout (RFC 6455 §5.2), all client frames below use mask key + * 12 34 56 78 (arbitrary, fixed for reproducibility): + * byte 0: FIN(1) RSV(3)=0 opcode(4) + * byte 1: MASK(1)=1 payload-len(7) [126/127 => 2/8 byte extended length] + * next 4 bytes: mask key + * remaining bytes: payload XORed with the mask key, repeating every 4 bytes + * + * Each array below was generated with a short throwaway Python script: + * MASK = bytes([0x12, 0x34, 0x56, 0x78]) + * def frame(opcode, payload, fin=True): + * b0 = (0x80 if fin else 0) | opcode + * n = len(payload) + * hdr = bytes([b0, 0x80 | n]) # n < 126 for all frames here + * return hdr + MASK + bytes(b ^ MASK[i % 4] for i, b in enumerate(payload)) + */ + +// text "hello", FIN=1 +static const unsigned char FRAME_TEXT_HELLO[] = {0x81, 0x85, 0x12, 0x34, 0x56, 0x78, 0x7a, 0x51, 0x3a, 0x14, 0x7d}; + +// text "hi", FIN=1 +static const unsigned char FRAME_TEXT_HI[] = {0x81, 0x82, 0x12, 0x34, 0x56, 0x78, 0x7a, 0x5d}; + +// text "foo", FIN=0 (first fragment of a message) +static const unsigned char FRAME_TEXT_FOO_FIN0[] = {0x01, 0x83, 0x12, 0x34, 0x56, 0x78, 0x74, 0x5b, 0x39}; + +// continuation "bar", FIN=1 (final fragment) +static const unsigned char FRAME_CONT_BAR_FIN1[] = {0x80, 0x83, 0x12, 0x34, 0x56, 0x78, 0x70, 0x55, 0x24}; + +// ping "ping", FIN=1 +static const unsigned char FRAME_PING[] = {0x89, 0x84, 0x12, 0x34, 0x56, 0x78, 0x62, 0x5d, 0x38, 0x1f}; + +// close, empty payload, FIN=1 +static const unsigned char FRAME_CLOSE[] = {0x88, 0x80, 0x12, 0x34, 0x56, 0x78}; + +static const unsigned char TEST_MASK[4] = {0x12, 0x34, 0x56, 0x78}; + +TH_TEST_BEGIN(ws_frame_parser) +{ + th_ws_frame_parser parser = {0}; + th_buf_vec payload; + th_buf_vec_init(&payload, NULL); + + TH_TEST_CASE_BEGIN(ws_frame_parser_single_text_frame) + { + unsigned char buf[sizeof(FRAME_TEXT_HELLO)]; + memcpy(buf, FRAME_TEXT_HELLO, sizeof(buf)); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(buf)); + TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(th_buf_vec_size(&payload) == 5); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "hello", 5) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_reports_eagain_until_whole_frame_present) + { + // Feed one more byte per attempt: each call consumes everything it's + // given (staged internally) and reports EAGAIN until the frame completes. + th_err err = TH_ERR_OK; + for (size_t n = 1; n <= sizeof(FRAME_TEXT_HI); ++n) { + unsigned char b = FRAME_TEXT_HI[n - 1]; + size_t parsed = 0; + th_ws_frame_type type; + err = th_ws_frame_parser_parse(&parser, (char*)&b, 1, &payload, &parsed, &type); + TH_EXPECT(parsed == 1); + if (n < sizeof(FRAME_TEXT_HI)) + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EAGAIN)); + } + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(th_buf_vec_size(&payload) == 2); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "hi", 2) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_reassembles_fragmented_message) + { + unsigned char buf[sizeof(FRAME_TEXT_FOO_FIN0) + sizeof(FRAME_CONT_BAR_FIN1)]; + memcpy(buf, FRAME_TEXT_FOO_FIN0, sizeof(FRAME_TEXT_FOO_FIN0)); + memcpy(buf + sizeof(FRAME_TEXT_FOO_FIN0), FRAME_CONT_BAR_FIN1, sizeof(FRAME_CONT_BAR_FIN1)); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(buf)); + TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(th_buf_vec_size(&payload) == 6); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "foobar", 6) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_reassembles_fragment_split_across_calls) + { + size_t parsed = 0; + th_ws_frame_type type; + unsigned char first[sizeof(FRAME_TEXT_FOO_FIN0)]; + memcpy(first, FRAME_TEXT_FOO_FIN0, sizeof(first)); + th_err err = th_ws_frame_parser_parse(&parser, (char*)first, sizeof(first), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EAGAIN)); // only the first fragment is here, no message yet + TH_EXPECT(parsed == sizeof(first)); + + unsigned char second[sizeof(FRAME_CONT_BAR_FIN1)]; + memcpy(second, FRAME_CONT_BAR_FIN1, sizeof(second)); + parsed = 0; + err = th_ws_frame_parser_parse(&parser, (char*)second, sizeof(second), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(second)); + TH_EXPECT(th_buf_vec_size(&payload) == 6); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "foobar", 6) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_ping_is_skipped_not_appended) + { + unsigned char buf[sizeof(FRAME_PING) + sizeof(FRAME_TEXT_HI)]; + memcpy(buf, FRAME_PING, sizeof(FRAME_PING)); + memcpy(buf + sizeof(FRAME_PING), FRAME_TEXT_HI, sizeof(FRAME_TEXT_HI)); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(FRAME_PING), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(FRAME_PING)); + TH_EXPECT(type == TH_WS_FRAME_PING); + TH_EXPECT(th_buf_vec_size(&payload) == 0); + + parsed = 0; + err = th_ws_frame_parser_parse(&parser, (char*)buf + sizeof(FRAME_PING), sizeof(FRAME_TEXT_HI), &payload, + &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(th_buf_vec_size(&payload) == 2); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "hi", 2) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_close_frame_reports_type) + { + unsigned char buf[sizeof(FRAME_CLOSE)]; + memcpy(buf, FRAME_CLOSE, sizeof(buf)); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(buf)); + TH_EXPECT(type == TH_WS_FRAME_CLOSE); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_extended_16_bit_length) + { + // binary, FIN=1, payload_len=512 (encoded as 0x7E + 16-bit 0x0200) + // payload is 0..255 repeated twice, masked with TEST_MASK. + unsigned char src[512]; + for (size_t i = 0; i < sizeof(src); ++i) + src[i] = (unsigned char)i; + unsigned char buf[8 + sizeof(src)]; + buf[0] = 0x82; + buf[1] = 0x80 | 126; + buf[2] = 0x02; + buf[3] = 0x00; + memcpy(buf + 4, TEST_MASK, 4); + for (size_t i = 0; i < sizeof(src); ++i) + buf[8 + i] = (unsigned char)(src[i] ^ TEST_MASK[i % 4]); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_OK); + TH_EXPECT(parsed == sizeof(buf)); + TH_EXPECT(th_buf_vec_size(&payload) == sizeof(src)); + TH_EXPECT(memcmp(th_buf_vec_begin(&payload), src, sizeof(src)) == 0); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_rejects_unmasked_frame) + { + // text "hello", FIN=1, MASK bit cleared (byte 1 = 0x05, not 0x85) + unsigned char buf[] = {0x81, 0x05, 'h', 'e', 'l', 'l', 'o'}; + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EPROTO)); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_rejects_reserved_bits) + { + // text "hello", FIN=1, RSV1 set (byte 0 = 0xC1, not 0x81) + unsigned char buf[] = {0xc1, 0x85, 0x12, 0x34, 0x56, 0x78, 0x7a, 0x51, 0x3a, 0x14, 0x7d}; + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EPROTO)); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_rejects_continuation_without_message) + { + // continuation opcode as the *first* frame - nothing to continue + unsigned char buf[sizeof(FRAME_CONT_BAR_FIN1)]; + memcpy(buf, FRAME_CONT_BAR_FIN1, sizeof(buf)); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EPROTO)); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ws_frame_parser_rejects_oversized_message) + { + // binary, FIN=1, 64-bit extended length one byte past the configured max. + unsigned char buf[14]; + buf[0] = 0x82; + buf[1] = 0x80 | 127; + uint64_t huge = (uint64_t)TH_CONFIG_WS_MAX_MESSAGE_LEN + 1; + for (int shift = 56, i = 2; shift >= 0; shift -= 8, ++i) + buf[i] = (unsigned char)(huge >> shift); + memcpy(buf + 10, TEST_MASK, 4); + + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); + TH_EXPECT(err == TH_ERR_SYSTEM(TH_EPROTO)); + } + TH_TEST_CASE_END + + th_buf_vec_deinit(&payload); +} +TH_TEST_END diff --git a/src/th_ws_test.c b/src/th_ws_test.c index 60be194..2f8cb23 100644 --- a/src/th_ws_test.c +++ b/src/th_ws_test.c @@ -2,9 +2,12 @@ #include "th_test.h" #include "th_ws.h" +#include + typedef struct th_fake_conn { th_conn base; bool destroyed; + void* recv_addr; void (*callback)(void* user_data, size_t size, th_err err); void* user_data; th_err next_recv_err; @@ -31,6 +34,7 @@ th_fake_conn_recv(void* self, void* addr, size_t len, bool exact, th_recv_cb cal (void)exact; th_fake_conn* conn = self; TH_ASSERT(conn->callback == NULL); + conn->recv_addr = addr; conn->callback = callback; conn->user_data = user_data; } @@ -76,6 +80,7 @@ th_fake_conn_init(th_fake_conn* conn) { conn->base.methods = &th_fake_conn_methods; conn->destroyed = false; + conn->recv_addr = NULL; conn->callback = NULL; conn->user_data = NULL; conn->next_recv_err = TH_ERR_EOF; @@ -92,17 +97,31 @@ th_fake_conn_run(th_fake_conn* conn) callback(user_data, 0, conn->next_recv_err); } +static void +th_fake_conn_deliver(th_fake_conn* conn, const unsigned char* data, size_t len) +{ + TH_ASSERT(conn->callback != NULL); + memcpy(conn->recv_addr, data, len); + void (*callback)(void*, size_t, th_err) = conn->callback; + void* user_data = conn->user_data; + conn->callback = NULL; + conn->user_data = NULL; + callback(user_data, len, TH_ERR_OK); +} + struct handler_calls { int open_count; int close_count; + int data_count; th_err return_on_open; + char data_buf[64]; + size_t data_len; }; static th_err th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) { (void)ws; - (void)data; struct handler_calls* calls = userp; switch (ev) { case TH_WS_EVENT_OPEN: @@ -111,6 +130,12 @@ th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) case TH_WS_EVENT_CLOSE: calls->close_count++; return TH_ERR_OK; + case TH_WS_EVENT_DATA: + calls->data_count++; + TH_ASSERT(data.len <= sizeof(calls->data_buf)); + memcpy(calls->data_buf, data.ptr, data.len); + calls->data_len = data.len; + return TH_ERR_OK; default: return TH_ERR_OK; } @@ -120,7 +145,7 @@ TH_TEST_BEGIN(ws) { th_fake_conn conn; th_fake_conn_init(&conn); - struct handler_calls calls = {0, 0, TH_ERR_OK}; + struct handler_calls calls = {.return_on_open = TH_ERR_OK}; TH_TEST_CASE_BEGIN(ws_start_fires_open) { @@ -179,6 +204,82 @@ TH_TEST_BEGIN(ws) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_data_frame_fires_data_event) + { + // masked "hi" text frame: FIN|TEXT, len=2, mask 11 22 33 44 + static const unsigned char frame[] = {0x81, 0x82, 0x11, 0x22, 0x33, 0x44, 0x79, 0x4b}; + + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + th_fake_conn_deliver(&conn, frame, sizeof(frame)); + TH_EXPECT(calls.data_count == 1); + TH_EXPECT(calls.data_len == 2); + TH_EXPECT(memcmp(calls.data_buf, "hi", 2) == 0); + TH_EXPECT(calls.close_count == 0); + TH_EXPECT(!conn.destroyed); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_close_frame_closes_connection) + { + // masked empty CLOSE frame: FIN|CLOSE, len=0, mask 11 22 33 44 + static const unsigned char frame[] = {0x88, 0x80, 0x11, 0x22, 0x33, 0x44}; + + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + th_fake_conn_deliver(&conn, frame, sizeof(frame)); + TH_EXPECT(calls.data_count == 0); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_data_frame_byte_by_byte) + { + // masked "hi" text frame: FIN|TEXT, len=2, mask 11 22 33 44 + static const unsigned char frame[] = {0x81, 0x82, 0x11, 0x22, 0x33, 0x44, 0x79, 0x4b}; + + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + for (size_t i = 0; i < sizeof(frame); ++i) { + th_fake_conn_deliver(&conn, &frame[i], 1); + TH_EXPECT(!conn.destroyed); + } + TH_EXPECT(calls.data_count == 1); + TH_EXPECT(calls.data_len == 2); + TH_EXPECT(memcmp(calls.data_buf, "hi", 2) == 0); + TH_EXPECT(calls.close_count == 0); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_malformed_frame_closes_connection) + { + // unmasked frame (MASK bit clear) - a protocol violation from a client + static const unsigned char frame[] = {0x81, 0x02, 'h', 'i'}; + + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + th_fake_conn_deliver(&conn, frame, sizeof(frame)); + TH_EXPECT(calls.data_count == 0); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(ws_send_returns_nosupport) { th_ws* ws = NULL; From f55099252f62912c8a904bd4706f786781f4faa1 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 13:38:30 -0500 Subject: [PATCH 05/13] feat: report text vs binary on received WebSocket messages Renames th_ws_msg_type to th_ws_type, and adds it as a parameter to th_ws_handler so TH_WS_EVENT_DATA callers know the message's frame opcode, not just its bytes. --- include/th.h | 25 +++++++++++++------------ src/th_http_test.c | 3 ++- src/th_router_test.c | 3 ++- src/th_ws.c | 9 +++++---- src/th_ws_frame.h | 3 ++- src/th_ws_frame_parser.c | 2 +- src/th_ws_frame_parser_test.c | 7 ++++--- src/th_ws_test.c | 27 +++++++++++++++++++++++++-- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/include/th.h b/include/th.h index 30ddc3a..5779a11 100644 --- a/include/th.h +++ b/include/th.h @@ -337,20 +337,21 @@ typedef enum th_ws_event { typedef struct th_ws th_ws; -/** th_ws_handler - * @brief WebSocket event callback. data is empty for TH_WS_EVENT_OPEN/CLOSE, - * and holds one complete message's payload for TH_WS_EVENT_DATA. ws must - * not be used after TH_WS_EVENT_CLOSE has been delivered. +/** th_ws_type + * @brief Text vs binary, for both received messages and th_ws_send. */ -typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data); +typedef enum th_ws_type { + TH_WS_TEXT, + TH_WS_BINARY, +} th_ws_type; -/** th_ws_msg_type - * @brief Selects the opcode a th_ws_send message goes out as. +/** th_ws_handler + * @brief WebSocket event callback. data is empty for TH_WS_EVENT_OPEN/CLOSE, + * and holds one complete message's payload for TH_WS_EVENT_DATA - type is + * only meaningful for TH_WS_EVENT_DATA. ws must not be used after + * TH_WS_EVENT_CLOSE has been delivered. */ -typedef enum th_ws_msg_type { - TH_WS_MSG_TEXT, - TH_WS_MSG_BINARY, -} th_ws_msg_type; +typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type); /** th_ws_send * @brief Sends one WebSocket message. type selects the opcode (text vs @@ -359,7 +360,7 @@ typedef enum th_ws_msg_type { * finished yet (retry once the next event is delivered), TH_ERR_INVALID_ARG * if the connection is closing/closed. */ -th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_msg_type type); +th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_type type); /** th_ws_close * @brief Starts closing the connection. TH_WS_EVENT_CLOSE will be diff --git a/src/th_http_test.c b/src/th_http_test.c index f448a6c..90aedc4 100644 --- a/src/th_http_test.c +++ b/src/th_http_test.c @@ -195,12 +195,13 @@ th_test_system_error_handler(void* user_data, const th_request* req, th_response } static th_err -th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type) { (void)userp; (void)ws; (void)ev; (void)data; + (void)type; return TH_ERR_OK; } diff --git a/src/th_router_test.c b/src/th_router_test.c index 51bb69a..101026f 100644 --- a/src/th_router_test.c +++ b/src/th_router_test.c @@ -33,12 +33,13 @@ expect_pathvars_handler(void* user_data, const th_request* req, th_response* res } static th_err -noop_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +noop_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type) { (void)userp; (void)ws; (void)ev; (void)data; + (void)type; return TH_ERR_OK; } diff --git a/src/th_ws.c b/src/th_ws.c index 615349f..f26bd81 100644 --- a/src/th_ws.c +++ b/src/th_ws.c @@ -47,7 +47,7 @@ th_ws_destroy(th_ws* ws) TH_LOCAL(void) th_ws_close_and_destroy(th_ws* ws) { - (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}); + (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}, TH_WS_TEXT); th_ws_destroy(ws); } @@ -76,7 +76,8 @@ th_ws_consume(th_ws* ws, char* data, size_t len) continue; th_buffer message = {th_buf_vec_begin(&ws->payload), th_buf_vec_size(&ws->payload)}; - err = ws->handler(ws->user_data, ws, TH_WS_EVENT_DATA, message); + th_ws_type msg_type = type == TH_WS_FRAME_TEXT ? TH_WS_TEXT : TH_WS_BINARY; + err = ws->handler(ws->user_data, ws, TH_WS_EVENT_DATA, message, msg_type); th_buf_vec_clear(&ws->payload); if (err != TH_ERR_OK) { TH_LOG_DEBUG("%p: DATA handler returned %s, closing", (void*)ws, th_strerror(err)); @@ -105,7 +106,7 @@ th_ws_handle_recv(void* user_data, size_t len, th_err err) TH_PRIVATE(void) th_ws_start(th_ws* ws) { - th_err err = ws->handler(ws->user_data, ws, TH_WS_EVENT_OPEN, (th_buffer){0}); + th_err err = ws->handler(ws->user_data, ws, TH_WS_EVENT_OPEN, (th_buffer){0}, TH_WS_TEXT); if (err != TH_ERR_OK) { TH_LOG_DEBUG("%p: OPEN handler returned %s, closing", (void*)ws, th_strerror(err)); th_ws_destroy(ws); @@ -115,7 +116,7 @@ th_ws_start(th_ws* ws) } TH_PUBLIC(th_err) -th_ws_send(th_ws* ws, th_buffer data, th_ws_msg_type type) +th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) { (void)ws; (void)data; diff --git a/src/th_ws_frame.h b/src/th_ws_frame.h index a4249c0..2102a0e 100644 --- a/src/th_ws_frame.h +++ b/src/th_ws_frame.h @@ -6,7 +6,8 @@ #include "th_config.h" typedef enum th_ws_frame_type { - TH_WS_FRAME_DATA, + TH_WS_FRAME_TEXT, + TH_WS_FRAME_BINARY, TH_WS_FRAME_PING, TH_WS_FRAME_PONG, TH_WS_FRAME_CLOSE, diff --git a/src/th_ws_frame_parser.c b/src/th_ws_frame_parser.c index 43237b1..6817ea4 100644 --- a/src/th_ws_frame_parser.c +++ b/src/th_ws_frame_parser.c @@ -131,8 +131,8 @@ th_ws_frame_parser_frame_done(th_ws_frame_parser* parser, bool* message_done, th if (parser->opcode != TH_WS_OPCODE_CONTINUATION) parser->message_opcode = parser->opcode; if (parser->fin) { + *type = parser->message_opcode == TH_WS_OPCODE_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; parser->message_opcode = 0; - *type = TH_WS_FRAME_DATA; *message_done = true; } } diff --git a/src/th_ws_frame_parser_test.c b/src/th_ws_frame_parser_test.c index 867228f..c19d63c 100644 --- a/src/th_ws_frame_parser_test.c +++ b/src/th_ws_frame_parser_test.c @@ -56,7 +56,7 @@ TH_TEST_BEGIN(ws_frame_parser) th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); TH_EXPECT(err == TH_ERR_OK); TH_EXPECT(parsed == sizeof(buf)); - TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(type == TH_WS_FRAME_TEXT); TH_EXPECT(th_buf_vec_size(&payload) == 5); TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "hello", 5) == 0); } @@ -93,7 +93,7 @@ TH_TEST_BEGIN(ws_frame_parser) th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); TH_EXPECT(err == TH_ERR_OK); TH_EXPECT(parsed == sizeof(buf)); - TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(type == TH_WS_FRAME_TEXT); TH_EXPECT(th_buf_vec_size(&payload) == 6); TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "foobar", 6) == 0); } @@ -138,7 +138,7 @@ TH_TEST_BEGIN(ws_frame_parser) err = th_ws_frame_parser_parse(&parser, (char*)buf + sizeof(FRAME_PING), sizeof(FRAME_TEXT_HI), &payload, &parsed, &type); TH_EXPECT(err == TH_ERR_OK); - TH_EXPECT(type == TH_WS_FRAME_DATA); + TH_EXPECT(type == TH_WS_FRAME_TEXT); TH_EXPECT(th_buf_vec_size(&payload) == 2); TH_EXPECT(memcmp(th_buf_vec_begin(&payload), "hi", 2) == 0); } @@ -179,6 +179,7 @@ TH_TEST_BEGIN(ws_frame_parser) th_err err = th_ws_frame_parser_parse(&parser, (char*)buf, sizeof(buf), &payload, &parsed, &type); TH_EXPECT(err == TH_ERR_OK); TH_EXPECT(parsed == sizeof(buf)); + TH_EXPECT(type == TH_WS_FRAME_BINARY); TH_EXPECT(th_buf_vec_size(&payload) == sizeof(src)); TH_EXPECT(memcmp(th_buf_vec_begin(&payload), src, sizeof(src)) == 0); } diff --git a/src/th_ws_test.c b/src/th_ws_test.c index 2f8cb23..781a9b4 100644 --- a/src/th_ws_test.c +++ b/src/th_ws_test.c @@ -116,10 +116,11 @@ struct handler_calls { th_err return_on_open; char data_buf[64]; size_t data_len; + th_ws_type data_type; }; static th_err -th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) +th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type) { (void)ws; struct handler_calls* calls = userp; @@ -135,6 +136,7 @@ th_test_ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data) TH_ASSERT(data.len <= sizeof(calls->data_buf)); memcpy(calls->data_buf, data.ptr, data.len); calls->data_len = data.len; + calls->data_type = type; return TH_ERR_OK; default: return TH_ERR_OK; @@ -217,6 +219,7 @@ TH_TEST_BEGIN(ws) TH_EXPECT(calls.data_count == 1); TH_EXPECT(calls.data_len == 2); TH_EXPECT(memcmp(calls.data_buf, "hi", 2) == 0); + TH_EXPECT(calls.data_type == TH_WS_TEXT); TH_EXPECT(calls.close_count == 0); TH_EXPECT(!conn.destroyed); @@ -226,6 +229,26 @@ TH_TEST_BEGIN(ws) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_recv_binary_frame_reports_binary_type) + { + // masked "hi" binary frame: FIN|BINARY, len=2, mask 11 22 33 44 + static const unsigned char frame[] = {0x82, 0x82, 0x11, 0x22, 0x33, 0x44, 0x79, 0x4b}; + + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + th_fake_conn_deliver(&conn, frame, sizeof(frame)); + TH_EXPECT(calls.data_count == 1); + TH_EXPECT(calls.data_type == TH_WS_BINARY); + TH_EXPECT(calls.close_count == 0); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(ws_recv_close_frame_closes_connection) { // masked empty CLOSE frame: FIN|CLOSE, len=0, mask 11 22 33 44 @@ -285,7 +308,7 @@ TH_TEST_BEGIN(ws) th_ws* ws = NULL; TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); th_ws_start(ws); - TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_MSG_TEXT) == TH_ERR_NOSUPPORT); + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_NOSUPPORT); conn.next_recv_err = TH_ERR_EOF; th_fake_conn_run(&conn); From d3c7d637b8e595bd23449f731ef206e05b79dc80 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:06:47 -0500 Subject: [PATCH 06/13] feat: implement WebSocket message sending - th_ws_send queues header+payload into a th_ring (growable chunked ring buffer) and drains it via th_conn_send; grows on overflow instead of rejecting, capped by TH_CONFIG_WS_SEND_MAX_LEN - th_ws_frame_header_write encodes the frame header, shared so close/ping-pong replies can reuse it later - fix: th_queue's _pop left a dangling tail pointer after removing the last item --- CMakeLists.txt | 5 ++ include/th.h | 10 +-- src/th_config.h | 8 ++ src/th_queue.h | 2 + src/th_queue_test.c | 56 ++++++++++++++ src/th_ring.c | 140 ++++++++++++++++++++++++++++++++++ src/th_ring.h | 78 +++++++++++++++++++ src/th_ring_test.c | 157 +++++++++++++++++++++++++++++++++++++++ src/th_ws.c | 51 +++++++++++-- src/th_ws.h | 6 ++ src/th_ws_frame.c | 38 ++++++++++ src/th_ws_frame.h | 12 +++ src/th_ws_frame_test.c | 47 ++++++++++++ src/th_ws_test.c | 165 +++++++++++++++++++++++++++++++++++++++-- 14 files changed, 757 insertions(+), 18 deletions(-) create mode 100644 src/th_queue_test.c create mode 100644 src/th_ring.c create mode 100644 src/th_ring.h create mode 100644 src/th_ring_test.c create mode 100644 src/th_ws_frame.c create mode 100644 src/th_ws_frame_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cfaf0b..b36afd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,9 @@ SET(TH_CORE_SRC src/th_sha1.c src/th_base64.c src/th_ws_handshake.c + src/th_ws_frame.c src/th_ws_frame_parser.c + src/th_ring.c src/th_ws.c # SSL (compiled out via TH_WITH_SSL=0 when OpenSSL is not found) src/th_ssl_smem_bio.c @@ -221,6 +223,7 @@ if (NOT TH_DISABLE_TESTS) set(CMAKE_TESTDRIVER_AFTER_TESTMAIN "th_test_teardown();") set(TH_TESTS + src/th_queue_test.c src/th_task_test.c src/th_timer_test.c src/th_loop_test.c @@ -244,7 +247,9 @@ if (NOT TH_DISABLE_TESTS) src/th_sha1_test.c src/th_base64_test.c src/th_ws_handshake_test.c + src/th_ws_frame_test.c src/th_ws_frame_parser_test.c + src/th_ring_test.c src/th_response_test.c src/th_http_test.c src/th_ws_test.c diff --git a/include/th.h b/include/th.h index 5779a11..a023961 100644 --- a/include/th.h +++ b/include/th.h @@ -354,11 +354,11 @@ typedef enum th_ws_type { typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type); /** th_ws_send - * @brief Sends one WebSocket message. type selects the opcode (text vs - * binary), it does not otherwise affect encoding - data is sent as-is. - * @return TH_ERR_BUSY if a previous send on this connection hasn't - * finished yet (retry once the next event is delivered), TH_ERR_INVALID_ARG - * if the connection is closing/closed. + * @brief Queues one WebSocket message for sending. type selects the + * opcode (text vs binary), it does not otherwise affect encoding - data + * is sent as-is. + * @return TH_ERR_SYSTEM(TH_EAGAIN) if the send queue is full - retry + * once a previously queued message has gone out. */ th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_type type); diff --git a/src/th_config.h b/src/th_config.h index c2c974d..b7cd2f3 100644 --- a/src/th_config.h +++ b/src/th_config.h @@ -34,6 +34,14 @@ #define TH_CONFIG_WS_MAX_MESSAGE_LEN (4 * 1024 * 1024) #endif +#ifndef TH_CONFIG_WS_SEND_RING_LEN +#define TH_CONFIG_WS_SEND_RING_LEN (16 * 1024) +#endif + +#ifndef TH_CONFIG_WS_SEND_MAX_LEN +#define TH_CONFIG_WS_SEND_MAX_LEN (256 * 1024) +#endif + /* feature configuration end */ #if defined(__APPLE__) diff --git a/src/th_queue.h b/src/th_queue.h index ee88a40..f2b53d7 100644 --- a/src/th_queue.h +++ b/src/th_queue.h @@ -71,6 +71,8 @@ T* item = queue->head; \ if (item) { \ queue->head = item->next; \ + if (queue->head == NULL) \ + queue->tail = NULL; \ item->next = NULL; \ } \ return item; \ diff --git a/src/th_queue_test.c b/src/th_queue_test.c new file mode 100644 index 0000000..383e712 --- /dev/null +++ b/src/th_queue_test.c @@ -0,0 +1,56 @@ +#include "th_queue.h" +#include "th_test.h" + +typedef struct th_queue_test_item { + struct th_queue_test_item* next; + int value; +} th_queue_test_item; + +TH_DEFINE_QUEUE(th_queue_test_queue, th_queue_test_item) + +TH_TEST_BEGIN(queue) +{ + TH_TEST_CASE_BEGIN(queue_pop_last_item_clears_tail) + { + th_queue_test_queue queue = th_queue_test_queue_make(); + th_queue_test_item item = {.value = 1}; + th_queue_test_queue_push(&queue, &item); + + TH_EXPECT(th_queue_test_queue_pop(&queue) == &item); + TH_EXPECT(queue.head == NULL); + TH_EXPECT(queue.tail == NULL); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(queue_push_after_draining_to_empty_reuses_tail_correctly) + { + th_queue_test_queue queue = th_queue_test_queue_make(); + th_queue_test_item item1 = {.value = 1}; + th_queue_test_item item2 = {.value = 2}; + th_queue_test_queue_push(&queue, &item1); + th_queue_test_queue_pop(&queue); + + // if pop left a stale tail pointing at item1, this push would + // write item2 into item1's already-popped next field instead of + // becoming both head and tail itself + th_queue_test_queue_push(&queue, &item2); + TH_EXPECT(queue.head == &item2); + TH_EXPECT(queue.tail == &item2); + TH_EXPECT(th_queue_test_queue_pop(&queue) == &item2); + TH_EXPECT(th_queue_test_queue_pop(&queue) == NULL); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(queue_push_pop_multiple) + { + th_queue_test_queue queue = th_queue_test_queue_make(); + th_queue_test_item item1 = {.value = 1}; + th_queue_test_item item2 = {.value = 2}; + th_queue_test_queue_push(&queue, &item1); + th_queue_test_queue_push(&queue, &item2); + + TH_EXPECT(th_queue_test_queue_pop(&queue) == &item1); + TH_EXPECT(th_queue_test_queue_pop(&queue) == &item2); + TH_EXPECT(queue.tail == NULL); + } + TH_TEST_CASE_END +} +TH_TEST_END diff --git a/src/th_ring.c b/src/th_ring.c new file mode 100644 index 0000000..0176bff --- /dev/null +++ b/src/th_ring.c @@ -0,0 +1,140 @@ +#include "th_ring.h" + +#include "th_align.h" +#include "th_system_error.h" + +#include + +// Chunk header + backing buffer live in one allocation - data points at +// an offset into the same block, rounded up so it's th_max_align-aligned. +#define TH_RING_CHUNK_HEADER_LEN TH_ALIGNUP(sizeof(th_ring_chunk), TH_ALIGNOF(th_max_align)) + +TH_LOCAL(th_ring_chunk*) +th_ring_chunk_create(th_allocator* allocator, size_t capacity) +{ + th_ring_chunk* chunk = th_allocator_alloc(allocator, TH_RING_CHUNK_HEADER_LEN + capacity); + if (!chunk) + return NULL; + chunk->data = (unsigned char*)chunk + TH_RING_CHUNK_HEADER_LEN; + chunk->capacity = capacity; + chunk->head = 0; + chunk->tail = 0; + return chunk; +} + +TH_LOCAL(size_t) +th_ring_chunk_len(const th_ring_chunk* chunk) +{ + return chunk->tail - chunk->head; +} + +TH_LOCAL(size_t) +th_ring_chunk_free_space(const th_ring_chunk* chunk) +{ + return chunk->capacity - th_ring_chunk_len(chunk); +} + +TH_LOCAL(void) +th_ring_chunk_write(th_ring_chunk* chunk, const void* data, size_t len) +{ + size_t offset = chunk->tail % chunk->capacity; + size_t first = chunk->capacity - offset < len ? chunk->capacity - offset : len; + memcpy(chunk->data + offset, data, first); + memcpy(chunk->data, (const unsigned char*)data + first, len - first); + chunk->tail += len; +} + +TH_PRIVATE(void) +th_ring_init(th_ring* rb, th_allocator* allocator, size_t initial_capacity, size_t max_len) +{ + rb->chunks = th_ring_chunk_queue_make(); + rb->len = 0; + rb->initial_capacity = initial_capacity; + rb->max_len = max_len; + rb->allocator = allocator ? allocator : th_default_allocator_get(); +} + +TH_PRIVATE(void) +th_ring_deinit(th_ring* rb) +{ + th_ring_chunk* chunk; + while ((chunk = th_ring_chunk_queue_pop(&rb->chunks)) != NULL) + th_allocator_free(rb->allocator, chunk); +} + +TH_LOCAL(size_t) +th_ring_parts_len(const th_iov* parts, size_t partcnt) +{ + size_t len = 0; + for (size_t i = 0; i < partcnt; ++i) + len += parts[i].len; + return len; +} + +TH_PRIVATE(th_err) +th_ring_write(th_ring* rb, const th_iov* parts, size_t partcnt) +{ + size_t len = th_ring_parts_len(parts, partcnt); + if (rb->len + len > rb->max_len) + return TH_ERR_INVALID_ARG; + + th_ring_chunk* tail_chunk = rb->chunks.tail; + if (!tail_chunk || th_ring_chunk_free_space(tail_chunk) < len) { + size_t capacity = tail_chunk ? tail_chunk->capacity * 2 : rb->initial_capacity; + if (capacity < len) + capacity = len; + th_ring_chunk* chunk = th_ring_chunk_create(rb->allocator, capacity); + if (!chunk) + return TH_ERR_SYSTEM(TH_EAGAIN); + + // an empty tail chunk is unreachable once anything is queued + // behind it - peek/consume only ever advance from chunks.head + if (tail_chunk && th_ring_chunk_len(tail_chunk) == 0) { + th_ring_chunk_queue_pop(&rb->chunks); + th_allocator_free(rb->allocator, tail_chunk); + } + th_ring_chunk_queue_push(&rb->chunks, chunk); + tail_chunk = chunk; + } + + for (size_t i = 0; i < partcnt; ++i) + th_ring_chunk_write(tail_chunk, parts[i].base, parts[i].len); + rb->len += len; + return TH_ERR_OK; +} + +TH_PRIVATE(size_t) +th_ring_peek(th_ring* rb, th_iov iov[2]) +{ + th_ring_chunk* chunk = rb->chunks.head; + size_t len = chunk ? th_ring_chunk_len(chunk) : 0; + if (len == 0) + return 0; + + size_t offset = chunk->head % chunk->capacity; + size_t first = chunk->capacity - offset < len ? chunk->capacity - offset : len; + iov[0].base = chunk->data + offset; + iov[0].len = first; + if (first == len) + return 1; + + iov[1].base = chunk->data; + iov[1].len = len - first; + return 2; +} + +TH_PRIVATE(void) +th_ring_consume(th_ring* rb, size_t len) +{ + th_ring_chunk* chunk = rb->chunks.head; + chunk->head += len; + rb->len -= len; + + bool drained = th_ring_chunk_len(chunk) == 0; + bool sole_chunk = chunk == rb->chunks.tail; + if (!drained || (sole_chunk && chunk->capacity <= rb->initial_capacity)) + return; + + th_ring_chunk_queue_pop(&rb->chunks); + th_allocator_free(rb->allocator, chunk); +} diff --git a/src/th_ring.h b/src/th_ring.h new file mode 100644 index 0000000..ba4bef2 --- /dev/null +++ b/src/th_ring.h @@ -0,0 +1,78 @@ +#ifndef TH_RING_H +#define TH_RING_H + +#include + +#include "th_allocator.h" +#include "th_config.h" +#include "th_iov.h" +#include "th_queue.h" + +#include + +typedef struct th_ring_chunk { + struct th_ring_chunk* next; + unsigned char* data; + size_t capacity; + // head/tail only ever increase - never wrapped themselves, so + // len = tail - head and full = (tail - head == capacity) always hold. + // Actual buffer offsets are head % capacity / tail % capacity. + size_t head; + size_t tail; +} th_ring_chunk; + +/* th_ring_chunk_queue declarations begin */ + +#ifndef TH_RING_CHUNK_QUEUE +#define TH_RING_CHUNK_QUEUE +TH_DEFINE_QUEUE(th_ring_chunk_queue, th_ring_chunk) +#endif + +/* th_ring_chunk_queue declarations end */ + +/** th_ring + * @brief FIFO byte queue backed by a linked list of ring-buffer chunks. + * Writes always land in the tail chunk; once it's full a new, twice as + * large chunk is appended. Reads (peek/consume) only ever touch the head + * chunk, which is freed once fully consumed. + */ +typedef struct th_ring { + th_ring_chunk_queue chunks; + size_t len; // total bytes currently queued, across all chunks + size_t initial_capacity; // size of the first chunk, allocated lazily on first write + size_t max_len; // th_ring_write rejects anything that would exceed this + th_allocator* allocator; +} th_ring; + +TH_PRIVATE(void) +th_ring_init(th_ring* rb, th_allocator* allocator, size_t initial_capacity, size_t max_len); + +TH_PRIVATE(void) +th_ring_deinit(th_ring* rb); + +/** th_ring_write + * @brief Queues parts as one message (never split across chunks), + * growing (doubling the tail chunk) if it doesn't have room. + * + * - TH_ERR_INVALID_ARG: total size alone exceeds max_len, retrying never helps + * - TH_ERR_SYSTEM(TH_EAGAIN): fits under max_len, but a chunk allocation failed + */ +TH_PRIVATE(th_err) +th_ring_write(th_ring* rb, const th_iov* parts, size_t partcnt); + +/** th_ring_peek + * @brief Fills iov[0..1] with the head chunk's queued bytes (iov[1] only + * used if that chunk's queued run wraps past the end of its buffer). + * @return Number of iov entries filled (0, 1, or 2). + */ +TH_PRIVATE(size_t) +th_ring_peek(th_ring* rb, th_iov iov[2]); + +/** th_ring_consume + * @brief Marks the oldest len queued bytes as sent, freeing that space. + * Frees the head chunk once it's fully drained. + */ +TH_PRIVATE(void) +th_ring_consume(th_ring* rb, size_t len); + +#endif diff --git a/src/th_ring_test.c b/src/th_ring_test.c new file mode 100644 index 0000000..187c16a --- /dev/null +++ b/src/th_ring_test.c @@ -0,0 +1,157 @@ +#include "th_ring.h" +#include "th_system_error.h" +#include "th_test.h" + +#include + +TH_LOCAL(th_err) +th_ring_write1(th_ring* rb, const char* data, size_t len) +{ + th_iov part = {(void*)data, len}; + return th_ring_write(rb, &part, 1); +} + +TH_TEST_BEGIN(ring) +{ + th_ring rb; + th_ring_init(&rb, NULL, 8, 64); + + TH_TEST_CASE_BEGIN(ring_write_and_peek) + { + TH_EXPECT(th_ring_write1(&rb, "abcd", 4) == TH_ERR_OK); + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 4); + TH_EXPECT(memcmp(iov[0].base, "abcd", 4) == 0); + + th_ring_consume(&rb, 4); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_consume_frees_space_in_place) + { + TH_EXPECT(th_ring_write1(&rb, "abcd", 4) == TH_ERR_OK); + th_ring_consume(&rb, 4); + + th_iov iov[2]; + TH_EXPECT(th_ring_peek(&rb, iov) == 0); + + // still the same 8-byte chunk - fits without growing + TH_EXPECT(th_ring_write1(&rb, "12345678", 8) == TH_ERR_OK); + th_ring_consume(&rb, 8); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_wraps_within_a_chunk_and_peek_reports_two_iovs) + { + TH_EXPECT(th_ring_write1(&rb, "123456", 6) == TH_ERR_OK); + th_ring_consume(&rb, 6); + // head is now at 6; writing 4 bytes wraps around the 8-byte chunk + TH_EXPECT(th_ring_write1(&rb, "abcd", 4) == TH_ERR_OK); + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 2); + TH_EXPECT(iov[0].len == 2); + TH_EXPECT(memcmp(iov[0].base, "ab", 2) == 0); + TH_EXPECT(iov[1].len == 2); + TH_EXPECT(memcmp(iov[1].base, "cd", 2) == 0); + + th_ring_consume(&rb, 4); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_grows_a_new_chunk_when_full) + { + TH_EXPECT(th_ring_write1(&rb, "12345678", 8) == TH_ERR_OK); // fills the 8-byte chunk + TH_EXPECT(th_ring_write1(&rb, "xy", 2) == TH_ERR_OK); // doesn't fit - grows a new chunk + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 8); + TH_EXPECT(memcmp(iov[0].base, "12345678", 8) == 0); + + th_ring_consume(&rb, 8); // frees the first chunk, head moves to the grown one + n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 2); + TH_EXPECT(memcmp(iov[0].base, "xy", 2) == 0); + + th_ring_consume(&rb, 2); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_never_splits_a_message_across_chunks) + { + TH_EXPECT(th_ring_write1(&rb, "123456", 6) == TH_ERR_OK); // 2 bytes free in the 8-byte chunk + TH_EXPECT(th_ring_write1(&rb, "abcd", 4) == TH_ERR_OK); // doesn't fit - grows instead of splitting + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 6); + th_ring_consume(&rb, 6); + + n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 4); + TH_EXPECT(memcmp(iov[0].base, "abcd", 4) == 0); + th_ring_consume(&rb, 4); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_grows_past_an_emptied_sole_chunk) + { + // drain the sole 8-byte chunk back to empty - it's kept alive + // (not freed) since it's still both head and tail + TH_EXPECT(th_ring_write1(&rb, "abcd", 4) == TH_ERR_OK); + th_ring_consume(&rb, 4); + + // now write something bigger than that emptied chunk's capacity + TH_EXPECT(th_ring_write1(&rb, "0123456789", 10) == TH_ERR_OK); + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n >= 1); + TH_EXPECT(iov[0].len > 0); // the new data must be reachable from peek + + size_t total = 0; + for (size_t i = 0; i < n; ++i) + total += iov[i].len; + TH_EXPECT(total == 10); + th_ring_consume(&rb, 10); + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_rejects_message_over_max_len) + { + char big[65]; + memset(big, 'x', sizeof(big)); + TH_EXPECT(th_ring_write1(&rb, big, sizeof(big)) == TH_ERR_INVALID_ARG); + + th_iov iov[2]; + TH_EXPECT(th_ring_peek(&rb, iov) == 0); // nothing was queued + } + TH_TEST_CASE_END + + TH_TEST_CASE_BEGIN(ring_multipart_write_lands_in_one_chunk) + { + th_iov parts[2] = {{(void*)"ab", 2}, {(void*)"cd", 2}}; + TH_EXPECT(th_ring_write(&rb, parts, 2) == TH_ERR_OK); + + th_iov iov[2]; + size_t n = th_ring_peek(&rb, iov); + TH_EXPECT(n == 1); + TH_EXPECT(iov[0].len == 4); + TH_EXPECT(memcmp(iov[0].base, "abcd", 4) == 0); + + th_ring_consume(&rb, 4); + } + TH_TEST_CASE_END + + th_ring_deinit(&rb); +} +TH_TEST_END diff --git a/src/th_ws.c b/src/th_ws.c index f26bd81..45793c8 100644 --- a/src/th_ws.c +++ b/src/th_ws.c @@ -15,12 +15,15 @@ th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_ ws->allocator = allocator ? allocator : th_default_allocator_get(); ws->parser = (th_ws_frame_parser){0}; th_buf_vec_init(&ws->payload, ws->allocator); + th_ring_init(&ws->send_ring, ws->allocator, TH_CONFIG_WS_SEND_RING_LEN, TH_CONFIG_WS_SEND_MAX_LEN); + ws->sending = false; } TH_PRIVATE(void) th_ws_deinit(th_ws* ws) { th_buf_vec_deinit(&ws->payload); + th_ring_deinit(&ws->send_ring); th_conn_destroy(ws->conn); } @@ -115,14 +118,52 @@ th_ws_start(th_ws* ws) th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); } +TH_LOCAL(void) +th_ws_handle_send(void* user_data, size_t len, th_err err); + +TH_LOCAL(void) +th_ws_send_drain(th_ws* ws) +{ + size_t iovcnt = th_ring_peek(&ws->send_ring, ws->send_iov); + if (iovcnt == 0) { + ws->sending = false; + return; + } + ws->sending = true; + th_conn_send(ws->conn, ws->send_iov, iovcnt, NULL, 0, 0, th_ws_handle_send, ws); +} + +TH_LOCAL(void) +th_ws_handle_send(void* user_data, size_t len, th_err err) +{ + th_ws* ws = user_data; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Send error: %s, closing", (void*)ws, th_strerror(err)); + th_ws_close_and_destroy(ws); + return; + } + th_ring_consume(&ws->send_ring, len); + th_ws_send_drain(ws); +} + TH_PUBLIC(th_err) th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) { - (void)ws; - (void)data; - (void)type; - TH_LOG_ERROR("WebSocket frame sending is not implemented yet."); - return TH_ERR_NOSUPPORT; + th_ws_frame_type frame_type = type == TH_WS_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + size_t header_len = th_ws_frame_header_write(header, frame_type, data.len); + + th_iov parts[2] = { + {header, header_len}, + {(void*)data.ptr, data.len}, + }; + th_err err = th_ring_write(&ws->send_ring, parts, 2); + if (err != TH_ERR_OK) + return err; + + if (!ws->sending) + th_ws_send_drain(ws); + return TH_ERR_OK; } TH_PUBLIC(th_err) diff --git a/src/th_ws.h b/src/th_ws.h index 3372aeb..56fdef6 100644 --- a/src/th_ws.h +++ b/src/th_ws.h @@ -4,6 +4,8 @@ #include #include "th_conn.h" +#include "th_iov.h" +#include "th_ring.h" #include "th_vec.h" #include "th_ws_frame_parser.h" @@ -17,6 +19,10 @@ struct th_ws { th_ws_frame_parser parser; th_buf_vec payload; // accumulates a message's payload across fragments/calls char scratch[TH_WS_SCRATCH_RECV_LEN]; + + th_ring send_ring; + th_iov send_iov[2]; + bool sending; }; TH_PRIVATE(void) diff --git a/src/th_ws_frame.c b/src/th_ws_frame.c new file mode 100644 index 0000000..1a1ae01 --- /dev/null +++ b/src/th_ws_frame.c @@ -0,0 +1,38 @@ +#include "th_ws_frame.h" + +TH_LOCAL(unsigned char) +th_ws_frame_opcode(th_ws_frame_type type) +{ + switch (type) { + case TH_WS_FRAME_TEXT: + return 0x1; + case TH_WS_FRAME_BINARY: + return 0x2; + case TH_WS_FRAME_CLOSE: + return 0x8; + case TH_WS_FRAME_PING: + return 0x9; + default: + return 0xA; // TH_WS_FRAME_PONG + } +} + +TH_PRIVATE(size_t) +th_ws_frame_header_write(unsigned char* header, th_ws_frame_type type, size_t len) +{ + header[0] = 0x80 | th_ws_frame_opcode(type); // FIN=1, no fragmentation on send + if (len < 126) { + header[1] = (unsigned char)len; + return 2; + } + if (len <= 0xFFFF) { + header[1] = 126; + header[2] = (unsigned char)(len >> 8); + header[3] = (unsigned char)len; + return 4; + } + header[1] = 127; + for (int i = 0; i < 8; ++i) + header[2 + i] = (unsigned char)(len >> (8 * (7 - i))); + return 10; +} diff --git a/src/th_ws_frame.h b/src/th_ws_frame.h index 2102a0e..d653914 100644 --- a/src/th_ws_frame.h +++ b/src/th_ws_frame.h @@ -5,6 +5,8 @@ #include "th_config.h" +#include + typedef enum th_ws_frame_type { TH_WS_FRAME_TEXT, TH_WS_FRAME_BINARY, @@ -13,4 +15,14 @@ typedef enum th_ws_frame_type { TH_WS_FRAME_CLOSE, } th_ws_frame_type; +// 2 base bytes + 8 byte extended length (server frames are never masked). +#define TH_WS_FRAME_HEADER_MAX_LEN 10 + +/** th_ws_frame_header_write + * @brief Encodes a FIN=1, unmasked frame header for len bytes of payload. + * @return Bytes written to header (2, 4, or 10). + */ +TH_PRIVATE(size_t) +th_ws_frame_header_write(unsigned char* header, th_ws_frame_type type, size_t len); + #endif diff --git a/src/th_ws_frame_test.c b/src/th_ws_frame_test.c new file mode 100644 index 0000000..cdb6e3e --- /dev/null +++ b/src/th_ws_frame_test.c @@ -0,0 +1,47 @@ +#include "th_test.h" +#include "th_ws_frame.h" + +#include + +TH_TEST_BEGIN(ws_frame) +{ + TH_TEST_CASE_BEGIN(ws_frame_header_write_text_small_payload) + { + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + size_t n = th_ws_frame_header_write(header, TH_WS_FRAME_TEXT, 5); + static const unsigned char expected[] = {0x81, 0x05}; + TH_EXPECT(n == sizeof(expected)); + TH_EXPECT(memcmp(header, expected, sizeof(expected)) == 0); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_frame_header_write_binary_16bit_length) + { + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + size_t n = th_ws_frame_header_write(header, TH_WS_FRAME_BINARY, 300); + static const unsigned char expected[] = {0x82, 126, 0x01, 0x2c}; + TH_EXPECT(n == sizeof(expected)); + TH_EXPECT(memcmp(header, expected, sizeof(expected)) == 0); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_frame_header_write_64bit_length) + { + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + size_t n = th_ws_frame_header_write(header, TH_WS_FRAME_BINARY, 70000); + static const unsigned char expected[] = {0x82, 127, 0, 0, 0, 0, 0, 1, 0x11, 0x70}; + TH_EXPECT(n == sizeof(expected)); + TH_EXPECT(memcmp(header, expected, sizeof(expected)) == 0); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_frame_header_write_close_ping_pong_opcodes) + { + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + TH_EXPECT(th_ws_frame_header_write(header, TH_WS_FRAME_CLOSE, 0) == 2); + TH_EXPECT(header[0] == 0x88); + TH_EXPECT(th_ws_frame_header_write(header, TH_WS_FRAME_PING, 0) == 2); + TH_EXPECT(header[0] == 0x89); + TH_EXPECT(th_ws_frame_header_write(header, TH_WS_FRAME_PONG, 0) == 2); + TH_EXPECT(header[0] == 0x8a); + } + TH_TEST_CASE_END +} +TH_TEST_END diff --git a/src/th_ws_test.c b/src/th_ws_test.c index 781a9b4..1213285 100644 --- a/src/th_ws_test.c +++ b/src/th_ws_test.c @@ -11,6 +11,13 @@ typedef struct th_fake_conn { void (*callback)(void* user_data, size_t size, th_err err); void* user_data; th_err next_recv_err; + + th_iov send_iov[2]; + size_t send_iovcnt; + void (*send_callback)(void* user_data, size_t size, th_err err); + void* send_user_data; + unsigned char sent_buf[256]; + size_t sent_len; } th_fake_conn; static th_address* @@ -42,15 +49,17 @@ th_fake_conn_recv(void* self, void* addr, size_t len, bool exact, th_recv_cb cal static void th_fake_conn_send(void* self, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) { - (void)self; - (void)iov; - (void)iovcnt; (void)file; (void)offset; (void)len; - (void)callback; - (void)user_data; - TH_ASSERT(0 && "not expected to be called in this slice"); + th_fake_conn* conn = self; + TH_ASSERT(conn->send_callback == NULL); + TH_ASSERT(iovcnt <= 2); + for (size_t i = 0; i < iovcnt; ++i) + conn->send_iov[i] = iov[i]; + conn->send_iovcnt = iovcnt; + conn->send_callback = callback; + conn->send_user_data = user_data; } static void @@ -84,6 +93,10 @@ th_fake_conn_init(th_fake_conn* conn) conn->callback = NULL; conn->user_data = NULL; conn->next_recv_err = TH_ERR_EOF; + conn->send_iovcnt = 0; + conn->send_callback = NULL; + conn->send_user_data = NULL; + conn->sent_len = 0; } static void @@ -109,6 +122,48 @@ th_fake_conn_deliver(th_fake_conn* conn, const unsigned char* data, size_t len) callback(user_data, len, TH_ERR_OK); } +// Completes the pending send as if every queued byte went out, without +// recording it anywhere - for tests that only care about the total length, +// too large to fit in sent_buf. +static size_t +th_fake_conn_complete_send_len(th_fake_conn* conn) +{ + TH_ASSERT(conn->send_callback != NULL); + size_t total = 0; + for (size_t i = 0; i < conn->send_iovcnt; ++i) + total += conn->send_iov[i].len; + + void (*callback)(void*, size_t, th_err) = conn->send_callback; + void* user_data = conn->send_user_data; + conn->send_callback = NULL; + conn->send_user_data = NULL; + conn->send_iovcnt = 0; + callback(user_data, total, TH_ERR_OK); + return total; +} + +// Completes the pending send as if every queued byte went out, appending +// it to sent_buf so tests can inspect everything sent so far. +static void +th_fake_conn_complete_send(th_fake_conn* conn) +{ + TH_ASSERT(conn->send_callback != NULL); + size_t total = 0; + for (size_t i = 0; i < conn->send_iovcnt; ++i) { + TH_ASSERT(conn->sent_len + total + conn->send_iov[i].len <= sizeof(conn->sent_buf)); + memcpy(conn->sent_buf + conn->sent_len + total, conn->send_iov[i].base, conn->send_iov[i].len); + total += conn->send_iov[i].len; + } + conn->sent_len += total; + + void (*callback)(void*, size_t, th_err) = conn->send_callback; + void* user_data = conn->send_user_data; + conn->send_callback = NULL; + conn->send_user_data = NULL; + conn->send_iovcnt = 0; + callback(user_data, total, TH_ERR_OK); +} + struct handler_calls { int open_count; int close_count; @@ -303,17 +358,111 @@ TH_TEST_BEGIN(ws) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END - TH_TEST_CASE_BEGIN(ws_send_returns_nosupport) + TH_TEST_CASE_BEGIN(ws_send_writes_unmasked_frame) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_OK); + th_fake_conn_complete_send(&conn); + + static const unsigned char expected[] = {0x81, 0x02, 'h', 'i'}; + TH_EXPECT(conn.sent_len == sizeof(expected)); + TH_EXPECT(memcmp(conn.sent_buf, expected, sizeof(expected)) == 0); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_binary_uses_binary_opcode) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_BINARY) == TH_ERR_OK); + th_fake_conn_complete_send(&conn); + + static const unsigned char expected[] = {0x82, 0x02, 'h', 'i'}; + TH_EXPECT(conn.sent_len == sizeof(expected)); + TH_EXPECT(memcmp(conn.sent_buf, expected, sizeof(expected)) == 0); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_queues_second_send_while_first_in_flight) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_OK); + TH_EXPECT(th_ws_send(ws, (th_buffer){"yo", 2}, TH_WS_TEXT) == TH_ERR_OK); // queued, first still in flight + + th_fake_conn_complete_send(&conn); // finishes "hi" frame, kicks off "yo" frame + th_fake_conn_complete_send(&conn); // finishes "yo" frame + + static const unsigned char expected[] = {0x81, 0x02, 'h', 'i', 0x81, 0x02, 'y', 'o'}; + TH_EXPECT(conn.sent_len == sizeof(expected)); + TH_EXPECT(memcmp(conn.sent_buf, expected, sizeof(expected)) == 0); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_grows_past_the_initial_ring_size) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + static char big[TH_CONFIG_WS_SEND_RING_LEN]; + memset(big, 'x', sizeof(big)); + // bigger than the initial chunk once the frame header is added - grows + // a new chunk instead of rejecting the send + TH_EXPECT(th_ws_send(ws, (th_buffer){big, sizeof(big)}, TH_WS_TEXT) == TH_ERR_OK); + size_t sent = th_fake_conn_complete_send_len(&conn); + TH_EXPECT(sent == sizeof(big) + 4); // 4-byte header: FIN|TEXT, 16-bit extended length + TH_EXPECT(!conn.destroyed); + + conn.next_recv_err = TH_ERR_EOF; + th_fake_conn_run(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_rejects_message_over_send_max_len) { th_ws* ws = NULL; TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); th_ws_start(ws); - TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_NOSUPPORT); + + static char huge[TH_CONFIG_WS_SEND_MAX_LEN + 1]; + memset(huge, 'x', sizeof(huge)); + TH_EXPECT(th_ws_send(ws, (th_buffer){huge, sizeof(huge)}, TH_WS_TEXT) == TH_ERR_INVALID_ARG); + TH_EXPECT(!conn.destroyed); + TH_EXPECT(conn.send_callback == NULL); // nothing was ever queued to send conn.next_recv_err = TH_ERR_EOF; th_fake_conn_run(&conn); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_error_closes_connection) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_OK); + void (*send_callback)(void*, size_t, th_err) = conn.send_callback; + void* send_user_data = conn.send_user_data; + conn.send_callback = NULL; + send_callback(send_user_data, 0, TH_ERR_SYSTEM(TH_EIO)); + + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(ws_close_returns_nosupport) { th_ws* ws = NULL; From ac695667e136b6406d2224c468074de4ac2d0132 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:27:44 -0500 Subject: [PATCH 07/13] feat: implement WebSocket close handshake - th_ws_close queues a CLOSE frame through the normal send path; TH_WS_EVENT_CLOSE fires and the connection tears down once it drains - receiving a CLOSE now echoes one back before closing, instead of dropping the connection without completing the handshake - th_ws_send/th_ws_close return TH_ERR_INVALID_ARG once closing - fix: th_ring_chunk_write crashed on a NULL, zero-length payload (e.g. an empty CLOSE frame) --- include/th.h | 4 +++- src/th_ring.c | 2 ++ src/th_ws.c | 39 ++++++++++++++++++++++++++++++-------- src/th_ws.h | 1 + src/th_ws_test.c | 49 +++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/include/th.h b/include/th.h index a023961..fc68d1d 100644 --- a/include/th.h +++ b/include/th.h @@ -358,13 +358,15 @@ typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffe * opcode (text vs binary), it does not otherwise affect encoding - data * is sent as-is. * @return TH_ERR_SYSTEM(TH_EAGAIN) if the send queue is full - retry - * once a previously queued message has gone out. + * once a previously queued message has gone out. TH_ERR_INVALID_ARG if + * the connection is closing/closed. */ th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_type type); /** th_ws_close * @brief Starts closing the connection. TH_WS_EVENT_CLOSE will be * delivered once the close handshake completes. + * @return TH_ERR_INVALID_ARG if the connection is already closing/closed. */ th_err th_ws_close(th_ws* ws); diff --git a/src/th_ring.c b/src/th_ring.c index 0176bff..1521ab2 100644 --- a/src/th_ring.c +++ b/src/th_ring.c @@ -37,6 +37,8 @@ th_ring_chunk_free_space(const th_ring_chunk* chunk) TH_LOCAL(void) th_ring_chunk_write(th_ring_chunk* chunk, const void* data, size_t len) { + if (len == 0) + return; size_t offset = chunk->tail % chunk->capacity; size_t first = chunk->capacity - offset < len ? chunk->capacity - offset : len; memcpy(chunk->data + offset, data, first); diff --git a/src/th_ws.c b/src/th_ws.c index 45793c8..7797f33 100644 --- a/src/th_ws.c +++ b/src/th_ws.c @@ -17,6 +17,7 @@ th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_ th_buf_vec_init(&ws->payload, ws->allocator); th_ring_init(&ws->send_ring, ws->allocator, TH_CONFIG_WS_SEND_RING_LEN, TH_CONFIG_WS_SEND_MAX_LEN); ws->sending = false; + ws->closing = false; } TH_PRIVATE(void) @@ -57,6 +58,9 @@ th_ws_close_and_destroy(th_ws* ws) TH_LOCAL(void) th_ws_handle_recv(void* user_data, size_t len, th_err err); +TH_LOCAL(th_err) +th_ws_queue_frame(th_ws* ws, th_ws_frame_type frame_type, th_buffer data); + TH_LOCAL(bool) th_ws_consume(th_ws* ws, char* data, size_t len) { @@ -73,8 +77,13 @@ th_ws_consume(th_ws* ws, char* data, size_t len) return false; } - if (type == TH_WS_FRAME_CLOSE) + if (type == TH_WS_FRAME_CLOSE) { + if (!ws->closing) { + ws->closing = true; + th_ws_queue_frame(ws, TH_WS_FRAME_CLOSE, (th_buffer){0}); + } return false; + } if (type == TH_WS_FRAME_PING || type == TH_WS_FRAME_PONG) continue; @@ -100,7 +109,10 @@ th_ws_handle_recv(void* user_data, size_t len, th_err err) return; } if (!th_ws_consume(ws, ws->scratch, len)) { - th_ws_close_and_destroy(ws); + // if closing, a CLOSE frame is now queued/in flight - the send + // path destroys once it drains, so as not to cut it off mid-send + if (!ws->closing) + th_ws_close_and_destroy(ws); return; } th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); @@ -127,6 +139,8 @@ th_ws_send_drain(th_ws* ws) size_t iovcnt = th_ring_peek(&ws->send_ring, ws->send_iov); if (iovcnt == 0) { ws->sending = false; + if (ws->closing) + th_ws_close_and_destroy(ws); return; } ws->sending = true; @@ -146,10 +160,9 @@ th_ws_handle_send(void* user_data, size_t len, th_err err) th_ws_send_drain(ws); } -TH_PUBLIC(th_err) -th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) +TH_LOCAL(th_err) +th_ws_queue_frame(th_ws* ws, th_ws_frame_type frame_type, th_buffer data) { - th_ws_frame_type frame_type = type == TH_WS_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; size_t header_len = th_ws_frame_header_write(header, frame_type, data.len); @@ -166,10 +179,20 @@ th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) return TH_ERR_OK; } +TH_PUBLIC(th_err) +th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) +{ + if (ws->closing) + return TH_ERR_INVALID_ARG; + th_ws_frame_type frame_type = type == TH_WS_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; + return th_ws_queue_frame(ws, frame_type, data); +} + TH_PUBLIC(th_err) th_ws_close(th_ws* ws) { - (void)ws; - TH_LOG_ERROR("WebSocket close handshake is not implemented yet."); - return TH_ERR_NOSUPPORT; + if (ws->closing) + return TH_ERR_INVALID_ARG; + ws->closing = true; + return th_ws_queue_frame(ws, TH_WS_FRAME_CLOSE, (th_buffer){0}); } diff --git a/src/th_ws.h b/src/th_ws.h index 56fdef6..5a3786d 100644 --- a/src/th_ws.h +++ b/src/th_ws.h @@ -23,6 +23,7 @@ struct th_ws { th_ring send_ring; th_iov send_iov[2]; bool sending; + bool closing; // a CLOSE frame is queued/in flight - destroy once send_ring drains }; TH_PRIVATE(void) diff --git a/src/th_ws_test.c b/src/th_ws_test.c index 1213285..9acaefe 100644 --- a/src/th_ws_test.c +++ b/src/th_ws_test.c @@ -304,7 +304,7 @@ TH_TEST_BEGIN(ws) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END - TH_TEST_CASE_BEGIN(ws_recv_close_frame_closes_connection) + TH_TEST_CASE_BEGIN(ws_recv_close_frame_echoes_close_then_destroys) { // masked empty CLOSE frame: FIN|CLOSE, len=0, mask 11 22 33 44 static const unsigned char frame[] = {0x88, 0x80, 0x11, 0x22, 0x33, 0x44}; @@ -315,6 +315,14 @@ TH_TEST_BEGIN(ws) th_fake_conn_deliver(&conn, frame, sizeof(frame)); TH_EXPECT(calls.data_count == 0); + TH_EXPECT(calls.close_count == 0); // not yet - our own CLOSE echo hasn't finished sending + TH_EXPECT(!conn.destroyed); + TH_EXPECT(conn.send_callback != NULL); + + th_fake_conn_complete_send(&conn); + static const unsigned char expected[] = {0x88, 0x00}; // unmasked, empty CLOSE echo + TH_EXPECT(conn.sent_len == sizeof(expected)); + TH_EXPECT(memcmp(conn.sent_buf, expected, sizeof(expected)) == 0); TH_EXPECT(calls.close_count == 1); TH_EXPECT(conn.destroyed); } @@ -463,15 +471,46 @@ TH_TEST_BEGIN(ws) TH_EXPECT(conn.destroyed); } TH_TEST_CASE_END - TH_TEST_CASE_BEGIN(ws_close_returns_nosupport) + TH_TEST_CASE_BEGIN(ws_close_sends_close_frame_then_destroys) { th_ws* ws = NULL; TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); th_ws_start(ws); - TH_EXPECT(th_ws_close(ws) == TH_ERR_NOSUPPORT); - conn.next_recv_err = TH_ERR_EOF; - th_fake_conn_run(&conn); + TH_EXPECT(th_ws_close(ws) == TH_ERR_OK); + TH_EXPECT(calls.close_count == 0); // not yet - waiting for our CLOSE frame to finish sending + TH_EXPECT(!conn.destroyed); + + th_fake_conn_complete_send(&conn); + static const unsigned char expected[] = {0x88, 0x00}; + TH_EXPECT(conn.sent_len == sizeof(expected)); + TH_EXPECT(memcmp(conn.sent_buf, expected, sizeof(expected)) == 0); + TH_EXPECT(calls.close_count == 1); + TH_EXPECT(conn.destroyed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_close_twice_returns_invalid_arg) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_close(ws) == TH_ERR_OK); + TH_EXPECT(th_ws_close(ws) == TH_ERR_INVALID_ARG); + + th_fake_conn_complete_send(&conn); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(ws_send_after_close_returns_invalid_arg) + { + th_ws* ws = NULL; + TH_EXPECT(th_ws_create(&ws, &conn.base, th_test_ws_handler, &calls, NULL) == TH_ERR_OK); + th_ws_start(ws); + + TH_EXPECT(th_ws_close(ws) == TH_ERR_OK); + TH_EXPECT(th_ws_send(ws, (th_buffer){"hi", 2}, TH_WS_TEXT) == TH_ERR_INVALID_ARG); + + th_fake_conn_complete_send(&conn); } TH_TEST_CASE_END } From 6160c4e612f8ad5ed392f8fc8fe74592187bbb97 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:35:26 -0500 Subject: [PATCH 08/13] docs: clean up th.h comments --- include/th.h | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/include/th.h b/include/th.h index fc68d1d..e95f4cd 100644 --- a/include/th.h +++ b/include/th.h @@ -347,19 +347,15 @@ typedef enum th_ws_type { /** th_ws_handler * @brief WebSocket event callback. data is empty for TH_WS_EVENT_OPEN/CLOSE, - * and holds one complete message's payload for TH_WS_EVENT_DATA - type is - * only meaningful for TH_WS_EVENT_DATA. ws must not be used after - * TH_WS_EVENT_CLOSE has been delivered. + * data and type is only useful for TH_WS_EVENT_DATA + * ws must not be used after TH_WS_EVENT_CLOSE has been delivered. */ typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type); /** th_ws_send - * @brief Queues one WebSocket message for sending. type selects the - * opcode (text vs binary), it does not otherwise affect encoding - data - * is sent as-is. - * @return TH_ERR_SYSTEM(TH_EAGAIN) if the send queue is full - retry - * once a previously queued message has gone out. TH_ERR_INVALID_ARG if - * the connection is closing/closed. + * @brief Queues one WebSocket message for sending. + * @return TH_ERR_SYSTEM(TH_EAGAIN) if the send queue is full. + * @return TH_ERR_INVALID_ARG if the connection is closing/closed. */ th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_type type); @@ -403,7 +399,7 @@ th_err th_route(th_server* server, th_method method, const char* route, th_handl */ th_err th_route_ws(th_server* server, const char* path, th_ws_handler handler, void* userp); -/** th_err +/** th_add_dir * @brief Add a directory to the server (for serving or storing files). * @param name Label for the directory. * @param path File system path. From 5746621674e75891a0b71814eb802f04b874799e Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:35:58 -0500 Subject: [PATCH 09/13] build: add websocket example to CMakeLists.txt --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b36afd6..cb221de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,7 @@ if (NOT TH_DISABLE_EXAMPLES) add_tiny_http_example(file_server) add_tiny_http_example(file_upload) add_tiny_http_example(hello_world) + add_tiny_http_example(websocket) endif() # Tests From 8931eac22467971857f1ba86167acd3f98d4be76 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:38:16 -0500 Subject: [PATCH 10/13] chore: update amalgamation --- th.c | 12880 +++++++++++++++++++++++++++++++-------------------------- th.h | 56 +- 2 files changed, 7113 insertions(+), 5823 deletions(-) diff --git a/th.c b/th.c index da78ce1..02cd537 100644 --- a/th.c +++ b/th.c @@ -31,6 +31,18 @@ #define TH_MAX_BODY_LEN (4 * 1024 * 1024) #endif +#ifndef TH_CONFIG_WS_MAX_MESSAGE_LEN +#define TH_CONFIG_WS_MAX_MESSAGE_LEN (4 * 1024 * 1024) +#endif + +#ifndef TH_CONFIG_WS_SEND_RING_LEN +#define TH_CONFIG_WS_SEND_RING_LEN (16 * 1024) +#endif + +#ifndef TH_CONFIG_WS_SEND_MAX_LEN +#define TH_CONFIG_WS_SEND_MAX_LEN (256 * 1024) +#endif + /* feature configuration end */ #if defined(__APPLE__) @@ -237,6 +249,13 @@ th_str_from_cstr(const char* str) TH_PRIVATE(bool) th_str_eq(th_str a, th_str b); +/** th_str_ieq + * @brief Case-insensitive version of th_str_eq. + * @return 1 if the strings are equal ignoring case, 0 otherwise. + */ +TH_PRIVATE(bool) +th_str_ieq(th_str a, th_str b); + /** th_str_empty * @brief Helper function to check if a th_str is empty. * @return true if the string is empty, false otherwise. @@ -301,808 +320,1059 @@ TH_PRIVATE(size_t) th_str_hash(th_str str); /* End of th_str.h */ -/* Start of th_cookie_parser.h */ - +/* Start of th_log.h */ -#include +#include +#include -/** th_cookie_parser - * @brief Incremental parser over a Cookie request header value - * (RFC 6265 section 4.2.1: cookie-string = cookie-pair *( ";" SP cookie-pair )). - * Non-owning: the underlying bytes must outlive the parser. Call - * th_cookie_parser_next repeatedly until th_cookie_parser_done is true. - */ -typedef struct th_cookie_parser { - th_str str; - size_t pos; -} th_cookie_parser; -/** th_cookie_parser_init - * @brief Initializes parser to walk cookie_header from the start. - */ -TH_PRIVATE(void) -th_cookie_parser_init(th_cookie_parser* parser, th_str cookie_header); +#ifndef TH_LOG_LEVEL +#define TH_LOG_LEVEL TH_LOG_LEVEL_INFO +#endif -/** th_cookie_parser_done - * @brief Returns true once the whole header has been consumed - either by - * th_cookie_parser_next reaching the end, or after it has returned an error. - * No more pairs remain to be parsed either way. - */ -TH_PRIVATE(bool) -th_cookie_parser_done(const th_cookie_parser* parser); +#define TH_LOG_TAG "default" -/** th_cookie_parser_next - * @brief Parses the next "name=value" - * - * cookie-name is validated against RFC 2616's token (no CTLs, and none of - * the separators "()<>@,;:\"/[]?={} SP HT). - * - * cookie-value is validated against RFC 6265's cookie-octet - * (%x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - printable ASCII minus space, DQUOTE, comma, semicolon, - * backslash), or the quoted form (DQUOTE *cookie-octet DQUOTE), with the - * surrounding DQUOTEs stripped. - * - * @return TH_ERR_OK on success, with *key / *value filled. - * @return TH_ERR_HTTP(TH_CODE_BAD_REQUEST) - */ -TH_PRIVATE(th_err) -th_cookie_parser_next(th_cookie_parser* parser, th_str* key, th_str* value); +TH_PRIVATE(th_log*) +th_default_log_get(void); -/* End of th_cookie_parser.h */ -/* Start of th_fmt.h */ +TH_PRIVATE(void) +th_log_printf(int level, const char* fmt, ...) TH_MAYBE_UNUSED TH_PRINTF_FMT(2, 3); +#if TH_LOG_LEVEL <= TH_LOG_LEVEL_TRACE +#define TH_LOG_TRACE(...) th_log_printf(TH_LOG_LEVEL_TRACE, "TRACE: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_TRACE(...) ((void)0) +#endif -#include -#include -#include +#if TH_LOG_LEVEL <= TH_LOG_LEVEL_DEBUG +#define TH_LOG_DEBUG(...) th_log_printf(TH_LOG_LEVEL_DEBUG, "DEBUG: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_DEBUG(...) ((void)0) +#endif +#if (TH_LOG_LEVEL <= TH_LOG_LEVEL_INFO) +#define TH_LOG_INFO(...) th_log_printf(TH_LOG_LEVEL_INFO, "INFO: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_INFO(...) ((void)0) +#endif -TH_PRIVATE(const char*) -th_fmt_uint_to_str(char* buf, size_t len, unsigned int val); +#if TH_LOG_LEVEL <= TH_LOG_LEVEL_WARN +#define TH_LOG_WARN(...) th_log_printf(TH_LOG_LEVEL_WARN, "WARN: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_WARN(...) ((void)0) +#endif -TH_PRIVATE(const char*) -th_fmt_uint_to_str_ex(char* buf, size_t len, unsigned int val, size_t* out_len); +#if TH_LOG_LEVEL <= TH_LOG_LEVEL_ERROR +#define TH_LOG_ERROR(...) th_log_printf(TH_LOG_LEVEL_ERROR, "ERROR: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_ERROR(...) ((void)0) +#endif -/** th_fmt_str_append - * @brief Append a string to a buffer. - * @param buf The buffer to append to. - * @param pos The current position in the buffer (where to append). - * @param len The length of the buffer. - * @param str The string to append. - * @return The number of characters appended. - */ -TH_PRIVATE(size_t) -th_fmt_str_append(char* buf, size_t pos, size_t len, const char* str); +#if TH_LOG_LEVEL <= TH_LOG_LEVEL_FATAL +#define TH_LOG_FATAL(...) th_log_printf(TH_LOG_LEVEL_FATAL, "FATAL: [" TH_LOG_TAG "] " __VA_ARGS__) +#else +#define TH_LOG_FATAL(...) ((void)0) +#endif -TH_PRIVATE(size_t) -th_fmt_strn_append(char* buf, size_t pos, size_t len, const char* str, size_t n); +/* End of th_log.h */ +/* Start of th_utility.h */ -TH_PRIVATE(size_t) -th_fmt_strtime(char* buf, size_t len, th_date date); -/* End of th_fmt.h */ -/* Start of th_system_error.h */ +#include -#if defined(TH_CONFIG_OS_POSIX) -#include -#include -#elif defined(TH_CONFIG_OS_WIN) -#include -#endif +#define TH_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define TH_MAX(a, b) ((a) > (b) ? (a) : (b)) +#define TH_ABS(a) ((a) < 0 ? -(a) : (a)) -TH_INLINE(const char*) -th_system_strerror(int errc) TH_MAYBE_UNUSED; +#define TH_ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) -TH_INLINE(const char*) -th_system_strerror(int errc) +// Move a pointer from src to dst and set src to NULL +TH_INLINE(void*) +th_move_ptr(void** src) { -#if defined(TH_CONFIG_OS_POSIX) - return strerror(errc); -#elif defined(TH_CONFIG_OS_WIN) - static char buf[256]; - FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf, sizeof(buf), NULL); - return buf; -#endif + void* dst = *src; + *src = NULL; + return dst; } -/* Define the system error codes that we use */ -#if defined(TH_CONFIG_OS_POSIX) -#define TH_ENOENT ENOENT -#define TH_EINTR EINTR -#define TH_EIO EIO -#define TH_EBADF EBADF -#define TH_EBUSY EBUSY -#define TH_EAGAIN EAGAIN -#define TH_EWOULDBLOCK EWOULDBLOCK -#define TH_ENOMEM ENOMEM -#define TH_ENOSYS ENOSYS -#define TH_ETIMEDOUT ETIMEDOUT -#define TH_ECANCELED ECANCELED -#elif defined(TH_CONFIG_OS_WIN) -#define TH_ENOENT ERROR_FILE_NOT_FOUND -#define TH_EINTR ERROR_INTERRUPT -#define TH_EIO ERROR_IO_DEVICE -#define TH_EBADF ERROR_BAD_FORMAT -#define TH_EBUSY ERROR_BUSY -#define TH_EAGAIN ERROR_RETRY -#define TH_EWOULDBLOCK ERROR_RETRY -#define TH_ENOMEM ERROR_OUTOFMEMORY -#define TH_ENOSYS ERROR_NOT_SUPPORTED -#define TH_ETIMEDOUT ERROR_TIMEOUT -#define TH_ECANCELED ERROR_CANCELLED -#endif - -/* End of th_system_error.h */ -/* Start of th_http_error.h */ +#define TH_MOVE_PTR(ptr) th_move_ptr((void**)&(ptr)) +// Custom assert macros +#ifndef NDEBUG +#define TH_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + TH_LOG_FATAL("Assertion failed: %s at %s:%d", #cond, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) +#else +#define TH_ASSERT(cond) ((void)0) +#endif -#include +// Mathematical utility functions -/** th_http_err - * @brief Converts a error code to a equivalent HTTP error code. - */ -TH_INLINE(th_err) -th_http_error(th_err err) +TH_INLINE(size_t) +th_next_pow2(size_t n) { - if (err == TH_ERR_OK) - return TH_ERR_HTTP(TH_CODE_OK); - switch (TH_ERR_CATEGORY(err)) { - case TH_ERR_CATEGORY_SYSTEM: - switch (TH_ERR_CODE(err)) { - case TH_ENOENT: - return TH_ERR_HTTP(TH_CODE_NOT_FOUND); - break; - case TH_ETIMEDOUT: - return TH_ERR_HTTP(TH_CODE_REQUEST_TIMEOUT); - break; - default: - return TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR); - break; - } - break; - case TH_ERR_CATEGORY_HTTP: - return err; - break; - default: - break; - } - return TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR); + TH_ASSERT(n > 0); + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + n++; + return n; } -TH_INLINE(const char*) -th_http_strerror(int code) -{ - switch (code) { - case TH_CODE_OK: - return "OK"; - break; - case TH_CODE_MOVED_PERMANENTLY: - return "Moved Permanently"; - break; - case TH_CODE_BAD_REQUEST: - return "Bad Request"; - break; - case TH_CODE_NOT_FOUND: - return "Not Found"; - break; - case TH_CODE_METHOD_NOT_ALLOWED: - return "Method Not Allowed"; - break; - case TH_CODE_PAYLOAD_TOO_LARGE: - return "Payload Too Large"; - break; - case TH_CODE_INTERNAL_SERVER_ERROR: - return "Internal Server Error"; - break; - case TH_CODE_SERVICE_UNAVAILABLE: - return "Service Unavailable"; - break; - case TH_CODE_NOT_IMPLEMENTED: - return "Method Not Implemented"; - break; - case TH_CODE_REQUEST_TIMEOUT: - return "Request Timeout"; - break; - case TH_CODE_TOO_MANY_REQUESTS: - return "Too Many Requests"; - break; - case TH_CODE_URI_TOO_LONG: - return "URI Too Long"; - break; - case TH_CODE_UNSUPPORTED_MEDIA_TYPE: - return "Unsupported Media Type"; - break; - case TH_CODE_RANGE_NOT_SATISFIABLE: - return "Range Not Satisfiable"; - break; - case TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE: - return "Request Header Fields Too Large"; - break; - case TH_CODE_UNAUTHORIZED: - return "Unauthorized"; - break; - case TH_CODE_FORBIDDEN: - return "Forbidden"; - break; - default: - return "Unknown"; - break; - } -} +/* End of th_utility.h */ +/* Start of th_list.h */ -typedef enum th_http_code_type { - TH_HTTP_CODE_TYPE_INFORMATIONAL, - TH_HTTP_CODE_TYPE_SUCCESS, - TH_HTTP_CODE_TYPE_REDIRECT, - TH_HTTP_CODE_TYPE_CLIENT_ERROR, - TH_HTTP_CODE_TYPE_SERVER_ERROR, -} th_http_code_type; -TH_INLINE(th_http_code_type) -th_http_code_get_type(int code) -{ - if (code >= 100 && code < 200) - return TH_HTTP_CODE_TYPE_INFORMATIONAL; - if (code >= 200 && code < 300) - return TH_HTTP_CODE_TYPE_SUCCESS; - if (code >= 300 && code < 400) - return TH_HTTP_CODE_TYPE_REDIRECT; - if (code >= 400 && code < 500) - return TH_HTTP_CODE_TYPE_CLIENT_ERROR; - if (code >= 500 && code < 600) - return TH_HTTP_CODE_TYPE_SERVER_ERROR; - return TH_HTTP_CODE_TYPE_SERVER_ERROR; -} +/** Generic doubly linked list implementation. + * that works with any struct that has a next and prev pointer. + */ +#define TH_DEFINE_LIST(NAME, T, PREV, NEXT) \ + typedef struct NAME { \ + T* head; \ + T* tail; \ + } NAME; \ + \ + TH_INLINE(void) \ + NAME##_push_back(NAME* list, T* item) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(T*) \ + NAME##_pop_front(NAME* list) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(T*) \ + NAME##_front(NAME* list) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_erase(NAME* list, T* item) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(T*) \ + NAME##_next(T* item) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_push_back(NAME* list, T* item) \ + { \ + TH_ASSERT(item != NULL); \ + if (list->head == NULL) { \ + list->head = item; \ + item->PREV = NULL; \ + } else { \ + list->tail->NEXT = item; \ + item->PREV = list->tail; \ + } \ + list->tail = item; \ + item->NEXT = NULL; \ + } \ + \ + TH_INLINE(T*) \ + NAME##_pop_front(NAME* list) \ + { \ + T* item = list->head; \ + if (item) { \ + list->head = item->NEXT; \ + if (list->head) { \ + list->head->PREV = NULL; \ + } else { \ + list->tail = NULL; \ + } \ + item->NEXT = NULL; \ + } \ + return item; \ + } \ + \ + TH_INLINE(T*) \ + NAME##_front(NAME* list) \ + { \ + return list->head; \ + } \ + \ + TH_INLINE(void) \ + NAME##_erase(NAME* list, T* item) \ + { \ + TH_ASSERT(item != NULL); \ + TH_ASSERT((item->NEXT || item == list->tail) && "Item is not in the list"); \ + TH_ASSERT((item->PREV || item == list->head) && "Item is not in the list"); \ + T* next = item->NEXT; \ + T* prev = item->PREV; \ + if (prev) { \ + prev->NEXT = next; \ + item->PREV = NULL; \ + } else { \ + list->head = next; \ + } \ + if (next) { \ + next->PREV = prev; \ + item->NEXT = NULL; \ + } else { \ + list->tail = prev; \ + } \ + } \ + \ + TH_INLINE(T*) \ + NAME##_next(T* item) \ + { \ + return item->NEXT; \ + } -/* End of th_http_error.h */ -/* Start of th_address.h */ +/* End of th_list.h */ +/* Start of th_allocator.h */ +#include +#include -#include +TH_INLINE(void*) +th_allocator_alloc(th_allocator* allocator, size_t size) +{ + return allocator->alloc(allocator, size); +} -/** th_address - * @brief Storage for a peer address filled in by th_acceptor_ops.accept. - */ -typedef struct th_address { - struct sockaddr_storage addr; - socklen_t addrlen; -} th_address; +TH_INLINE(void*) +th_allocator_realloc(th_allocator* allocator, void* ptr, size_t size) +{ + return allocator->realloc(allocator, ptr, size); +} TH_INLINE(void) -th_address_init(th_address* addr) +th_allocator_free(th_allocator* allocator, void* ptr) { - addr->addrlen = sizeof(addr->addr); + allocator->free(allocator, ptr); } -/* End of th_address.h */ -/* Start of th_dir.h */ +TH_PRIVATE(th_allocator*) +th_default_allocator_get(void); +/* th_arena_allocator begin */ +typedef struct th_arena_allocator { + th_allocator base; + th_allocator* allocator; + void* buf; + size_t size; + size_t pos; + size_t prev_pos; + uint16_t alignment; +} th_arena_allocator; -/** th_dir_ops - * @brief The raw open/close syscalls a th_dir performs. Injected at - * construction time so tests can fake a directory fd without touching the - * filesystem. open behaves like the underlying syscall: TH_ERR_OK with - * *fd set on success, TH_ERR_SYSTEM(errno) on failure. +/** th_arena_allocator_init + * @brief The arena allocator is a simple allocator that allocates memory from a fixed-size buffer. + * It only frees memory when the free operation is called on the previously allocated memory. + * If no memory is available in the buffer, it will fall back to the default allocator. + * @param allocator The arena allocator to initialize. + * @param buf The buffer to use for allocations. + * @param size The size of the buffer. */ -typedef struct th_dir_ops { - th_err (*open)(void* self, const char* path, int* fd); - void (*close)(void* self, int fd); -} th_dir_ops; - -TH_PRIVATE(th_dir_ops*) -th_dir_ops_os(void); - -typedef struct th_dir { - th_dir_ops* ops; - int fd; -} th_dir; - -TH_PRIVATE(void) -th_dir_init(th_dir* dir, th_dir_ops* ops); - -TH_PRIVATE(th_err) -th_dir_open(th_dir* dir, th_str path); - TH_PRIVATE(void) -th_dir_deinit(th_dir* dir); - -/* End of th_dir.h */ -/* Start of th_filepath.h */ - - +th_arena_allocator_init(th_arena_allocator* allocator, void* buf, size_t size, th_allocator* fallback); -/** th_filepath - * @brief A validated, NUL-terminated relative path, ready to pass to a - * syscall. th_filepath_init rejects absolute paths and "." / ".." - * components - openat(dir->fd, ...) doesn't confine resolution to dir, a - * ".." component walks back out of it like normal path resolution - so - * any path built from untrusted input (e.g. a client-supplied filename) - * must go through this first. +/** th_arena_allocator_init_with_alignment + * @brief Just like th_arena_allocator_init, but allows specifying the alignment of the allocations. */ -typedef struct th_filepath { - char buf[TH_CONFIG_MAX_PATH_LEN + 1]; -} th_filepath; +TH_PRIVATE(void) +th_arena_allocator_init_with_alignment(th_arena_allocator* allocator, void* buf, size_t size, size_t alignment, th_allocator* fallback); -/** th_filepath_init - * @brief Fills path with str NUL-terminated. - * @return TH_ERR_INVALID_ARG if str is absolute, too long, empty, or has - * a "." / ".." component. +/* th_arena_allocator end */ +/** Generic object pool allocator. + * The pool allocator is a allocator that allocates objects from a pool of fixed-size blocks. + * It can be used with any object that has a next and prev pointer. */ -TH_PRIVATE(th_err) -th_filepath_init(th_filepath* path, th_str str); - -TH_INLINE(const char*) -th_filepath_cstr(const th_filepath* path) -{ - return path->buf; -} - -/* End of th_filepath.h */ -/* Start of th_file.h */ +#define TH_DEFINE_POOL_ALLOCATOR(NAME, T, PREV, NEXT) \ + TH_DEFINE_LIST(NAME##_list, T, PREV, NEXT) \ + typedef struct NAME { \ + th_allocator base; \ + NAME##_list free_list; \ + NAME##_list used_list; \ + th_allocator* allocator; \ + size_t count; \ + size_t max; \ + } NAME; \ + \ + TH_INLINE(void) \ + NAME##_init(NAME* pool, th_allocator* allocator, size_t initial, size_t max) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_deinit(NAME* pool) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void*) \ + NAME##_alloc(void* self, size_t) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_free(void* self, void* ptr) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_init(NAME* pool, th_allocator* allocator, size_t initial, size_t max) \ + { \ + TH_ASSERT(allocator != NULL && "Invalid allocator"); \ + TH_ASSERT(max > 0 && "Invalid max"); \ + pool->base.alloc = NAME##_alloc; \ + pool->base.realloc = NULL; \ + pool->base.free = NAME##_free; \ + pool->allocator = allocator; \ + pool->count = 0; \ + pool->max = max; \ + pool->used_list = (NAME##_list){0}; \ + pool->free_list = (NAME##_list){0}; \ + for (size_t i = 0; i < initial; i++) { \ + T* item = (T*)th_allocator_alloc(pool->allocator, sizeof(T)); \ + if (item) { \ + NAME##_list_push_back(&pool->free_list, item); \ + ++pool->count; \ + } \ + } \ + } \ + \ + TH_INLINE(void) \ + NAME##_deinit(NAME* pool) \ + { \ + T* item = NULL; \ + while ((item = NAME##_list_pop_front(&pool->free_list))) { \ + th_allocator_free(pool->allocator, item); \ + } \ + item = NAME##_list_pop_front(&pool->used_list); \ + TH_ASSERT(item == NULL); \ + } \ + \ + TH_INLINE(void*) \ + NAME##_alloc(void* self, size_t size) \ + { \ + TH_ASSERT(size == sizeof(T) && "Invalid size"); \ + (void)size; \ + NAME* pool = (NAME*)self; \ + T* item = NAME##_list_pop_front(&pool->free_list); \ + if (item == NULL) { \ + if (pool->count < pool->max) { \ + item = (T*)th_allocator_alloc(pool->allocator, sizeof(T)); \ + if (item) { \ + pool->count++; \ + } \ + } \ + } \ + if (item) { \ + NAME##_list_push_back(&pool->used_list, item); \ + } \ + return item; \ + } \ + \ + TH_INLINE(void) \ + NAME##_free(void* self, void* ptr) \ + { \ + NAME* pool = (NAME*)self; \ + T* item = (T*)ptr; \ + if (item) { \ + NAME##_list_erase(&pool->used_list, item); \ + NAME##_list_push_back(&pool->free_list, item); \ + } \ + } + +/* End of th_allocator.h */ +/* Start of th_vec.h */ + + + +#include + +#define TH_DEFINE_VEC(NAME, TYPE, DEINIT) \ + typedef struct NAME { \ + TYPE* data; \ + size_t size; \ + size_t capacity; \ + th_allocator* allocator; \ + } NAME; \ + \ + TH_INLINE(void) \ + NAME##_init(NAME* vec, th_allocator* allocator) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_clear(NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_deinit(NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(size_t) \ + NAME##_size(const NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(size_t) \ + NAME##_capacity(const NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(th_err) \ + NAME##_resize(NAME* vec, size_t size) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(th_err) \ + NAME##_push_back(NAME* vec, TYPE value) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(TYPE*) \ + NAME##_at(NAME* vec, size_t index) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(const TYPE*) \ + NAME##_cat(const NAME* vec, size_t index) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(TYPE*) \ + NAME##_begin(NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(TYPE*) \ + NAME##_end(NAME* vec) TH_MAYBE_UNUSED; \ + \ + TH_INLINE(void) \ + NAME##_init(NAME* vec, th_allocator* allocator) \ + { \ + vec->allocator = allocator ? allocator : th_default_allocator_get(); \ + vec->capacity = 0; \ + vec->size = 0; \ + vec->data = NULL; \ + } \ + \ + TH_INLINE(void) \ + NAME##_deinit(NAME* vec) \ + { \ + if (vec->data) { \ + for (size_t i = 0; i < vec->size; i++) { \ + DEINIT(&vec->data[i]); \ + } \ + th_allocator_free(vec->allocator, vec->data); \ + } \ + } \ + \ + TH_INLINE(void) \ + NAME##_clear(NAME* vec) \ + { \ + if (vec->data) { \ + for (size_t i = 0; i < vec->size; i++) { \ + DEINIT(&vec->data[i]); \ + } \ + } \ + vec->size = 0; \ + } \ + \ + TH_INLINE(size_t) \ + NAME##_size(const NAME* vec) \ + { \ + return vec->size; \ + } \ + \ + TH_INLINE(size_t) \ + NAME##_capacity(const NAME* vec) \ + { \ + return vec->capacity; \ + } \ + \ + TH_INLINE(th_err) \ + NAME##_resize(NAME* vec, size_t size) \ + { \ + if (size < vec->size) { \ + vec->size = size; \ + return TH_ERR_OK; \ + } \ + if (size > vec->capacity) { \ + size_t new_capacity = th_next_pow2(size); \ + TYPE* new_data = th_allocator_realloc(vec->allocator, vec->data, new_capacity * sizeof(TYPE)); \ + if (new_data == NULL) { \ + return TH_ERR_BAD_ALLOC; \ + } \ + vec->data = new_data; \ + vec->capacity = new_capacity; \ + } \ + vec->size = size; \ + return TH_ERR_OK; \ + } \ + \ + TH_INLINE(th_err) \ + NAME##_push_back(NAME* vec, TYPE value) \ + { \ + if (vec->size >= vec->capacity) { \ + size_t new_capacity = vec->capacity == 0 ? 1 : vec->capacity * 2; \ + TYPE* new_data = th_allocator_realloc(vec->allocator, vec->data, new_capacity * sizeof(TYPE)); \ + if (new_data == NULL) { \ + return TH_ERR_BAD_ALLOC; \ + } \ + vec->data = new_data; \ + vec->capacity = new_capacity; \ + } \ + vec->data[vec->size++] = value; \ + return TH_ERR_OK; \ + } \ + \ + TH_INLINE(TYPE*) \ + NAME##_at(NAME* vec, size_t index) \ + { \ + TH_ASSERT(index <= vec->size); \ + return vec->data + index; \ + } \ + \ + TH_INLINE(const TYPE*) \ + NAME##_cat(const NAME* vec, size_t index) \ + { \ + TH_ASSERT(index <= vec->size); \ + return vec->data + index; \ + } \ + \ + TH_INLINE(TYPE*) \ + NAME##_begin(NAME* vec) \ + { \ + return vec->data; \ + } \ + \ + TH_INLINE(TYPE*) \ + NAME##_end(NAME* vec) \ + { \ + return vec->data + vec->size; \ + } + +// Default vectors +TH_DEFINE_VEC(th_buf_vec, char, (void)) + +/* End of th_vec.h */ +/* Start of th_string.h */ + + +typedef struct th_detail_large_string { + size_t capacity; + size_t len; + char* ptr; + th_allocator* allocator; +} th_detail_large_string; + +#define TH_STRING_SMALL_BUF_LEN (sizeof(char*) + sizeof(size_t) + sizeof(size_t) - 1) +#define TH_STRING_SMALL_MAX_LEN (TH_STRING_SMALL_BUF_LEN - 1) +typedef struct th_detail_small_string { + unsigned char small : 1; + unsigned char len : 7; + char buf[TH_STRING_SMALL_BUF_LEN]; + th_allocator* allocator; +} th_detail_small_string; + +typedef struct th_string { + union { + th_detail_small_string small; + th_detail_large_string large; + } impl; +} th_string; + +TH_PRIVATE(void) +th_string_init(th_string* self, th_allocator* allocator); + +TH_PRIVATE(th_err) +th_string_init_with(th_string* self, th_str str, th_allocator* allocator); + +TH_PRIVATE(th_err) +th_string_set(th_string* self, th_str str); + +TH_PRIVATE(th_err) +th_string_append(th_string* self, th_str str); + +TH_PRIVATE(th_err) +th_string_append_cstr(th_string* self, const char* str); + +TH_PRIVATE(th_err) +th_string_push_back(th_string* self, char c); + +TH_PRIVATE(th_err) +th_string_resize(th_string* self, size_t new_len, char fill); + +TH_PRIVATE(th_str) +th_string_view(const th_string* self); + +TH_PRIVATE(char*) +th_string_at(th_string* self, size_t index); + +TH_PRIVATE(const char*) +th_string_data(const th_string* self); + +TH_PRIVATE(size_t) +th_string_len(const th_string* self); + +TH_PRIVATE(void) +th_string_deinit(th_string* self); + +TH_PRIVATE(void) +th_string_clear(th_string* self); + +TH_PRIVATE(void) +th_string_to_lower(th_string* self); + +TH_PRIVATE(bool) +th_string_eq(const th_string* self, th_str other); + +// TH_PRIVATE(uint32_t) +// th_string_hash(const th_string* self); + +TH_DEFINE_VEC(th_string_vec, th_string, th_string_deinit) + +/* End of th_string.h */ +/* Start of th_base64.h */ + + +/** th_base64_encode + * @brief Base64-encodes input, overwriting output with the result. + */ +TH_PRIVATE(th_err) +th_base64_encode(th_str input, th_string* output); + +/* End of th_base64.h */ +/* Start of th_cookie_parser.h */ + + + +#include + +/** th_cookie_parser + * @brief Incremental parser over a Cookie request header value + * (RFC 6265 section 4.2.1: cookie-string = cookie-pair *( ";" SP cookie-pair )). + * Non-owning: the underlying bytes must outlive the parser. Call + * th_cookie_parser_next repeatedly until th_cookie_parser_done is true. + */ +typedef struct th_cookie_parser { + th_str str; + size_t pos; +} th_cookie_parser; +/** th_cookie_parser_init + * @brief Initializes parser to walk cookie_header from the start. + */ +TH_PRIVATE(void) +th_cookie_parser_init(th_cookie_parser* parser, th_str cookie_header); +/** th_cookie_parser_done + * @brief Returns true once the whole header has been consumed - either by + * th_cookie_parser_next reaching the end, or after it has returned an error. + * No more pairs remain to be parsed either way. + */ +TH_PRIVATE(bool) +th_cookie_parser_done(const th_cookie_parser* parser); -#include +/** th_cookie_parser_next + * @brief Parses the next "name=value" + * + * cookie-name is validated against RFC 2616's token (no CTLs, and none of + * the separators "()<>@,;:\"/[]?={} SP HT). + * + * cookie-value is validated against RFC 6265's cookie-octet + * (%x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - printable ASCII minus space, DQUOTE, comma, semicolon, + * backslash), or the quoted form (DQUOTE *cookie-octet DQUOTE), with the + * surrounding DQUOTEs stripped. + * + * @return TH_ERR_OK on success, with *key / *value filled. + * @return TH_ERR_HTTP(TH_CODE_BAD_REQUEST) + */ +TH_PRIVATE(th_err) +th_cookie_parser_next(th_cookie_parser* parser, th_str* key, th_str* value); -typedef struct th_open_opt { - bool read; - bool write; - bool create; - bool truncate; -} th_open_opt; +/* End of th_cookie_parser.h */ +/* Start of th_fmt.h */ -/** th_file_ops - * @brief The raw syscalls a th_file performs. Injected at construction time - * so tests can fake a file fd without touching the filesystem. Each method - * behaves like the underlying syscall: TH_ERR_OK (with any out-params set) - * on success, TH_ERR_SYSTEM(errno) on failure. - */ -typedef struct th_file_ops { - th_err (*openat)(void* self, int dirfd, const char* path, int flags, int* fd); - th_err (*seek)(void* self, int fd, int whence, size_t* pos); - th_err (*read)(void* self, int fd, void* addr, size_t len, size_t offset, size_t* read); - th_err (*write)(void* self, int fd, const void* addr, size_t len, size_t offset, size_t* written); - th_err (*stat)(void* self, int fd, struct stat* out); - void (*close)(void* self, int fd); -} th_file_ops; -TH_PRIVATE(th_file_ops*) -th_file_ops_os(void); +#include +#include +#include -typedef struct th_file { - th_file_ops* ops; - int fd; - size_t size; -} th_file; -TH_PRIVATE(void) -th_file_init(th_file* stream, th_file_ops* ops); +TH_PRIVATE(const char*) +th_fmt_uint_to_str(char* buf, size_t len, unsigned int val); -TH_PRIVATE(th_err) -th_file_openat(th_file* stream, th_dir* dir, const th_filepath* path, th_open_opt opt); +TH_PRIVATE(const char*) +th_fmt_uint_to_str_ex(char* buf, size_t len, unsigned int val, size_t* out_len); -TH_PRIVATE(th_err) -th_file_read(th_file* stream, void* addr, size_t len, size_t offset, size_t* read) TH_MAYBE_UNUSED; +/** th_fmt_str_append + * @brief Append a string to a buffer. + * @param buf The buffer to append to. + * @param pos The current position in the buffer (where to append). + * @param len The length of the buffer. + * @param str The string to append. + * @return The number of characters appended. + */ +TH_PRIVATE(size_t) +th_fmt_str_append(char* buf, size_t pos, size_t len, const char* str); -TH_PRIVATE(th_err) -th_file_write(th_file* stream, const void* addr, size_t len, size_t offset, size_t* written) TH_MAYBE_UNUSED; +TH_PRIVATE(size_t) +th_fmt_strn_append(char* buf, size_t pos, size_t len, const char* str, size_t n); -TH_PRIVATE(uint32_t) -th_file_stat_hash(th_file* stream); +TH_PRIVATE(size_t) +th_fmt_strtime(char* buf, size_t len, th_date date); +/* End of th_fmt.h */ +/* Start of th_system_error.h */ -TH_PRIVATE(void) -th_file_close(th_file* stream); -TH_PRIVATE(void) -th_file_deinit(th_file* stream); +#if defined(TH_CONFIG_OS_POSIX) +#include +#include +#elif defined(TH_CONFIG_OS_WIN) +#include +#endif -/* End of th_file.h */ -/* Start of th_iov.h */ +TH_INLINE(const char*) +th_system_strerror(int errc) TH_MAYBE_UNUSED; -#include -#include +TH_INLINE(const char*) +th_system_strerror(int errc) +{ +#if defined(TH_CONFIG_OS_POSIX) + return strerror(errc); +#elif defined(TH_CONFIG_OS_WIN) + static char buf[256]; + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf, sizeof(buf), NULL); + return buf; +#endif +} + +/* Define the system error codes that we use */ +#if defined(TH_CONFIG_OS_POSIX) +#define TH_ENOENT ENOENT +#define TH_EINTR EINTR +#define TH_EIO EIO +#define TH_EBADF EBADF +#define TH_EBUSY EBUSY +#define TH_EAGAIN EAGAIN +#define TH_EWOULDBLOCK EWOULDBLOCK +#define TH_ENOMEM ENOMEM +#define TH_ENOSYS ENOSYS +#define TH_ETIMEDOUT ETIMEDOUT +#define TH_ECANCELED ECANCELED +#define TH_EPROTO EPROTO +#elif defined(TH_CONFIG_OS_WIN) +#define TH_ENOENT ERROR_FILE_NOT_FOUND +#define TH_EINTR ERROR_INTERRUPT +#define TH_EIO ERROR_IO_DEVICE +#define TH_EBADF ERROR_BAD_FORMAT +#define TH_EBUSY ERROR_BUSY +#define TH_EAGAIN ERROR_RETRY +#define TH_EWOULDBLOCK ERROR_RETRY +#define TH_ENOMEM ERROR_OUTOFMEMORY +#define TH_ENOSYS ERROR_NOT_SUPPORTED +#define TH_ETIMEDOUT ERROR_TIMEOUT +#define TH_ECANCELED ERROR_CANCELLED +#define TH_EPROTO ERROR_INVALID_DATA +#endif +/* End of th_system_error.h */ +/* Start of th_http_error.h */ -/** th_iov - *@brief I/O vector. - */ -typedef struct th_iov { - void* base; - size_t len; -} th_iov; -/** th_iov_consume - *@brief Consume the I/O vector and - * return the number of bytes that were not consumed. +#include + +/** th_http_err + * @brief Converts a error code to a equivalent HTTP error code. */ -TH_INLINE(size_t) -th_iov_consume(th_iov** iov, size_t* iov_len, size_t consume) +TH_INLINE(th_err) +th_http_error(th_err err) { - size_t zeroed = 0; - for (size_t i = 0; i < *iov_len; i++) { - if (consume < (*iov)[i].len) { - (*iov)[i].base = (char*)(*iov)[i].base + consume; - (*iov)[i].len -= consume; - consume = 0; + if (err == TH_ERR_OK) + return TH_ERR_HTTP(TH_CODE_OK); + switch (TH_ERR_CATEGORY(err)) { + case TH_ERR_CATEGORY_SYSTEM: + switch (TH_ERR_CODE(err)) { + case TH_ENOENT: + return TH_ERR_HTTP(TH_CODE_NOT_FOUND); + break; + case TH_ETIMEDOUT: + return TH_ERR_HTTP(TH_CODE_REQUEST_TIMEOUT); + break; + default: + return TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR); break; } - consume -= (*iov)[i].len; - (*iov)[i].len = 0; - zeroed++; + break; + case TH_ERR_CATEGORY_HTTP: + return err; + break; + default: + break; + } + return TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR); +} + +TH_INLINE(const char*) +th_http_strerror(int code) +{ + switch (code) { + case TH_CODE_SWITCHING_PROTOCOLS: + return "Switching Protocols"; + break; + case TH_CODE_OK: + return "OK"; + break; + case TH_CODE_MOVED_PERMANENTLY: + return "Moved Permanently"; + break; + case TH_CODE_BAD_REQUEST: + return "Bad Request"; + break; + case TH_CODE_NOT_FOUND: + return "Not Found"; + break; + case TH_CODE_METHOD_NOT_ALLOWED: + return "Method Not Allowed"; + break; + case TH_CODE_PAYLOAD_TOO_LARGE: + return "Payload Too Large"; + break; + case TH_CODE_INTERNAL_SERVER_ERROR: + return "Internal Server Error"; + break; + case TH_CODE_SERVICE_UNAVAILABLE: + return "Service Unavailable"; + break; + case TH_CODE_NOT_IMPLEMENTED: + return "Method Not Implemented"; + break; + case TH_CODE_REQUEST_TIMEOUT: + return "Request Timeout"; + break; + case TH_CODE_TOO_MANY_REQUESTS: + return "Too Many Requests"; + break; + case TH_CODE_URI_TOO_LONG: + return "URI Too Long"; + break; + case TH_CODE_UNSUPPORTED_MEDIA_TYPE: + return "Unsupported Media Type"; + break; + case TH_CODE_RANGE_NOT_SATISFIABLE: + return "Range Not Satisfiable"; + break; + case TH_CODE_UPGRADE_REQUIRED: + return "Upgrade Required"; + break; + case TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE: + return "Request Header Fields Too Large"; + break; + case TH_CODE_UNAUTHORIZED: + return "Unauthorized"; + break; + case TH_CODE_FORBIDDEN: + return "Forbidden"; + break; + default: + return "Unknown"; + break; } - *iov_len -= zeroed; - (*iov) += zeroed; - return consume; } -TH_INLINE(size_t) -th_iov_bytes(th_iov* iov, size_t iov_len) +typedef enum th_http_code_type { + TH_HTTP_CODE_TYPE_INFORMATIONAL, + TH_HTTP_CODE_TYPE_SUCCESS, + TH_HTTP_CODE_TYPE_REDIRECT, + TH_HTTP_CODE_TYPE_CLIENT_ERROR, + TH_HTTP_CODE_TYPE_SERVER_ERROR, +} th_http_code_type; + +TH_INLINE(th_http_code_type) +th_http_code_get_type(int code) { - size_t bytes = 0; - for (size_t i = 0; i < iov_len; i++) { - bytes += iov[i].len; - } - return bytes; + if (code >= 100 && code < 200) + return TH_HTTP_CODE_TYPE_INFORMATIONAL; + if (code >= 200 && code < 300) + return TH_HTTP_CODE_TYPE_SUCCESS; + if (code >= 300 && code < 400) + return TH_HTTP_CODE_TYPE_REDIRECT; + if (code >= 400 && code < 500) + return TH_HTTP_CODE_TYPE_CLIENT_ERROR; + if (code >= 500 && code < 600) + return TH_HTTP_CODE_TYPE_SERVER_ERROR; + return TH_HTTP_CODE_TYPE_SERVER_ERROR; } -/* End of th_iov.h */ -/* Start of th_log.h */ - - -#include -#include - - -#ifndef TH_LOG_LEVEL -#define TH_LOG_LEVEL TH_LOG_LEVEL_INFO -#endif - -#define TH_LOG_TAG "default" - -TH_PRIVATE(th_log*) -th_default_log_get(void); - -TH_PRIVATE(void) -th_log_printf(int level, const char* fmt, ...) TH_MAYBE_UNUSED TH_PRINTF_FMT(2, 3); - -#if TH_LOG_LEVEL <= TH_LOG_LEVEL_TRACE -#define TH_LOG_TRACE(...) th_log_printf(TH_LOG_LEVEL_TRACE, "TRACE: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_TRACE(...) ((void)0) -#endif - -#if TH_LOG_LEVEL <= TH_LOG_LEVEL_DEBUG -#define TH_LOG_DEBUG(...) th_log_printf(TH_LOG_LEVEL_DEBUG, "DEBUG: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_DEBUG(...) ((void)0) -#endif - -#if (TH_LOG_LEVEL <= TH_LOG_LEVEL_INFO) -#define TH_LOG_INFO(...) th_log_printf(TH_LOG_LEVEL_INFO, "INFO: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_INFO(...) ((void)0) -#endif - -#if TH_LOG_LEVEL <= TH_LOG_LEVEL_WARN -#define TH_LOG_WARN(...) th_log_printf(TH_LOG_LEVEL_WARN, "WARN: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_WARN(...) ((void)0) -#endif - -#if TH_LOG_LEVEL <= TH_LOG_LEVEL_ERROR -#define TH_LOG_ERROR(...) th_log_printf(TH_LOG_LEVEL_ERROR, "ERROR: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_ERROR(...) ((void)0) -#endif - -#if TH_LOG_LEVEL <= TH_LOG_LEVEL_FATAL -#define TH_LOG_FATAL(...) th_log_printf(TH_LOG_LEVEL_FATAL, "FATAL: [" TH_LOG_TAG "] " __VA_ARGS__) -#else -#define TH_LOG_FATAL(...) ((void)0) -#endif - -/* End of th_log.h */ -/* Start of th_utility.h */ +/* End of th_http_error.h */ +/* Start of th_address.h */ -#include -#define TH_MIN(a, b) ((a) < (b) ? (a) : (b)) -#define TH_MAX(a, b) ((a) > (b) ? (a) : (b)) -#define TH_ABS(a) ((a) < 0 ? -(a) : (a)) +#include -#define TH_ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) +/** th_address + * @brief Storage for a peer address filled in by th_acceptor_ops.accept. + */ +typedef struct th_address { + struct sockaddr_storage addr; + socklen_t addrlen; +} th_address; -// Move a pointer from src to dst and set src to NULL -TH_INLINE(void*) -th_move_ptr(void** src) +TH_INLINE(void) +th_address_init(th_address* addr) { - void* dst = *src; - *src = NULL; - return dst; + addr->addrlen = sizeof(addr->addr); } -#define TH_MOVE_PTR(ptr) th_move_ptr((void**)&(ptr)) +/* End of th_address.h */ +/* Start of th_dir.h */ -// Custom assert macros -#ifndef NDEBUG -#define TH_ASSERT(cond) \ - do { \ - if (!(cond)) { \ - TH_LOG_FATAL("Assertion failed: %s at %s:%d", #cond, __FILE__, __LINE__); \ - abort(); \ - } \ - } while (0) -#else -#define TH_ASSERT(cond) ((void)0) -#endif -// Mathematical utility functions +/** th_dir_ops + * @brief The raw open/close syscalls a th_dir performs. Injected at + * construction time so tests can fake a directory fd without touching the + * filesystem. open behaves like the underlying syscall: TH_ERR_OK with + * *fd set on success, TH_ERR_SYSTEM(errno) on failure. + */ +typedef struct th_dir_ops { + th_err (*open)(void* self, const char* path, int* fd); + void (*close)(void* self, int fd); +} th_dir_ops; -TH_INLINE(size_t) -th_next_pow2(size_t n) -{ - TH_ASSERT(n > 0); - n--; - n |= n >> 1; - n |= n >> 2; - n |= n >> 4; - n |= n >> 8; - n |= n >> 16; - n++; - return n; -} +TH_PRIVATE(th_dir_ops*) +th_dir_ops_os(void); -/* End of th_utility.h */ -/* Start of th_list.h */ +typedef struct th_dir { + th_dir_ops* ops; + int fd; +} th_dir; +TH_PRIVATE(void) +th_dir_init(th_dir* dir, th_dir_ops* ops); -/** Generic doubly linked list implementation. - * that works with any struct that has a next and prev pointer. - */ -#define TH_DEFINE_LIST(NAME, T, PREV, NEXT) \ - typedef struct NAME { \ - T* head; \ - T* tail; \ - } NAME; \ - \ - TH_INLINE(void) \ - NAME##_push_back(NAME* list, T* item) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(T*) \ - NAME##_pop_front(NAME* list) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(T*) \ - NAME##_front(NAME* list) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_erase(NAME* list, T* item) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(T*) \ - NAME##_next(T* item) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_push_back(NAME* list, T* item) \ - { \ - TH_ASSERT(item != NULL); \ - if (list->head == NULL) { \ - list->head = item; \ - item->PREV = NULL; \ - } else { \ - list->tail->NEXT = item; \ - item->PREV = list->tail; \ - } \ - list->tail = item; \ - item->NEXT = NULL; \ - } \ - \ - TH_INLINE(T*) \ - NAME##_pop_front(NAME* list) \ - { \ - T* item = list->head; \ - if (item) { \ - list->head = item->NEXT; \ - if (list->head) { \ - list->head->PREV = NULL; \ - } else { \ - list->tail = NULL; \ - } \ - item->NEXT = NULL; \ - } \ - return item; \ - } \ - \ - TH_INLINE(T*) \ - NAME##_front(NAME* list) \ - { \ - return list->head; \ - } \ - \ - TH_INLINE(void) \ - NAME##_erase(NAME* list, T* item) \ - { \ - TH_ASSERT(item != NULL); \ - TH_ASSERT((item->NEXT || item == list->tail) && "Item is not in the list"); \ - TH_ASSERT((item->PREV || item == list->head) && "Item is not in the list"); \ - T* next = item->NEXT; \ - T* prev = item->PREV; \ - if (prev) { \ - prev->NEXT = next; \ - item->PREV = NULL; \ - } else { \ - list->head = next; \ - } \ - if (next) { \ - next->PREV = prev; \ - item->NEXT = NULL; \ - } else { \ - list->tail = prev; \ - } \ - } \ - \ - TH_INLINE(T*) \ - NAME##_next(T* item) \ - { \ - return item->NEXT; \ - } +TH_PRIVATE(th_err) +th_dir_open(th_dir* dir, th_str path); -/* End of th_list.h */ -/* Start of th_allocator.h */ +TH_PRIVATE(void) +th_dir_deinit(th_dir* dir); -#include -#include +/* End of th_dir.h */ +/* Start of th_filepath.h */ -TH_INLINE(void*) -th_allocator_alloc(th_allocator* allocator, size_t size) -{ - return allocator->alloc(allocator, size); -} -TH_INLINE(void*) -th_allocator_realloc(th_allocator* allocator, void* ptr, size_t size) -{ - return allocator->realloc(allocator, ptr, size); -} +/** th_filepath + * @brief A validated, NUL-terminated relative path, ready to pass to a + * syscall. th_filepath_init rejects absolute paths and "." / ".." + * components - openat(dir->fd, ...) doesn't confine resolution to dir, a + * ".." component walks back out of it like normal path resolution - so + * any path built from untrusted input (e.g. a client-supplied filename) + * must go through this first. + */ +typedef struct th_filepath { + char buf[TH_CONFIG_MAX_PATH_LEN + 1]; +} th_filepath; -TH_INLINE(void) -th_allocator_free(th_allocator* allocator, void* ptr) +/** th_filepath_init + * @brief Fills path with str NUL-terminated. + * @return TH_ERR_INVALID_ARG if str is absolute, too long, empty, or has + * a "." / ".." component. + */ +TH_PRIVATE(th_err) +th_filepath_init(th_filepath* path, th_str str); + +TH_INLINE(const char*) +th_filepath_cstr(const th_filepath* path) { - allocator->free(allocator, ptr); + return path->buf; } -TH_PRIVATE(th_allocator*) -th_default_allocator_get(void); +/* End of th_filepath.h */ +/* Start of th_file.h */ -/* th_arena_allocator begin */ -typedef struct th_arena_allocator { - th_allocator base; - th_allocator* allocator; - void* buf; - size_t size; - size_t pos; - size_t prev_pos; - uint16_t alignment; -} th_arena_allocator; -/** th_arena_allocator_init - * @brief The arena allocator is a simple allocator that allocates memory from a fixed-size buffer. - * It only frees memory when the free operation is called on the previously allocated memory. - * If no memory is available in the buffer, it will fall back to the default allocator. - * @param allocator The arena allocator to initialize. - * @param buf The buffer to use for allocations. - * @param size The size of the buffer. +#include + +typedef struct th_open_opt { + bool read; + bool write; + bool create; + bool truncate; +} th_open_opt; + +/** th_file_ops + * @brief The raw syscalls a th_file performs. Injected at construction time + * so tests can fake a file fd without touching the filesystem. Each method + * behaves like the underlying syscall: TH_ERR_OK (with any out-params set) + * on success, TH_ERR_SYSTEM(errno) on failure. */ +typedef struct th_file_ops { + th_err (*openat)(void* self, int dirfd, const char* path, int flags, int* fd); + th_err (*seek)(void* self, int fd, int whence, size_t* pos); + th_err (*read)(void* self, int fd, void* addr, size_t len, size_t offset, size_t* read); + th_err (*write)(void* self, int fd, const void* addr, size_t len, size_t offset, size_t* written); + th_err (*stat)(void* self, int fd, struct stat* out); + void (*close)(void* self, int fd); +} th_file_ops; + +TH_PRIVATE(th_file_ops*) +th_file_ops_os(void); + +typedef struct th_file { + th_file_ops* ops; + int fd; + size_t size; +} th_file; + TH_PRIVATE(void) -th_arena_allocator_init(th_arena_allocator* allocator, void* buf, size_t size, th_allocator* fallback); +th_file_init(th_file* stream, th_file_ops* ops); + +TH_PRIVATE(th_err) +th_file_openat(th_file* stream, th_dir* dir, const th_filepath* path, th_open_opt opt); + +TH_PRIVATE(th_err) +th_file_read(th_file* stream, void* addr, size_t len, size_t offset, size_t* read) TH_MAYBE_UNUSED; + +TH_PRIVATE(th_err) +th_file_write(th_file* stream, const void* addr, size_t len, size_t offset, size_t* written) TH_MAYBE_UNUSED; + +TH_PRIVATE(uint32_t) +th_file_stat_hash(th_file* stream); -/** th_arena_allocator_init_with_alignment - * @brief Just like th_arena_allocator_init, but allows specifying the alignment of the allocations. - */ TH_PRIVATE(void) -th_arena_allocator_init_with_alignment(th_arena_allocator* allocator, void* buf, size_t size, size_t alignment, th_allocator* fallback); +th_file_close(th_file* stream); -/* th_arena_allocator end */ -/** Generic object pool allocator. - * The pool allocator is a allocator that allocates objects from a pool of fixed-size blocks. - * It can be used with any object that has a next and prev pointer. +TH_PRIVATE(void) +th_file_deinit(th_file* stream); + +/* End of th_file.h */ +/* Start of th_iov.h */ + +#include +#include + + +/** th_iov + *@brief I/O vector. */ -#define TH_DEFINE_POOL_ALLOCATOR(NAME, T, PREV, NEXT) \ - TH_DEFINE_LIST(NAME##_list, T, PREV, NEXT) \ - typedef struct NAME { \ - th_allocator base; \ - NAME##_list free_list; \ - NAME##_list used_list; \ - th_allocator* allocator; \ - size_t count; \ - size_t max; \ - } NAME; \ - \ - TH_INLINE(void) \ - NAME##_init(NAME* pool, th_allocator* allocator, size_t initial, size_t max) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_deinit(NAME* pool) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void*) \ - NAME##_alloc(void* self, size_t) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_free(void* self, void* ptr) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_init(NAME* pool, th_allocator* allocator, size_t initial, size_t max) \ - { \ - TH_ASSERT(allocator != NULL && "Invalid allocator"); \ - TH_ASSERT(max > 0 && "Invalid max"); \ - pool->base.alloc = NAME##_alloc; \ - pool->base.realloc = NULL; \ - pool->base.free = NAME##_free; \ - pool->allocator = allocator; \ - pool->count = 0; \ - pool->max = max; \ - pool->used_list = (NAME##_list){0}; \ - pool->free_list = (NAME##_list){0}; \ - for (size_t i = 0; i < initial; i++) { \ - T* item = (T*)th_allocator_alloc(pool->allocator, sizeof(T)); \ - if (item) { \ - NAME##_list_push_back(&pool->free_list, item); \ - ++pool->count; \ - } \ - } \ - } \ - \ - TH_INLINE(void) \ - NAME##_deinit(NAME* pool) \ - { \ - T* item = NULL; \ - while ((item = NAME##_list_pop_front(&pool->free_list))) { \ - th_allocator_free(pool->allocator, item); \ - } \ - item = NAME##_list_pop_front(&pool->used_list); \ - TH_ASSERT(item == NULL); \ - } \ - \ - TH_INLINE(void*) \ - NAME##_alloc(void* self, size_t size) \ - { \ - TH_ASSERT(size == sizeof(T) && "Invalid size"); \ - (void)size; \ - NAME* pool = (NAME*)self; \ - T* item = NAME##_list_pop_front(&pool->free_list); \ - if (item == NULL) { \ - if (pool->count < pool->max) { \ - item = (T*)th_allocator_alloc(pool->allocator, sizeof(T)); \ - if (item) { \ - pool->count++; \ - } \ - } \ - } \ - if (item) { \ - NAME##_list_push_back(&pool->used_list, item); \ - } \ - return item; \ - } \ - \ - TH_INLINE(void) \ - NAME##_free(void* self, void* ptr) \ - { \ - NAME* pool = (NAME*)self; \ - T* item = (T*)ptr; \ - if (item) { \ - NAME##_list_erase(&pool->used_list, item); \ - NAME##_list_push_back(&pool->free_list, item); \ - } \ + +typedef struct th_iov { + void* base; + size_t len; +} th_iov; + +/** th_iov_consume + *@brief Consume the I/O vector and + * return the number of bytes that were not consumed. + */ +TH_INLINE(size_t) +th_iov_consume(th_iov** iov, size_t* iov_len, size_t consume) +{ + size_t zeroed = 0; + for (size_t i = 0; i < *iov_len; i++) { + if (consume < (*iov)[i].len) { + (*iov)[i].base = (char*)(*iov)[i].base + consume; + (*iov)[i].len -= consume; + consume = 0; + break; + } + consume -= (*iov)[i].len; + (*iov)[i].len = 0; + zeroed++; } + *iov_len -= zeroed; + (*iov) += zeroed; + return consume; +} -/* End of th_allocator.h */ +TH_INLINE(size_t) +th_iov_bytes(th_iov* iov, size_t iov_len) +{ + size_t bytes = 0; + for (size_t i = 0; i < iov_len; i++) { + bytes += iov[i].len; + } + return bytes; +} + +/* End of th_iov.h */ /* Start of th_queue.h */ @@ -1174,6 +1444,8 @@ th_arena_allocator_init_with_alignment(th_arena_allocator* allocator, void* buf, T* item = queue->head; \ if (item) { \ queue->head = item->next; \ + if (queue->head == NULL) \ + queue->tail = NULL; \ item->next = NULL; \ } \ return item; \ @@ -1869,7 +2141,8 @@ th_conn_cancel(th_conn* conn) TH_INLINE(void) th_conn_destroy(th_conn* conn) { - conn->methods->destroy(conn); + if (conn) + conn->methods->destroy(conn); } /* th_conn interface end */ @@ -2326,277 +2599,44 @@ th_hash_cstr(const char* str) return e; \ } \ } \ - return NULL; \ - } \ - \ - TH_INLINE(NAME##_entry*) \ - NAME##_prev(NAME* map, NAME##_entry* entry) \ - { \ - TH_ASSERT(entry >= map->entries && entry < map->entries + map->capacity && "Entry is out of bounds"); \ - size_t i = (size_t)(entry - map->entries); \ - for (size_t j = i - 1; j >= map->begin; j--) { \ - NAME##_entry* e = &map->entries[j]; \ - if (!K_EQ(e->key, K_NULL)) { \ - return e; \ - } \ - } \ - return NAME##_begin(map); \ - } - -/* th_cstr_map begin */ - -TH_INLINE(size_t) -th_cstr_hash(const char* str) -{ - return th_hash_cstr(str); -} - -TH_INLINE(bool) -th_cstr_eq(const char* a, const char* b) -{ - if (!a || !b) - return a == b; - return *a == *b && (strcmp(a, b) == 0); -} - -TH_DEFINE_HASHMAP(th_cstr_map, const char*, const char*, th_cstr_hash, th_cstr_eq, NULL) - -/* th_cstr_map end */ - -/* End of th_hashmap.h */ -/* Start of th_vec.h */ - - - -#include - -#define TH_DEFINE_VEC(NAME, TYPE, DEINIT) \ - typedef struct NAME { \ - TYPE* data; \ - size_t size; \ - size_t capacity; \ - th_allocator* allocator; \ - } NAME; \ - \ - TH_INLINE(void) \ - NAME##_init(NAME* vec, th_allocator* allocator) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_clear(NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_deinit(NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(size_t) \ - NAME##_size(const NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(size_t) \ - NAME##_capacity(const NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(th_err) \ - NAME##_resize(NAME* vec, size_t size) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(th_err) \ - NAME##_push_back(NAME* vec, TYPE value) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(TYPE*) \ - NAME##_at(NAME* vec, size_t index) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(const TYPE*) \ - NAME##_cat(const NAME* vec, size_t index) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(TYPE*) \ - NAME##_begin(NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(TYPE*) \ - NAME##_end(NAME* vec) TH_MAYBE_UNUSED; \ - \ - TH_INLINE(void) \ - NAME##_init(NAME* vec, th_allocator* allocator) \ - { \ - vec->allocator = allocator ? allocator : th_default_allocator_get(); \ - vec->capacity = 0; \ - vec->size = 0; \ - vec->data = NULL; \ - } \ - \ - TH_INLINE(void) \ - NAME##_deinit(NAME* vec) \ - { \ - if (vec->data) { \ - for (size_t i = 0; i < vec->size; i++) { \ - DEINIT(&vec->data[i]); \ - } \ - th_allocator_free(vec->allocator, vec->data); \ - } \ - } \ - \ - TH_INLINE(void) \ - NAME##_clear(NAME* vec) \ - { \ - if (vec->data) { \ - for (size_t i = 0; i < vec->size; i++) { \ - DEINIT(&vec->data[i]); \ - } \ - } \ - vec->size = 0; \ - } \ - \ - TH_INLINE(size_t) \ - NAME##_size(const NAME* vec) \ - { \ - return vec->size; \ - } \ - \ - TH_INLINE(size_t) \ - NAME##_capacity(const NAME* vec) \ - { \ - return vec->capacity; \ - } \ - \ - TH_INLINE(th_err) \ - NAME##_resize(NAME* vec, size_t size) \ - { \ - if (size < vec->size) { \ - vec->size = size; \ - return TH_ERR_OK; \ - } \ - if (size > vec->capacity) { \ - size_t new_capacity = th_next_pow2(size); \ - TYPE* new_data = th_allocator_realloc(vec->allocator, vec->data, new_capacity * sizeof(TYPE)); \ - if (new_data == NULL) { \ - return TH_ERR_BAD_ALLOC; \ - } \ - vec->data = new_data; \ - vec->capacity = new_capacity; \ - } \ - vec->size = size; \ - return TH_ERR_OK; \ - } \ - \ - TH_INLINE(th_err) \ - NAME##_push_back(NAME* vec, TYPE value) \ - { \ - if (vec->size >= vec->capacity) { \ - size_t new_capacity = vec->capacity == 0 ? 1 : vec->capacity * 2; \ - TYPE* new_data = th_allocator_realloc(vec->allocator, vec->data, new_capacity * sizeof(TYPE)); \ - if (new_data == NULL) { \ - return TH_ERR_BAD_ALLOC; \ - } \ - vec->data = new_data; \ - vec->capacity = new_capacity; \ - } \ - vec->data[vec->size++] = value; \ - return TH_ERR_OK; \ - } \ - \ - TH_INLINE(TYPE*) \ - NAME##_at(NAME* vec, size_t index) \ - { \ - TH_ASSERT(index <= vec->size); \ - return vec->data + index; \ - } \ - \ - TH_INLINE(const TYPE*) \ - NAME##_cat(const NAME* vec, size_t index) \ - { \ - TH_ASSERT(index <= vec->size); \ - return vec->data + index; \ - } \ - \ - TH_INLINE(TYPE*) \ - NAME##_begin(NAME* vec) \ - { \ - return vec->data; \ - } \ - \ - TH_INLINE(TYPE*) \ - NAME##_end(NAME* vec) \ - { \ - return vec->data + vec->size; \ - } - -// Default vectors -TH_DEFINE_VEC(th_buf_vec, char, (void)) - -/* End of th_vec.h */ -/* Start of th_string.h */ - - -typedef struct th_detail_large_string { - size_t capacity; - size_t len; - char* ptr; - th_allocator* allocator; -} th_detail_large_string; - -#define TH_STRING_SMALL_BUF_LEN (sizeof(char*) + sizeof(size_t) + sizeof(size_t) - 1) -#define TH_STRING_SMALL_MAX_LEN (TH_STRING_SMALL_BUF_LEN - 1) -typedef struct th_detail_small_string { - unsigned char small : 1; - unsigned char len : 7; - char buf[TH_STRING_SMALL_BUF_LEN]; - th_allocator* allocator; -} th_detail_small_string; - -typedef struct th_string { - union { - th_detail_small_string small; - th_detail_large_string large; - } impl; -} th_string; - -TH_PRIVATE(void) -th_string_init(th_string* self, th_allocator* allocator); - -TH_PRIVATE(th_err) -th_string_init_with(th_string* self, th_str str, th_allocator* allocator); - -TH_PRIVATE(th_err) -th_string_set(th_string* self, th_str str); - -TH_PRIVATE(th_err) -th_string_append(th_string* self, th_str str); - -TH_PRIVATE(th_err) -th_string_append_cstr(th_string* self, const char* str); - -TH_PRIVATE(th_err) -th_string_push_back(th_string* self, char c); - -TH_PRIVATE(th_err) -th_string_resize(th_string* self, size_t new_len, char fill); - -TH_PRIVATE(th_str) -th_string_view(const th_string* self); - -TH_PRIVATE(char*) -th_string_at(th_string* self, size_t index); - -TH_PRIVATE(const char*) -th_string_data(const th_string* self); - -TH_PRIVATE(size_t) -th_string_len(const th_string* self); - -TH_PRIVATE(void) -th_string_deinit(th_string* self); + return NULL; \ + } \ + \ + TH_INLINE(NAME##_entry*) \ + NAME##_prev(NAME* map, NAME##_entry* entry) \ + { \ + TH_ASSERT(entry >= map->entries && entry < map->entries + map->capacity && "Entry is out of bounds"); \ + size_t i = (size_t)(entry - map->entries); \ + for (size_t j = i - 1; j >= map->begin; j--) { \ + NAME##_entry* e = &map->entries[j]; \ + if (!K_EQ(e->key, K_NULL)) { \ + return e; \ + } \ + } \ + return NAME##_begin(map); \ + } -TH_PRIVATE(void) -th_string_clear(th_string* self); +/* th_cstr_map begin */ -TH_PRIVATE(void) -th_string_to_lower(th_string* self); +TH_INLINE(size_t) +th_cstr_hash(const char* str) +{ + return th_hash_cstr(str); +} -TH_PRIVATE(bool) -th_string_eq(const th_string* self, th_str other); +TH_INLINE(bool) +th_cstr_eq(const char* a, const char* b) +{ + if (!a || !b) + return a == b; + return *a == *b && (strcmp(a, b) == 0); +} -// TH_PRIVATE(uint32_t) -// th_string_hash(const th_string* self); +TH_DEFINE_HASHMAP(th_cstr_map, const char*, const char*, th_cstr_hash, th_cstr_eq, NULL) -TH_DEFINE_VEC(th_string_vec, th_string, th_string_deinit) +/* th_cstr_map end */ -/* End of th_string.h */ +/* End of th_hashmap.h */ /* Start of th_dir_mgr.h */ @@ -3090,6 +3130,11 @@ typedef struct th_capture { th_str value; } th_capture; +/** th_router_capture_cb + * @brief Called per capture found while resolving a path. NULL = dry run. + */ +typedef void (*th_router_capture_cb)(void* userp, th_str key, th_str value); + typedef enum th_capture_type { TH_CAPTURE_TYPE_NONE = 0, TH_CAPTURE_TYPE_INT, @@ -3097,11 +3142,17 @@ typedef enum th_capture_type { TH_CAPTURE_TYPE_PATH, } th_capture_type; +typedef struct th_ws_route_handler { + th_ws_handler handler; + void* user_data; +} th_ws_route_handler; + typedef struct th_route_segment th_route_segment; struct th_route_segment { th_capture_type type; th_string name; th_route_handler handler[TH_METHOD_MAX]; + th_ws_route_handler ws_handler; th_route_segment* next; th_route_segment* children; th_allocator* allocator; @@ -3131,6 +3182,16 @@ th_router_would_handle(th_router* router, th_method method, th_request* request) TH_PRIVATE(th_err) th_router_add_route(th_router* router, th_method method, th_str route, th_handler handler, void* user_data); +TH_PRIVATE(th_err) +th_router_add_ws_route(th_router* router, th_str route, th_ws_handler handler, void* user_data); + +/** th_router_find_ws_route + * @brief Resolves path to a registered WS route (ignoring method). On a + * match, sets handler and user_data and returns true. + */ +TH_PRIVATE(bool) +th_router_find_ws_route(th_router* router, th_str path, th_ws_handler* handler, void** user_data); + /* End of th_router.h */ /* Start of th_http.h */ @@ -3154,6 +3215,10 @@ struct th_http { // true if the connection should be closed bool close; + + // set by th_http_try_upgrade_ws, read once the 101 response is written + th_ws_handler ws_handler; + void* ws_user_data; }; typedef struct th_http_upgrader { @@ -3466,6 +3531,19 @@ TH_PRIVATE(void) th_sendvec_op_init(th_sendvec_op* op, th_socket* socket, th_iov* iov, size_t iovcnt, th_send_cb callback, void* user_data); /* End of th_sendvec.h */ +/* Start of th_sha1.h */ + + + +#define TH_SHA1_DIGEST_LEN 20 + +/** th_sha1 + * @brief Computes the SHA-1 digest of data into digest[TH_SHA1_DIGEST_LEN]. + */ +TH_PRIVATE(void) +th_sha1(th_buffer data, unsigned char digest[TH_SHA1_DIGEST_LEN]); + +/* End of th_sha1.h */ /* Start of th_ssl_conn.h */ @@ -3644,6342 +3722,7500 @@ typedef struct th_ssl_recv_op { bool exact; } th_ssl_recv_op; -TH_PRIVATE(void) -th_ssl_recv_op_init(th_ssl_recv_op* op, th_socket* socket, th_ssl_session* session, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data); +TH_PRIVATE(void) +th_ssl_recv_op_init(th_ssl_recv_op* op, th_socket* socket, th_ssl_session* session, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data); + +#endif +/* End of th_ssl_recv.h */ +/* Start of th_ssl_send.h */ + + +#if TH_WITH_SSL + + +#define TH_SSL_SEND_CHUNK_LEN (16 * 1024) + +/** th_ssl_send_op + * @brief Writes iov (mutated in place as buffers are consumed) as + * plaintext through a th_ssl_session (shuttling ciphertext over socket + * as needed), followed by len bytes of file starting at offset if file + * is non-NULL, retrying in TH_SSL_SEND_CHUNK_LEN-sized steps until every + * byte has been written or an error occurs. After init, the first + * th_ssl_io_op write is already in flight. + */ +typedef struct th_ssl_send_op { + th_ssl_io_op io; + th_socket* socket; + th_ssl_session* session; + th_send_cb callback; + void* user_data; + th_iov* iov; + size_t iovcnt; + th_file* file; + size_t offset; + size_t len; + size_t file_pos; + size_t pos; + char buffer[TH_SSL_SEND_CHUNK_LEN]; +} th_ssl_send_op; + +TH_PRIVATE(void) +th_ssl_send_op_init(th_ssl_send_op* op, th_socket* socket, th_ssl_session* session, + th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, + th_send_cb callback, void* user_data); + +#endif +/* End of th_ssl_send.h */ +/* Start of th_ssl_smem_bio.h */ + + +#if TH_WITH_SSL + +#include + + +TH_PRIVATE(BIO_METHOD*) +th_smem_bio(th_ssl_context* ssl_context); + +TH_PRIVATE(void) +th_smem_bio_setup_buf(BIO* bio, th_allocator* allocator, size_t max_len); + +TH_PRIVATE(size_t) +th_smem_ensure_buf_size(BIO* bio, size_t size); + +TH_PRIVATE(void) +th_smem_bio_set_eof(BIO* bio); + +TH_PRIVATE(void) +th_smem_bio_get_rdata(BIO* bio, th_iov* buf); + +TH_PRIVATE(void) +th_smem_bio_get_wbuf(BIO* bio, th_iov* buf); + +TH_PRIVATE(void) +th_smem_bio_inc_read_pos(BIO* bio, size_t len); + +TH_PRIVATE(void) +th_smem_bio_inc_write_pos(BIO* bio, size_t len); + +#endif +/* End of th_ssl_smem_bio.h */ +/* Start of th_tcp_conn.h */ + + + +/** th_tcp_conn_create + * @brief Allocates and initializes a plain (non-SSL) th_conn, taking + * ownership of socket by value (the caller's th_socket is moved in, not + * referenced — construct it with th_socket_init and don't use it again + * after this call). The returned conn has no fd yet; set one via + * th_socket_set_fd(th_conn_get_socket(conn), fd) before use. + */ +TH_PRIVATE(th_err) +th_tcp_conn_create(th_conn** out, th_socket* socket, + th_conn_upgrader* upgrader, th_conn_observer* observer, + th_allocator* allocator); + +/* End of th_tcp_conn.h */ +/* Start of th_url_decode.h */ + + + +#include + +typedef enum th_url_decode_type { + TH_URL_DECODE_TYPE_PATH = 0, + TH_URL_DECODE_TYPE_QUERY +} th_url_decode_type; + +TH_PRIVATE(th_err) +th_url_decode_string(th_str input, th_string* output, th_url_decode_type type); + +/* End of th_url_decode.h */ +/* Start of th_ring.h */ + + + +#include + +typedef struct th_ring_chunk { + struct th_ring_chunk* next; + unsigned char* data; + size_t capacity; + // head/tail only ever increase - never wrapped themselves, so + // len = tail - head and full = (tail - head == capacity) always hold. + // Actual buffer offsets are head % capacity / tail % capacity. + size_t head; + size_t tail; +} th_ring_chunk; + +/* th_ring_chunk_queue declarations begin */ + +#ifndef TH_RING_CHUNK_QUEUE +#define TH_RING_CHUNK_QUEUE +TH_DEFINE_QUEUE(th_ring_chunk_queue, th_ring_chunk) +#endif + +/* th_ring_chunk_queue declarations end */ + +/** th_ring + * @brief FIFO byte queue backed by a linked list of ring-buffer chunks. + * Writes always land in the tail chunk; once it's full a new, twice as + * large chunk is appended. Reads (peek/consume) only ever touch the head + * chunk, which is freed once fully consumed. + */ +typedef struct th_ring { + th_ring_chunk_queue chunks; + size_t len; // total bytes currently queued, across all chunks + size_t initial_capacity; // size of the first chunk, allocated lazily on first write + size_t max_len; // th_ring_write rejects anything that would exceed this + th_allocator* allocator; +} th_ring; + +TH_PRIVATE(void) +th_ring_init(th_ring* rb, th_allocator* allocator, size_t initial_capacity, size_t max_len); + +TH_PRIVATE(void) +th_ring_deinit(th_ring* rb); + +/** th_ring_write + * @brief Queues parts as one message (never split across chunks), + * growing (doubling the tail chunk) if it doesn't have room. + * + * - TH_ERR_INVALID_ARG: total size alone exceeds max_len, retrying never helps + * - TH_ERR_SYSTEM(TH_EAGAIN): fits under max_len, but a chunk allocation failed + */ +TH_PRIVATE(th_err) +th_ring_write(th_ring* rb, const th_iov* parts, size_t partcnt); + +/** th_ring_peek + * @brief Fills iov[0..1] with the head chunk's queued bytes (iov[1] only + * used if that chunk's queued run wraps past the end of its buffer). + * @return Number of iov entries filled (0, 1, or 2). + */ +TH_PRIVATE(size_t) +th_ring_peek(th_ring* rb, th_iov iov[2]); + +/** th_ring_consume + * @brief Marks the oldest len queued bytes as sent, freeing that space. + * Frees the head chunk once it's fully drained. + */ +TH_PRIVATE(void) +th_ring_consume(th_ring* rb, size_t len); + +/* End of th_ring.h */ +/* Start of th_ws_frame.h */ + + + +#include + +typedef enum th_ws_frame_type { + TH_WS_FRAME_TEXT, + TH_WS_FRAME_BINARY, + TH_WS_FRAME_PING, + TH_WS_FRAME_PONG, + TH_WS_FRAME_CLOSE, +} th_ws_frame_type; + +// 2 base bytes + 8 byte extended length (server frames are never masked). +#define TH_WS_FRAME_HEADER_MAX_LEN 10 + +/** th_ws_frame_header_write + * @brief Encodes a FIN=1, unmasked frame header for len bytes of payload. + * @return Bytes written to header (2, 4, or 10). + */ +TH_PRIVATE(size_t) +th_ws_frame_header_write(unsigned char* header, th_ws_frame_type type, size_t len); + +/* End of th_ws_frame.h */ +/* Start of th_ws_frame_parser.h */ + + + +#include + +typedef enum th_ws_frame_parser_state { + TH_WS_FRAME_PARSER_STATE_HEADER, + TH_WS_FRAME_PARSER_STATE_PAYLOAD, +} th_ws_frame_parser_state; + +// Largest a header can be: 2 fixed bytes + 8 byte extended length + 4 byte mask key. +#define TH_WS_FRAME_PARSER_HEADER_MAX_LEN 14 + +typedef struct th_ws_frame_parser { + th_ws_frame_parser_state state; + + // Header bytes seen so far, for a header split across recv() calls. + unsigned char header_buf[TH_WS_FRAME_PARSER_HEADER_MAX_LEN]; + size_t header_len; + + // Current frame, once its header is fully parsed. + bool fin; + unsigned char opcode; + unsigned char mask_key[4]; + uint64_t payload_len; + uint64_t payload_read; // bytes of this frame's payload consumed so far + + unsigned char message_opcode; // opcode of a fragmented message still in progress, 0 if none +} th_ws_frame_parser; + +// data must be mutable - payloads are unmasked in place. +// *type is only set when this returns TH_ERR_OK. +// +// - TH_ERR_OK: one full message is in payload (empty for TH_WS_FRAME_CLOSE) +// - TH_ERR_SYSTEM(TH_EAGAIN): need more data; *parsed still reflects bytes +// consumed so far - keep the remainder and retry once more bytes arrive +// - TH_ERR_SYSTEM(TH_EPROTO): protocol violation +TH_PRIVATE(th_err) +th_ws_frame_parser_parse(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + th_ws_frame_type* type); + +/* End of th_ws_frame_parser.h */ +/* Start of th_ws.h */ + + + +#define TH_WS_SCRATCH_RECV_LEN 8192 + +struct th_ws { + th_conn* conn; + th_ws_handler handler; + void* user_data; + th_allocator* allocator; + th_ws_frame_parser parser; + th_buf_vec payload; // accumulates a message's payload across fragments/calls + char scratch[TH_WS_SCRATCH_RECV_LEN]; + + th_ring send_ring; + th_iov send_iov[2]; + bool sending; + bool closing; // a CLOSE frame is queued/in flight - destroy once send_ring drains +}; + +TH_PRIVATE(void) +th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator); + +TH_PRIVATE(void) +th_ws_deinit(th_ws* ws); + +TH_PRIVATE(th_err) +th_ws_create(th_ws** out, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator); + +/** th_ws_start + * @brief Fires TH_WS_EVENT_OPEN. If the handler returns an error, the + * connection is torn down immediately without ever reading a frame. + */ +TH_PRIVATE(void) +th_ws_start(th_ws* ws); + +/* End of th_ws.h */ +/* Start of th_ws_handshake.h */ + + + +/** th_ws_is_handshake + * @brief Checks whether request is a valid RFC 6455 upgrade request. + */ +TH_PRIVATE(bool) +th_ws_is_handshake(th_request* request); + +/** th_ws_handshake_accept_key + * @brief Computes Sec-WebSocket-Accept for a Sec-WebSocket-Key value. + * @return TH_ERR_INVALID_ARG if key is too long, TH_ERR_OK otherwise. + */ +TH_PRIVATE(th_err) +th_ws_handshake_accept_key(th_str key, th_string* out); + +/* End of th_ws_handshake.h */ +/* Start of th_align.h */ + +#include + +#define TH_ALIGNOF(type) ((size_t)&(((struct { char c; type member; }*)0)->member)) +#define TH_ALIGNAS(align, ptr) ((void*)(((uintptr_t)(ptr) + ((uintptr_t)(align) - 1)) & ~((uintptr_t)(align) - 1))) +#define TH_ALIGNUP(n, align) (((n) + (size_t)(align) - 1) & ~((size_t)(align) - 1)) +#define TH_ALIGNDOWN(n, align) ((n) & ~((align) - 1)) + +typedef long double th_max_align; + +/* End of th_align.h */ +/* Start of src/th_server.c */ + -#endif -/* End of th_ssl_recv.h */ -/* Start of th_ssl_send.h */ +struct th_server { + th_reactor* reactor; + th_loop loop; + th_router router; + th_dir_mgr dir_mgr; + th_fcache fcache; + th_listener* listeners; + th_allocator* allocator; +}; +TH_LOCAL(th_err) +th_server_init(th_server* server, th_allocator* allocator) +{ + th_router_init(&server->router, allocator); + th_err err = TH_ERR_OK; + th_loop_init(&server->loop, NULL); + if ((err = th_poll_create(&server->reactor, &server->loop, allocator, th_clock_os(), th_pollops_os())) != TH_ERR_OK) + goto cleanup_router; + server->loop.reactor = server->reactor; + th_dir_mgr_init(&server->dir_mgr, allocator); + th_fcache_init(&server->fcache, th_file_ops_os(), allocator); + server->listeners = NULL; + server->allocator = allocator; +cleanup_router: + th_router_deinit(&server->router); + return err; +} -#if TH_WITH_SSL +TH_LOCAL(void) +th_server_stop(th_server* server) +{ + th_listener* listener = server->listeners; + while (listener) { + th_listener_stop(listener); + listener = listener->next; + } + th_loop_run(&server->loop); +} +TH_LOCAL(void) +th_server_deinit(th_server* server) +{ + th_listener* listener = server->listeners; + while (listener) { + th_listener* next = listener->next; + th_listener_destroy(listener); + listener = next; + } + th_loop_deinit(&server->loop); + th_reactor_destroy(server->reactor); + th_router_deinit(&server->router); + th_fcache_deinit(&server->fcache); + th_dir_mgr_deinit(&server->dir_mgr); +} -#define TH_SSL_SEND_CHUNK_LEN (16 * 1024) +TH_LOCAL(th_err) +th_server_bind(th_server* server, const char* host, const char* port, th_bind_opt* opt) +{ + th_listener* listener = NULL; + th_err err = TH_ERR_OK; + if ((err = th_listener_create(&listener, &server->loop, + host, port, + &server->router, &server->dir_mgr, &server->fcache, + opt, server->allocator)) + != TH_ERR_OK) { + return err; + } + if ((err = th_listener_start(listener)) != TH_ERR_OK) { + th_listener_destroy(listener); + return err; + } + listener->next = server->listeners; + server->listeners = listener; + return TH_ERR_OK; +} -/** th_ssl_send_op - * @brief Writes iov (mutated in place as buffers are consumed) as - * plaintext through a th_ssl_session (shuttling ciphertext over socket - * as needed), followed by len bytes of file starting at offset if file - * is non-NULL, retrying in TH_SSL_SEND_CHUNK_LEN-sized steps until every - * byte has been written or an error occurs. After init, the first - * th_ssl_io_op write is already in flight. - */ -typedef struct th_ssl_send_op { - th_ssl_io_op io; - th_socket* socket; - th_ssl_session* session; - th_send_cb callback; - void* user_data; - th_iov* iov; - size_t iovcnt; - th_file* file; - size_t offset; - size_t len; - size_t file_pos; - size_t pos; - char buffer[TH_SSL_SEND_CHUNK_LEN]; -} th_ssl_send_op; +TH_LOCAL(th_err) +th_server_route(th_server* server, th_method method, const char* path, th_handler handler, void* user_data) +{ + return th_router_add_route(&server->router, method, th_str_from_cstr(path), handler, user_data); +} -TH_PRIVATE(void) -th_ssl_send_op_init(th_ssl_send_op* op, th_socket* socket, th_ssl_session* session, - th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, - th_send_cb callback, void* user_data); +TH_LOCAL(th_err) +th_server_route_ws(th_server* server, const char* path, th_ws_handler handler, void* user_data) +{ + return th_router_add_ws_route(&server->router, th_str_from_cstr(path), handler, user_data); +} -#endif -/* End of th_ssl_send.h */ -/* Start of th_ssl_smem_bio.h */ +TH_LOCAL(th_err) +th_server_add_dir(th_server* server, const char* name, const char* path) +{ + th_dir dir; + th_dir_init(&dir, th_dir_ops_os()); + th_err err = TH_ERR_OK; + if ((err = th_dir_open(&dir, th_str_from_cstr(path))) != TH_ERR_OK) { + th_dir_deinit(&dir); + return err; + } + return th_dir_mgr_add(&server->dir_mgr, th_str_from_cstr(name), dir); +} +TH_LOCAL(th_err) +th_server_save_to_disk(th_server* server, th_buffer data, const char* dir_label, const char* filepath) +{ + th_dir* dir = th_dir_mgr_get(&server->dir_mgr, th_str_from_cstr(dir_label)); + if (!dir) + return TH_ERR_HTTP(TH_CODE_NOT_FOUND); + th_err err = TH_ERR_OK; + th_filepath path; + if ((err = th_filepath_init(&path, th_str_from_cstr(filepath))) != TH_ERR_OK) + return err; + th_open_opt opt = {.create = true, .write = true, .truncate = true}; + th_file file; + th_file_init(&file, server->fcache.file_ops); + if ((err = th_file_openat(&file, dir, &path, opt)) != TH_ERR_OK) + return err; + size_t total_written = 0; + while (total_written < data.len) { + size_t written = 0; + if ((err = th_file_write(&file, data.ptr + total_written, data.len - total_written, total_written, &written)) + != TH_ERR_OK) { + th_file_close(&file); + return err; + } + total_written += written; + } + th_file_close(&file); + return TH_ERR_OK; +} -#if TH_WITH_SSL +TH_LOCAL(th_err) +th_server_poll(th_server* server, int timeout_ms) +{ + return th_loop_poll(&server->loop, timeout_ms); +} -#include +/* public server API */ +TH_PUBLIC(th_err) +th_server_create(th_server** out, th_allocator* allocator) +{ + allocator = allocator ? allocator : th_default_allocator_get(); + th_server* server = th_allocator_alloc(allocator, sizeof(th_server)); + if (!server) + return TH_ERR_BAD_ALLOC; + th_err err = TH_ERR_OK; + if ((err = th_server_init(server, allocator)) != TH_ERR_OK) { + th_allocator_free(server->allocator, server); + return err; + } + *out = server; + return TH_ERR_OK; +} -TH_PRIVATE(BIO_METHOD*) -th_smem_bio(th_ssl_context* ssl_context); +TH_PUBLIC(void) +th_server_destroy(th_server* server) +{ + th_server_stop(server); + th_server_deinit(server); + th_allocator_free(server->allocator, server); +} -TH_PRIVATE(void) -th_smem_bio_setup_buf(BIO* bio, th_allocator* allocator, size_t max_len); +TH_PUBLIC(th_err) +th_bind(th_server* server, const char* addr, const char* port, th_bind_opt* opt) +{ + return th_server_bind(server, addr, port, opt); +} -TH_PRIVATE(size_t) -th_smem_ensure_buf_size(BIO* bio, size_t size); +TH_PUBLIC(th_err) +th_route(th_server* server, th_method method, const char* route, th_handler handler, void* userp) +{ + return th_server_route(server, method, route, handler, userp); +} -TH_PRIVATE(void) -th_smem_bio_set_eof(BIO* bio); +TH_PUBLIC(th_err) +th_route_ws(th_server* server, const char* path, th_ws_handler handler, void* userp) +{ + return th_server_route_ws(server, path, handler, userp); +} -TH_PRIVATE(void) -th_smem_bio_get_rdata(BIO* bio, th_iov* buf); +TH_PUBLIC(th_err) +th_add_dir(th_server* server, const char* name, const char* path) +{ + return th_server_add_dir(server, name, path); +} -TH_PRIVATE(void) -th_smem_bio_get_wbuf(BIO* bio, th_iov* buf); +TH_PUBLIC(th_err) +th_save_to_disk(th_server* server, th_buffer data, const char* dir_label, const char* filepath) +{ + return th_server_save_to_disk(server, data, dir_label, filepath); +} -TH_PRIVATE(void) -th_smem_bio_inc_read_pos(BIO* bio, size_t len); +TH_PUBLIC(th_err) +th_poll(th_server* server, int timeout_ms) +{ + return th_server_poll(server, timeout_ms); +} +/* End of src/th_server.c */ +/* Start of src/th_listener.c */ -TH_PRIVATE(void) -th_smem_bio_inc_write_pos(BIO* bio, size_t len); +#include +#include +#include + + +#undef TH_LOG_TAG +#define TH_LOG_TAG "listener" +TH_LOCAL(th_err) +th_listener_enable_ssl(th_listener* listener, const char* key_file, const char* cert_file) +{ +#if TH_WITH_SSL + th_err err = TH_ERR_OK; + if ((err = th_ssl_context_init(&listener->ssl_context, th_ssl_ops_os(), key_file, cert_file)) != TH_ERR_OK) + return err; + listener->ssl_enabled = true; + return TH_ERR_OK; +#else + (void)listener; + (void)key_file; + (void)cert_file; + TH_LOG_ERROR("SSL is not enabled in this build."); + return TH_ERR_NOSUPPORT; #endif -/* End of th_ssl_smem_bio.h */ -/* Start of th_tcp_conn.h */ - +} +TH_LOCAL(th_err) +th_listener_init(th_listener* listener, th_loop* loop, + const char* host, const char* port, + th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, + th_bind_opt* opt, th_allocator* allocator) +{ + listener->loop = loop; + listener->running = 0; + listener->ssl_enabled = false; + listener->allocator = allocator ? allocator : th_default_allocator_get(); + th_err err = TH_ERR_OK; + th_acceptor_init(&listener->acceptor, loop, th_acceptor_ops_os()); + if ((err = th_acceptor_open(&listener->acceptor, host, port)) != TH_ERR_OK) + return err; + if (opt && opt->key_file && opt->cert_file) { + if ((err = th_listener_enable_ssl(listener, opt->key_file, opt->cert_file)) != TH_ERR_OK) + goto cleanup_acceptor; + } + th_conn_tracker_init(&listener->conn_tracker); + th_http_upgrader_init(&listener->upgrader, &listener->conn_tracker, router, dir_mgr, fcache, allocator); + TH_LOG_INFO("Created listener on %s:%s", host, port); + return TH_ERR_OK; +cleanup_acceptor: + th_acceptor_deinit(&listener->acceptor); + return err; +} -/** th_tcp_conn_create - * @brief Allocates and initializes a plain (non-SSL) th_conn, taking - * ownership of socket by value (the caller's th_socket is moved in, not - * referenced — construct it with th_socket_init and don't use it again - * after this call). The returned conn has no fd yet; set one via - * th_socket_set_fd(th_conn_get_socket(conn), fd) before use. - */ TH_PRIVATE(th_err) -th_tcp_conn_create(th_conn** out, th_socket* socket, - th_conn_upgrader* upgrader, th_conn_observer* observer, - th_allocator* allocator); - -/* End of th_tcp_conn.h */ -/* Start of th_url_decode.h */ +th_listener_create(th_listener** out, th_loop* loop, + const char* host, const char* port, + th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, + th_bind_opt* opt, th_allocator* allocator) +{ + th_listener* listener = th_allocator_alloc(allocator, sizeof(th_listener)); + if (!listener) + return TH_ERR_BAD_ALLOC; + th_err err = TH_ERR_OK; + if ((err = th_listener_init(listener, loop, host, port, router, dir_mgr, fcache, opt, allocator)) != TH_ERR_OK) + goto cleanup; + *out = listener; + return TH_ERR_OK; +cleanup: + th_allocator_free(allocator, listener); + return err; +} +TH_LOCAL(void) +th_listener_accept_complete(void* user_data, th_err err); +TH_LOCAL(th_err) +th_listener_async_accept(th_listener* listener) +{ + th_socket socket; + th_socket_init(&socket, listener->loop, th_socket_ops_os()); + th_err err = TH_ERR_OK; +#if TH_WITH_SSL + if (listener->ssl_enabled) { + err = th_ssl_conn_create(&listener->conn, &socket, &listener->ssl_context, th_ssl_ops_os(), + &listener->upgrader.base, + (th_conn_observer*)&listener->conn_tracker, + listener->allocator); + } else +#endif + { + err = th_tcp_conn_create(&listener->conn, &socket, + &listener->upgrader.base, + (th_conn_observer*)&listener->conn_tracker, + listener->allocator); + } + if (err != TH_ERR_OK) { + return err; + } + th_accept_op_init(&listener->accept_op, &listener->acceptor, &listener->accept_addr, + th_conn_get_socket(listener->conn), + th_listener_accept_complete, listener); + th_op_perform(&listener->accept_op.base); + return TH_ERR_OK; +} -#include +TH_LOCAL(void) +th_listener_client_destroy_handler_fn(void* self) +{ + th_listener* listener = self; + if (!listener->running) + return; + th_err err = TH_ERR_OK; + if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to initiate accept: %s, try again later", th_strerror(err)); + th_conn_tracker_async_wait(&listener->conn_tracker, &listener->client_destroy_handler.base); + } +} -typedef enum th_url_decode_type { - TH_URL_DECODE_TYPE_PATH = 0, - TH_URL_DECODE_TYPE_QUERY -} th_url_decode_type; +TH_LOCAL(void) +th_listener_accept_complete(void* user_data, th_err err) +{ + th_listener* listener = user_data; + if (err != TH_ERR_OK) { + TH_LOG_ERROR("Accept failed: %s", th_strerror(err)); + th_conn_destroy(TH_MOVE_PTR(listener->conn)); + } else { + th_conn_start(listener->conn); + } + if (!listener->running) { + return; + } + if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to initiate accept: %s, try again later", th_strerror(err)); + th_conn_tracker_async_wait(&listener->conn_tracker, &listener->client_destroy_handler.base); + } +} TH_PRIVATE(th_err) -th_url_decode_string(th_str input, th_string* output, th_url_decode_type type); - -/* End of th_url_decode.h */ -/* Start of th_align.h */ - -#include +th_listener_start(th_listener* listener) +{ + // Client destroy handler + listener->client_destroy_handler.listener = listener; + th_task_init(&listener->client_destroy_handler.base, th_listener_client_destroy_handler_fn); + listener->running = 1; + th_err err = TH_ERR_OK; + if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) + return err; + return TH_ERR_OK; +} -#define TH_ALIGNOF(type) ((size_t)&(((struct { char c; type member; }*)0)->member)) -#define TH_ALIGNAS(align, ptr) ((void*)(((uintptr_t)(ptr) + ((uintptr_t)(align) - 1)) & ~((uintptr_t)(align) - 1))) -#define TH_ALIGNUP(n, align) (((n) + (size_t)(align) - 1) & ~((size_t)(align) - 1)) -#define TH_ALIGNDOWN(n, align) ((n) & ~((align) - 1)) +TH_PRIVATE(void) +th_listener_stop(th_listener* listener) +{ + listener->running = 0; + th_acceptor_cancel(&listener->acceptor); + th_conn_tracker_cancel_all(&listener->conn_tracker); +} -typedef long double th_max_align; +TH_LOCAL(void) +th_listener_deinit(th_listener* listener) +{ + th_acceptor_deinit(&listener->acceptor); + th_conn_tracker_deinit(&listener->conn_tracker); +#if TH_WITH_SSL + if (listener->ssl_enabled) + th_ssl_context_deinit(&listener->ssl_context); +#endif +} -/* End of th_align.h */ -/* Start of src/th_server.c */ +TH_PRIVATE(void) +th_listener_destroy(th_listener* listener) +{ + th_listener_deinit(listener); + th_allocator_free(listener->allocator, listener); +} +/* End of src/th_listener.c */ +/* Start of src/th_router.c */ +#include +#include -struct th_server { - th_reactor* reactor; - th_loop loop; - th_router router; - th_dir_mgr dir_mgr; - th_fcache fcache; - th_listener* listeners; - th_allocator* allocator; -}; +TH_LOCAL(th_err) +th_route_init(th_route_segment* route, th_capture_type type, th_str segment, th_allocator* allocator) +{ + th_string_init(&route->name, allocator); + th_err err = TH_ERR_OK; + if ((err = th_string_set(&route->name, segment)) != TH_ERR_OK) { + th_string_deinit(&route->name); + return err; + } + route->type = type; + route->next = NULL; + route->children = NULL; + route->allocator = allocator; + for (size_t i = 0; i < TH_METHOD_MAX; ++i) + route->handler[i] = (th_route_handler){NULL, NULL}; + route->ws_handler = (th_ws_route_handler){NULL, NULL}; + return TH_ERR_OK; +} TH_LOCAL(th_err) -th_server_init(th_server* server, th_allocator* allocator) +th_route_create(th_route_segment** out, th_capture_type type, th_str token, th_allocator* allocator) { - th_router_init(&server->router, allocator); + th_route_segment* route = th_allocator_alloc(allocator, sizeof(th_route_segment)); + if (!route) + return TH_ERR_BAD_ALLOC; th_err err = TH_ERR_OK; - th_loop_init(&server->loop, NULL); - if ((err = th_poll_create(&server->reactor, &server->loop, allocator, th_clock_os(), th_pollops_os())) != TH_ERR_OK) - goto cleanup_router; - server->loop.reactor = server->reactor; - th_dir_mgr_init(&server->dir_mgr, allocator); - th_fcache_init(&server->fcache, th_file_ops_os(), allocator); - server->listeners = NULL; - server->allocator = allocator; -cleanup_router: - th_router_deinit(&server->router); - return err; + if ((err = th_route_init(route, type, token, allocator)) != TH_ERR_OK) { + th_allocator_free(allocator, route); + return err; + } + *out = route; + return TH_ERR_OK; } TH_LOCAL(void) -th_server_stop(th_server* server) +th_route_destroy(th_route_segment* route); + +TH_LOCAL(void) +th_route_deinit(th_route_segment* route) { - th_listener* listener = server->listeners; - while (listener) { - th_listener_stop(listener); - listener = listener->next; + for (th_route_segment* child = route->children; child != NULL;) { + th_route_segment* next = child->next; + th_route_destroy(child); + child = next; } - th_loop_run(&server->loop); + th_string_deinit(&route->name); } TH_LOCAL(void) -th_server_deinit(th_server* server) +th_route_destroy(th_route_segment* route) +{ + th_route_deinit(route); + th_allocator_free(route->allocator, route); +} + +TH_PRIVATE(void) +th_router_init(th_router* router, th_allocator* allocator) +{ + router->routes = NULL; + router->allocator = allocator; + if (!router->allocator) + router->allocator = th_default_allocator_get(); +} + +TH_PRIVATE(void) +th_router_deinit(th_router* router) { - th_listener* listener = server->listeners; - while (listener) { - th_listener* next = listener->next; - th_listener_destroy(listener); - listener = next; + for (th_route_segment* route = router->routes; route != NULL;) { + th_route_segment* next = route->next; + th_route_destroy(route); + route = next; } - th_loop_deinit(&server->loop); - th_reactor_destroy(server->reactor); - th_router_deinit(&server->router); - th_fcache_deinit(&server->fcache); - th_dir_mgr_deinit(&server->dir_mgr); } TH_LOCAL(th_err) -th_server_bind(th_server* server, const char* host, const char* port, th_bind_opt* opt) +th_route_consume_trail(th_route_segment* route, th_str* trail, th_router_capture_cb on_capture, void* userp, bool* result) { - th_listener* listener = NULL; + th_str route_name = th_string_view(&route->name); + th_str raw_segment = th_str_substr(*trail, 0, th_str_find_first_of(*trail, 0, "/?")); + th_string decoded; + bool decoded_init = false; + th_str segment = raw_segment; th_err err = TH_ERR_OK; - if ((err = th_listener_create(&listener, &server->loop, - host, port, - &server->router, &server->dir_mgr, &server->fcache, - opt, server->allocator)) - != TH_ERR_OK) { - return err; + if (th_str_find_first(raw_segment, 0, '%') != th_str_npos) { + th_string_init(&decoded, route->allocator); + decoded_init = true; + if ((err = th_url_decode_string(raw_segment, &decoded, TH_URL_DECODE_TYPE_PATH)) != TH_ERR_OK) { + goto cleanup; + } + segment = th_string_view(&decoded); } - if ((err = th_listener_start(listener)) != TH_ERR_OK) { - th_listener_destroy(listener); - return err; + switch (route->type) { + case TH_CAPTURE_TYPE_NONE: + if (th_str_eq(route_name, segment)) { + *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); + *result = true; + } + break; + case TH_CAPTURE_TYPE_INT: + if (th_str_is_uint(segment)) { + if (on_capture) + on_capture(userp, route_name, segment); + *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); + *result = true; + } + break; + case TH_CAPTURE_TYPE_STRING: + if (on_capture) + on_capture(userp, route_name, segment); + *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); + *result = true; + break; + case TH_CAPTURE_TYPE_PATH: + if (on_capture) + on_capture(userp, route_name, *trail); + *trail = th_str_make(NULL, 0); + *result = true; + break; + default: + break; } - listener->next = server->listeners; - server->listeners = listener; - return TH_ERR_OK; +cleanup: + if (decoded_init) + th_string_deinit(&decoded); + return err; } TH_LOCAL(th_err) -th_server_route(th_server* server, th_method method, const char* path, th_handler handler, void* user_data) +th_router_resolve(th_router* router, th_str path, th_router_capture_cb on_capture, void* userp, th_route_segment** out) { - return th_router_add_route(&server->router, method, th_str_from_cstr(path), handler, user_data); + if (th_str_empty(path) || *path.ptr != '/') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + th_str trail = th_str_substr(path, 1, th_str_npos); + th_route_segment* route = router->routes; + while (1) { + th_err err = TH_ERR_OK; + bool consumed = false; + if (route == NULL) { + break; + } else if ((err = th_route_consume_trail(route, &trail, on_capture, userp, &consumed)) != TH_ERR_OK + || consumed) { + if (err != TH_ERR_OK) + return err; + if (th_str_empty(trail)) + break; + route = route->children; + } else { + route = route->next; + } + } + if (route == NULL) { + return TH_ERR_HTTP(TH_CODE_NOT_FOUND); + } + *out = route; + return TH_ERR_OK; } -TH_LOCAL(th_err) -th_server_add_dir(th_server* server, const char* name, const char* path) +TH_LOCAL(void) +th_router_capture_to_pathvar(void* userp, th_str key, th_str value) { - th_dir dir; - th_dir_init(&dir, th_dir_ops_os()); - th_err err = TH_ERR_OK; - if ((err = th_dir_open(&dir, th_str_from_cstr(path))) != TH_ERR_OK) { - th_dir_deinit(&dir); - return err; - } - return th_dir_mgr_add(&server->dir_mgr, th_str_from_cstr(name), dir); + th_request* request = userp; + (void)th_request_add_pathvar(request, key, value); } TH_LOCAL(th_err) -th_server_save_to_disk(th_server* server, th_buffer data, const char* dir_label, const char* filepath) +th_router_do_handle(th_router* router, th_method method, th_request* request, th_response* response, bool dry) { - th_dir* dir = th_dir_mgr_get(&server->dir_mgr, th_str_from_cstr(dir_label)); - if (!dir) - return TH_ERR_HTTP(TH_CODE_NOT_FOUND); + th_route_segment* route = NULL; th_err err = TH_ERR_OK; - th_filepath path; - if ((err = th_filepath_init(&path, th_str_from_cstr(filepath))) != TH_ERR_OK) - return err; - th_open_opt opt = {.create = true, .write = true, .truncate = true}; - th_file file; - th_file_init(&file, server->fcache.file_ops); - if ((err = th_file_openat(&file, dir, &path, opt)) != TH_ERR_OK) + th_router_capture_cb on_capture = dry ? NULL : th_router_capture_to_pathvar; + if ((err = th_router_resolve(router, th_string_view(&request->uri_path), on_capture, request, &route)) != TH_ERR_OK) return err; - size_t total_written = 0; - while (total_written < data.len) { - size_t written = 0; - if ((err = th_file_write(&file, data.ptr + total_written, data.len - total_written, total_written, &written)) - != TH_ERR_OK) { - th_file_close(&file); - return err; - } - total_written += written; + th_route_handler handler = route->handler[method].handler ? route->handler[method] : route->handler[TH_METHOD_ANY]; + if (handler.handler == NULL) { + return TH_ERR_HTTP(TH_CODE_METHOD_NOT_ALLOWED); } - th_file_close(&file); - return TH_ERR_OK; + if (dry) + return TH_ERR_OK; + return handler.handler(handler.user_data, request, response); } -TH_LOCAL(th_err) -th_server_poll(th_server* server, int timeout_ms) +TH_PRIVATE(th_err) +th_router_handle(th_router* router, th_request* request, th_response* response) { - return th_loop_poll(&server->loop, timeout_ms); + return th_router_do_handle(router, request->method, request, response, false); } -/* public server API */ - -TH_PUBLIC(th_err) -th_server_create(th_server** out, th_allocator* allocator) +TH_PRIVATE(bool) +th_router_would_handle(th_router* router, th_method method, th_request* request) { - allocator = allocator ? allocator : th_default_allocator_get(); - th_server* server = th_allocator_alloc(allocator, sizeof(th_server)); - if (!server) - return TH_ERR_BAD_ALLOC; - th_err err = TH_ERR_OK; - if ((err = th_server_init(server, allocator)) != TH_ERR_OK) { - th_allocator_free(server->allocator, server); - return err; - } - *out = server; - return TH_ERR_OK; + return th_router_do_handle(router, method, request, NULL, true) == TH_ERR_OK; } -TH_PUBLIC(void) -th_server_destroy(th_server* server) +TH_PRIVATE(bool) +th_router_find_ws_route(th_router* router, th_str path, th_ws_handler* handler, void** user_data) { - th_server_stop(server); - th_server_deinit(server); - th_allocator_free(server->allocator, server); + th_route_segment* route = NULL; + if (th_router_resolve(router, path, NULL, NULL, &route) != TH_ERR_OK) + return false; + if (route->ws_handler.handler == NULL) + return false; + *handler = route->ws_handler.handler; + *user_data = route->ws_handler.user_data; + return true; } -TH_PUBLIC(th_err) -th_bind(th_server* server, const char* addr, const char* port, th_bind_opt* opt) +// abc < {int} < {string} < {path} +TH_LOCAL(bool) +th_route_lower(th_route_segment* lh, th_route_segment* rh) { - return th_server_bind(server, addr, port, opt); + return lh->type < rh->type; } -TH_PUBLIC(th_err) -th_route(th_server* server, th_method method, const char* route, th_handler handler, void* userp) +TH_LOCAL(void) +th_route_insert_sorted(th_route_segment** list, th_route_segment* route) { - return th_server_route(server, method, route, handler, userp); + while (*list != NULL && th_route_lower(*list, route)) + list = &(*list)->next; + th_route_segment* temp = *list; + *list = route; + route->next = temp; } -TH_PUBLIC(th_err) -th_add_dir(th_server* server, const char* name, const char* path) +TH_LOCAL(th_err) +th_route_parse_trail(th_str* trail, th_str* name, th_capture_type* type) { - return th_server_add_dir(server, name, path); + th_str segment = th_str_substr(*trail, 0, th_str_find_first_of(*trail, 0, "/")); + size_t open_curly = th_str_find_first(segment, 0, '{'); + size_t close_curly = th_str_find_first(segment, 0, '}'); + if (segment.len > 2 && open_curly == 0 && close_curly == segment.len - 1) { + th_str capture = th_str_substr(segment, 1, segment.len - 2); + size_t sep = th_str_find_first(capture, 0, ':'); + if (sep == th_str_npos) { + *name = capture; + *type = TH_CAPTURE_TYPE_STRING; + } else { + th_str type_str = th_str_substr(capture, 0, sep); + if (th_str_eq(type_str, TH_STR("int"))) { + *name = th_str_substr(capture, sep + 1, th_str_npos); + *type = TH_CAPTURE_TYPE_INT; + } else if (th_str_eq(type_str, TH_STR("path"))) { + *name = th_str_substr(capture, sep + 1, th_str_npos); + *type = TH_CAPTURE_TYPE_PATH; + } else { + return TH_ERR_INVALID_ARG; + } + } + } else if (open_curly == th_str_npos && close_curly == th_str_npos) { + *name = segment; + *type = TH_CAPTURE_TYPE_NONE; + } else { + return TH_ERR_INVALID_ARG; + } + // Consume segment + *trail = th_str_substr(*trail, segment.len + 1, th_str_npos); + return TH_ERR_OK; } -TH_PUBLIC(th_err) -th_save_to_disk(th_server* server, th_buffer data, const char* dir_label, const char* filepath) +TH_LOCAL(th_err) +th_router_find_or_create_segment(th_router* router, th_str path, th_route_segment** out) { - return th_server_save_to_disk(server, data, dir_label, filepath); + if (th_str_empty(path) || path.ptr[0] != '/') + return TH_ERR_INVALID_ARG; + th_str trail = th_str_substr(path, 1, th_str_npos); + th_route_segment** list = &router->routes; + th_route_segment* route = *list; + + // find a matching route + bool last = false; + while (!last) { + th_str name = {0}; + th_capture_type type = TH_CAPTURE_TYPE_NONE; + th_err err = TH_ERR_OK; + if ((err = th_route_parse_trail(&trail, &name, &type)) != TH_ERR_OK) + return err; + last = th_str_empty(trail); + if (type == TH_CAPTURE_TYPE_PATH && !last) + return TH_ERR_INVALID_ARG; + while (1) { + if (route == NULL) { + if ((err = th_route_create(&route, type, name, router->allocator)) != TH_ERR_OK) + return err; + th_route_insert_sorted(list, route); + route = *list; // restart + } + if ((type == TH_CAPTURE_TYPE_NONE + && th_str_eq(th_string_view(&route->name), name)) + || (type != TH_CAPTURE_TYPE_NONE && type == route->type)) { + if (last) + break; + list = &route->children; + route = *list; + break; + } else { + route = route->next; + } + } + } + *out = route; + return TH_ERR_OK; } -TH_PUBLIC(th_err) -th_poll(th_server* server, int timeout_ms) +TH_LOCAL(th_err) +th_router_ws_default_handler(void* user_data, const th_request* request, th_response* response) { - return th_server_poll(server, timeout_ms); + (void)user_data; + (void)request; + (void)response; + return TH_ERR_HTTP(TH_CODE_SWITCHING_PROTOCOLS); } -/* End of src/th_server.c */ -/* Start of src/th_listener.c */ - -#include -#include -#include - - -#undef TH_LOG_TAG -#define TH_LOG_TAG "listener" -TH_LOCAL(th_err) -th_listener_enable_ssl(th_listener* listener, const char* key_file, const char* cert_file) +TH_PRIVATE(th_err) +th_router_add_route(th_router* router, th_method method, th_str path, th_handler handler, void* user_data) { -#if TH_WITH_SSL + th_route_segment* route = NULL; th_err err = TH_ERR_OK; - if ((err = th_ssl_context_init(&listener->ssl_context, th_ssl_ops_os(), key_file, cert_file)) != TH_ERR_OK) + if ((err = th_router_find_or_create_segment(router, path, &route)) != TH_ERR_OK) return err; - listener->ssl_enabled = true; + bool is_ws_default = route->handler[method].handler == th_router_ws_default_handler; + if (route->handler[TH_METHOD_ANY].handler != NULL + || (route->handler[method].handler != NULL && !is_ws_default)) + return TH_ERR_INVALID_ARG; // Route already exists + route->handler[method].handler = handler; + route->handler[method].user_data = user_data; return TH_ERR_OK; -#else - (void)listener; - (void)key_file; - (void)cert_file; - TH_LOG_ERROR("SSL is not enabled in this build."); - return TH_ERR_NOSUPPORT; -#endif } -TH_LOCAL(th_err) -th_listener_init(th_listener* listener, th_loop* loop, - const char* host, const char* port, - th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, - th_bind_opt* opt, th_allocator* allocator) +TH_PRIVATE(th_err) +th_router_add_ws_route(th_router* router, th_str path, th_ws_handler handler, void* user_data) { - listener->loop = loop; - listener->running = 0; - listener->ssl_enabled = false; - listener->allocator = allocator ? allocator : th_default_allocator_get(); + th_route_segment* route = NULL; th_err err = TH_ERR_OK; - th_acceptor_init(&listener->acceptor, loop, th_acceptor_ops_os()); - if ((err = th_acceptor_open(&listener->acceptor, host, port)) != TH_ERR_OK) + if ((err = th_router_find_or_create_segment(router, path, &route)) != TH_ERR_OK) return err; - if (opt && opt->key_file && opt->cert_file) { - if ((err = th_listener_enable_ssl(listener, opt->key_file, opt->cert_file)) != TH_ERR_OK) - goto cleanup_acceptor; + if (route->ws_handler.handler != NULL) + return TH_ERR_INVALID_ARG; // WS route already exists + route->ws_handler.handler = handler; + route->ws_handler.user_data = user_data; + // No gating th_route registered for this path/method yet: default to + // allowing the upgrade, so a WS-only route doesn't 405. + if (route->handler[TH_METHOD_GET].handler == NULL && route->handler[TH_METHOD_ANY].handler == NULL) { + route->handler[TH_METHOD_GET].handler = th_router_ws_default_handler; + route->handler[TH_METHOD_GET].user_data = NULL; } - th_conn_tracker_init(&listener->conn_tracker); - th_http_upgrader_init(&listener->upgrader, &listener->conn_tracker, router, dir_mgr, fcache, allocator); - TH_LOG_INFO("Created listener on %s:%s", host, port); return TH_ERR_OK; -cleanup_acceptor: - th_acceptor_deinit(&listener->acceptor); - return err; } +/* End of src/th_router.c */ +/* Start of src/th_mime.c */ +/* ANSI-C code produced by gperf version 3.2.1 */ +/* Computed positions: -k'1,$' */ -TH_PRIVATE(th_err) -th_listener_create(th_listener** out, th_loop* loop, - const char* host, const char* port, - th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, - th_bind_opt* opt, th_allocator* allocator) +#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ + && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ + && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ + && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ + && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ + && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ + && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ + && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ + && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ + && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ + && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ + && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ + && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ + && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ + && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ + && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ + && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ + && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ + && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ + && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ + && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ + && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ + && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) +/* The character set is not based on ISO-646. */ +#error "gperf generated tables don't work with this execution character set. Please report a bug to ." +#endif + + +#include +#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#pragma GCC diagnostic ignored "-Wconversion" +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +struct th_mime_mapping; + +#define TH_MIME_TOTAL_KEYWORDS 33 +#define TH_MIME_MIN_WORD_LENGTH 2 +#define TH_MIME_MAX_WORD_LENGTH 5 +#define TH_MIME_MIN_HASH_VALUE 3 +#define TH_MIME_MAX_HASH_VALUE 88 +/* maximum key range = 86, duplicates = 0 */ + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static unsigned int +th_mime_hash (register const char *str, register size_t len) { - th_listener* listener = th_allocator_alloc(allocator, sizeof(th_listener)); - if (!listener) - return TH_ERR_BAD_ALLOC; - th_err err = TH_ERR_OK; - if ((err = th_listener_init(listener, loop, host, port, router, dir_mgr, fcache, opt, allocator)) != TH_ERR_OK) - goto cleanup; - *out = listener; - return TH_ERR_OK; -cleanup: - th_allocator_free(allocator, listener); - return err; + static unsigned char asso_values[] = + { + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 0, 45, 40, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 15, 89, 40, + 0, 89, 25, 10, 0, 1, 5, 89, 35, 40, + 0, 0, 20, 89, 89, 10, 35, 89, 0, 5, + 30, 89, 55, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, + 89, 89, 89, 89, 89, 89 + }; + return len + asso_values[(unsigned char)str[len - 1]] + asso_values[(unsigned char)str[0]]; } -TH_LOCAL(void) -th_listener_accept_complete(void* user_data, th_err err); - -TH_LOCAL(th_err) -th_listener_async_accept(th_listener* listener) +struct th_mime_mapping * +th_mime_mapping_find (register const char *str, register size_t len) { - th_socket socket; - th_socket_init(&socket, listener->loop, th_socket_ops_os()); - th_err err = TH_ERR_OK; -#if TH_WITH_SSL - if (listener->ssl_enabled) { - err = th_ssl_conn_create(&listener->conn, &socket, &listener->ssl_context, th_ssl_ops_os(), - &listener->upgrader.base, - (th_conn_observer*)&listener->conn_tracker, - listener->allocator); - } else +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif + static struct th_mime_mapping wordlist[] = { - err = th_tcp_conn_create(&listener->conn, &socket, - &listener->upgrader.base, - (th_conn_observer*)&listener->conn_tracker, - listener->allocator); - } - if (err != TH_ERR_OK) { - return err; - } - th_accept_op_init(&listener->accept_op, &listener->acceptor, &listener->accept_addr, - th_conn_get_socket(listener->conn), - th_listener_accept_complete, listener); - th_op_perform(&listener->accept_op.base); - return TH_ERR_OK; -} + {""}, {""}, {""}, + {"ogv", TH_STR_INIT("video/ogg")}, + {"ico", TH_STR_INIT("image/x-icon")}, + {""}, {""}, {""}, + {"wav", TH_STR_INIT("audio/wav")}, + {"json", TH_STR_INIT("application/json")}, + {"woff2",TH_STR_INIT("font/woff2")}, + {""}, {""}, + {"ogg", TH_STR_INIT("audio/ogg")}, + {"opus", TH_STR_INIT("audio/opus")}, + {""}, {""}, + {"js", TH_STR_INIT("text/javascript")}, + {"jpg", TH_STR_INIT("image/jpeg")}, + {"jpeg", TH_STR_INIT("image/jpeg")}, + {""}, {""}, {""}, + {"svg", TH_STR_INIT("image/svg+xml")}, + {"weba", TH_STR_INIT("audio/webm")}, + {""}, {""}, {""}, + {"otf", TH_STR_INIT("font/otf")}, + {"webp", TH_STR_INIT("image/webp")}, + {""}, {""}, {""}, + {"png", TH_STR_INIT("image/png")}, + {"woff", TH_STR_INIT("font/woff")}, + {""}, {""}, {""}, + {"gif", TH_STR_INIT("image/gif")}, + {"html", TH_STR_INIT("text/html")}, + {""}, {""}, + {"md", TH_STR_INIT("text/markdown")}, + {"csv", TH_STR_INIT("text/csv")}, + {"avif", TH_STR_INIT("image/avif")}, + {""}, {""}, {""}, + {"pdf", TH_STR_INIT("application/pdf")}, + {"webm", TH_STR_INIT("video/webm")}, + {""}, {""}, {""}, + {"css", TH_STR_INIT("text/css")}, + {"mpeg", TH_STR_INIT("video/mpeg")}, + {""}, {""}, {""}, + {"aac", TH_STR_INIT("audio/aac")}, + {""}, {""}, {""}, {""}, + {"ttf", TH_STR_INIT("font/ttf")}, + {""}, {""}, {""}, {""}, + {"xml", TH_STR_INIT("application/xml")}, + {""}, + {"xhtml",TH_STR_INIT("application/xhtml+xml")}, + {""}, {""}, + {"txt", TH_STR_INIT("text/plain")}, + {""}, {""}, {""}, {""}, + {"zip", TH_STR_INIT("application/zip")}, + {""}, {""}, {""}, {""}, + {"mp4", TH_STR_INIT("video/mp4")}, + {""}, {""}, {""}, {""}, + {"mp3", TH_STR_INIT("audio/mpeg")} + }; +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic pop +#endif + + if (len <= TH_MIME_MAX_WORD_LENGTH && len >= TH_MIME_MIN_WORD_LENGTH) + { + register unsigned int key = th_mime_hash (str, len); -TH_LOCAL(void) -th_listener_client_destroy_handler_fn(void* self) -{ - th_listener* listener = self; - if (!listener->running) - return; - th_err err = TH_ERR_OK; - if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to initiate accept: %s, try again later", th_strerror(err)); - th_conn_tracker_async_wait(&listener->conn_tracker, &listener->client_destroy_handler.base); - } -} + if (key <= TH_MIME_MAX_HASH_VALUE) + { + register const char *s = wordlist[key].name; -TH_LOCAL(void) -th_listener_accept_complete(void* user_data, th_err err) -{ - th_listener* listener = user_data; - if (err != TH_ERR_OK) { - TH_LOG_ERROR("Accept failed: %s", th_strerror(err)); - th_conn_destroy(TH_MOVE_PTR(listener->conn)); - } else { - th_conn_start(listener->conn); - } - if (!listener->running) { - return; - } - if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to initiate accept: %s, try again later", th_strerror(err)); - th_conn_tracker_async_wait(&listener->conn_tracker, &listener->client_destroy_handler.base); + if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') + return &wordlist[key]; + } } + return (struct th_mime_mapping *) 0; } -TH_PRIVATE(th_err) -th_listener_start(th_listener* listener) -{ - // Client destroy handler - listener->client_destroy_handler.listener = listener; - th_task_init(&listener->client_destroy_handler.base, th_listener_client_destroy_handler_fn); - listener->running = 1; - th_err err = TH_ERR_OK; - if ((err = th_listener_async_accept(listener)) != TH_ERR_OK) - return err; - return TH_ERR_OK; -} - -TH_PRIVATE(void) -th_listener_stop(th_listener* listener) -{ - listener->running = 0; - th_acceptor_cancel(&listener->acceptor); - th_conn_tracker_cancel_all(&listener->conn_tracker); -} +#pragma GCC diagnostic pop +/* End of src/th_mime.c */ +/* Start of src/th_method.c */ +/* ANSI-C code produced by gperf version 3.2.1 */ +/* Computed positions: -k'1' */ -TH_LOCAL(void) -th_listener_deinit(th_listener* listener) -{ - th_acceptor_deinit(&listener->acceptor); - th_conn_tracker_deinit(&listener->conn_tracker); -#if TH_WITH_SSL - if (listener->ssl_enabled) - th_ssl_context_deinit(&listener->ssl_context); +#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ + && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ + && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ + && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ + && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ + && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ + && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ + && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ + && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ + && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ + && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ + && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ + && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ + && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ + && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ + && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ + && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ + && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ + && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ + && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ + && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ + && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ + && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) +/* The character set is not based on ISO-646. */ +#error "gperf generated tables don't work with this execution character set. Please report a bug to ." #endif -} -TH_PRIVATE(void) -th_listener_destroy(th_listener* listener) -{ - th_listener_deinit(listener); - th_allocator_free(listener->allocator, listener); -} -/* End of src/th_listener.c */ -/* Start of src/th_router.c */ -#include +#include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#pragma GCC diagnostic ignored "-Wconversion" +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +struct th_method_mapping; -#undef TH_LOG_TAG -#define TH_LOG_TAG "router" +#define TH_METHOD_TOTAL_KEYWORDS 9 +#define TH_METHOD_MIN_WORD_LENGTH 3 +#define TH_METHOD_MAX_WORD_LENGTH 7 +#define TH_METHOD_MIN_HASH_VALUE 3 +#define TH_METHOD_MAX_HASH_VALUE 12 +/* maximum key range = 10, duplicates = 0 */ -TH_LOCAL(th_err) -th_route_init(th_route_segment* route, th_capture_type type, th_str segment, th_allocator* allocator) +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static unsigned int +th_method_hash (register const char *str, register size_t len) { - th_string_init(&route->name, allocator); - th_err err = TH_ERR_OK; - if ((err = th_string_set(&route->name, segment)) != TH_ERR_OK) { - th_string_deinit(&route->name); - return err; - } - route->type = type; - route->next = NULL; - route->children = NULL; - route->allocator = allocator; - for (size_t i = 0; i < TH_METHOD_MAX; ++i) - route->handler[i] = (th_route_handler){NULL, NULL}; - return TH_ERR_OK; + static unsigned char asso_values[] = + { + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 5, 0, 13, + 13, 0, 0, 13, 13, 13, 13, 13, 13, 0, + 5, 13, 13, 13, 0, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13 + }; + return len + asso_values[(unsigned char)str[0]]; } -TH_LOCAL(th_err) -th_route_create(th_route_segment** out, th_capture_type type, th_str token, th_allocator* allocator) +struct th_method_mapping * +th_method_mapping_find (register const char *str, register size_t len) { - th_route_segment* route = th_allocator_alloc(allocator, sizeof(th_route_segment)); - if (!route) - return TH_ERR_BAD_ALLOC; - th_err err = TH_ERR_OK; - if ((err = th_route_init(route, type, token, allocator)) != TH_ERR_OK) { - th_allocator_free(allocator, route); - return err; - } - *out = route; - return TH_ERR_OK; -} +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + static struct th_method_mapping wordlist[] = + { + {""}, {""}, {""}, + {"GET", TH_METHOD_GET}, + {"HEAD", TH_METHOD_HEAD}, + {"TRACE", TH_METHOD_TRACE}, + {"DELETE", TH_METHOD_DELETE}, + {"OPTIONS", TH_METHOD_OPTIONS}, + {"PUT", TH_METHOD_PUT}, + {"POST", TH_METHOD_POST}, + {"PATCH", TH_METHOD_PATCH}, + {""}, + {"CONNECT", TH_METHOD_CONNECT} + }; +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic pop +#endif -TH_LOCAL(void) -th_route_destroy(th_route_segment* route); + if (len <= TH_METHOD_MAX_WORD_LENGTH && len >= TH_METHOD_MIN_WORD_LENGTH) + { + register unsigned int key = th_method_hash (str, len); -TH_LOCAL(void) -th_route_deinit(th_route_segment* route) -{ - for (th_route_segment* child = route->children; child != NULL;) { - th_route_segment* next = child->next; - th_route_destroy(child); - child = next; + if (key <= TH_METHOD_MAX_HASH_VALUE) + { + register const char *s = wordlist[key].name; + + if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') + return &wordlist[key]; + } } - th_string_deinit(&route->name); + return (struct th_method_mapping *) 0; } -TH_LOCAL(void) -th_route_destroy(th_route_segment* route) -{ - th_route_deinit(route); - th_allocator_free(route->allocator, route); -} +#pragma GCC diagnostic pop +/* End of src/th_method.c */ +/* Start of src/th_allocator.c */ -TH_PRIVATE(void) -th_router_init(th_router* router, th_allocator* allocator) -{ - router->routes = NULL; - router->allocator = allocator; - if (!router->allocator) - router->allocator = th_default_allocator_get(); -} +#include +#include +#include +#include + +typedef struct th_default_allocator { + th_allocator base; +} th_default_allocator; -TH_PRIVATE(void) -th_router_deinit(th_router* router) +TH_LOCAL(void*) +th_default_allocator_alloc(void* self, size_t size) { - for (th_route_segment* route = router->routes; route != NULL;) { - th_route_segment* next = route->next; - th_route_destroy(route); - route = next; - } + (void)self; + void* ptr = malloc(size); + return ptr; } -TH_LOCAL(th_err) -th_route_consume_trail(th_route_segment* route, th_request* request, th_str* trail, bool dry, bool* result) +TH_LOCAL(void*) +th_default_allocator_realloc(void* self, void* ptr, size_t size) { - th_str route_name = th_string_view(&route->name); - th_str raw_segment = th_str_substr(*trail, 0, th_str_find_first_of(*trail, 0, "/?")); - th_string decoded; - bool decoded_init = false; - th_str segment = raw_segment; - th_err err = TH_ERR_OK; - if (th_str_find_first(raw_segment, 0, '%') != th_str_npos) { - th_string_init(&decoded, route->allocator); - decoded_init = true; - if ((err = th_url_decode_string(raw_segment, &decoded, TH_URL_DECODE_TYPE_PATH)) != TH_ERR_OK) { - goto cleanup; - } - segment = th_string_view(&decoded); - } - switch (route->type) { - case TH_CAPTURE_TYPE_NONE: - if (th_str_eq(route_name, segment)) { - *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); - *result = true; - } - break; - case TH_CAPTURE_TYPE_INT: - if (th_str_is_uint(segment)) { - if (!dry) - (void)th_request_add_pathvar(request, route_name, segment); - *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); - *result = true; - } - break; - case TH_CAPTURE_TYPE_STRING: - if (!dry) - (void)th_request_add_pathvar(request, route_name, segment); - *trail = th_str_substr(*trail, raw_segment.len + 1, th_str_npos); - *result = true; - break; - case TH_CAPTURE_TYPE_PATH: - if (!dry) - (void)th_request_add_pathvar(request, route_name, *trail); - *trail = th_str_make(NULL, 0); - *result = true; - break; - default: - break; - } -cleanup: - if (decoded_init) - th_string_deinit(&decoded); - return err; + (void)self; + return realloc(ptr, size); } -TH_LOCAL(th_err) -th_router_do_handle(th_router* router, th_method method, th_request* request, th_response* response, bool dry) +TH_LOCAL(void) +th_default_allocator_free(void* self, void* ptr) { - TH_LOG_DEBUG("Handling request %p: %s", request, th_string_data(&request->uri_path)); - if (*th_string_at(&request->uri_path, 0) != '/') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - th_str trail = th_str_substr(th_string_view(&request->uri_path), 1, th_str_npos); - th_route_segment* route = router->routes; - while (1) { - th_err err = TH_ERR_OK; - bool consumed = false; - if (route == NULL) { - break; - } else if ((err = th_route_consume_trail(route, request, &trail, dry, &consumed)) != TH_ERR_OK - || consumed) { - if (err != TH_ERR_OK) - return err; - if (th_str_empty(trail)) - break; - route = route->children; - } else { - route = route->next; - } - } - if (route == NULL) { - return TH_ERR_HTTP(TH_CODE_NOT_FOUND); - } - th_route_handler handler = route->handler[method].handler ? route->handler[method] : route->handler[TH_METHOD_ANY]; - if (handler.handler == NULL) { - return TH_ERR_HTTP(TH_CODE_METHOD_NOT_ALLOWED); - } - if (dry) - return TH_ERR_OK; - return handler.handler(handler.user_data, request, response); + (void)self; + free(ptr); } -TH_PRIVATE(th_err) -th_router_handle(th_router* router, th_request* request, th_response* response) -{ - return th_router_do_handle(router, request->method, request, response, false); -} +static th_default_allocator default_allocator = { + .base = { + .alloc = th_default_allocator_alloc, + .realloc = th_default_allocator_realloc, + .free = th_default_allocator_free, + }, +}; -TH_PRIVATE(bool) -th_router_would_handle(th_router* router, th_method method, th_request* request) +static th_allocator* user_default_allocator = NULL; + +TH_PUBLIC(th_allocator*) +th_default_allocator_get(void) { - return th_router_do_handle(router, method, request, NULL, true) == TH_ERR_OK; + if (user_default_allocator) + return user_default_allocator; + return &default_allocator.base; } -// abc < {int} < {string} < {path} -TH_LOCAL(bool) -th_route_lower(th_route_segment* lh, th_route_segment* rh) +TH_PUBLIC(void) +th_default_allocator_set(th_allocator* allocator) { - return lh->type < rh->type; + user_default_allocator = allocator; } -TH_LOCAL(void) -th_route_insert_sorted(th_route_segment** list, th_route_segment* route) +/* th_arena_allocator implementation begin */ + +TH_LOCAL(void*) +th_arena_allocator_alloc(void* self, size_t size) { - while (*list != NULL && th_route_lower(*list, route)) - list = &(*list)->next; - th_route_segment* temp = *list; - *list = route; - route->next = temp; + th_arena_allocator* allocator = self; + if (allocator->pos + size > allocator->size) { + if (!allocator->allocator) + return NULL; + return th_allocator_alloc(allocator->allocator, size); + } + void* ptr = (char*)allocator->buf + allocator->pos; + allocator->prev_pos = allocator->pos; + allocator->pos += (size_t)TH_ALIGNAS(allocator->alignment, size); + return ptr; } -TH_LOCAL(th_err) -th_route_parse_trail(th_str* trail, th_str* name, th_capture_type* type) +TH_LOCAL(void*) +th_arena_allocator_realloc(void* self, void* ptr, size_t size) { - th_str segment = th_str_substr(*trail, 0, th_str_find_first_of(*trail, 0, "/")); - size_t open_curly = th_str_find_first(segment, 0, '{'); - size_t close_curly = th_str_find_first(segment, 0, '}'); - if (segment.len > 2 && open_curly == 0 && close_curly == segment.len - 1) { - th_str capture = th_str_substr(segment, 1, segment.len - 2); - size_t sep = th_str_find_first(capture, 0, ':'); - if (sep == th_str_npos) { - *name = capture; - *type = TH_CAPTURE_TYPE_STRING; - } else { - th_str type_str = th_str_substr(capture, 0, sep); - if (th_str_eq(type_str, TH_STR("int"))) { - *name = th_str_substr(capture, sep + 1, th_str_npos); - *type = TH_CAPTURE_TYPE_INT; - } else if (th_str_eq(type_str, TH_STR("path"))) { - *name = th_str_substr(capture, sep + 1, th_str_npos); - *type = TH_CAPTURE_TYPE_PATH; - } else { - return TH_ERR_INVALID_ARG; - } + th_arena_allocator* allocator = self; + if (ptr == NULL) + return th_arena_allocator_alloc(self, size); + if ((char*)ptr < (char*)allocator->buf || (char*)ptr >= (char*)allocator->buf + allocator->size) + return th_allocator_realloc(allocator->allocator, ptr, size); + if (ptr == (char*)allocator->buf + allocator->prev_pos) { + if (allocator->prev_pos + size > allocator->size) { + if (!allocator->allocator) + return NULL; + void* newp = th_allocator_alloc(allocator->allocator, size); + if (!newp) + return NULL; + memcpy(newp, ptr, allocator->pos - allocator->prev_pos); + allocator->pos = allocator->prev_pos; + return newp; } - } else if (open_curly == th_str_npos && close_curly == th_str_npos) { - *name = segment; - *type = TH_CAPTURE_TYPE_NONE; - } else { - return TH_ERR_INVALID_ARG; + allocator->pos = allocator->prev_pos + size; + return ptr; } - // Consume segment - *trail = th_str_substr(*trail, segment.len + 1, th_str_npos); - return TH_ERR_OK; + void* newp = th_allocator_alloc(self, size); + if (!newp) + return NULL; + size_t max_possible = (size_t)(((uint8_t*)allocator->buf + allocator->prev_pos) - (uint8_t*)ptr); + memcpy(newp, ptr, max_possible); + return newp; } -TH_PRIVATE(th_err) -th_router_add_route(th_router* router, th_method method, th_str path, th_handler handler, void* user_data) +TH_LOCAL(void) +th_arena_allocator_free(void* self, void* ptr) { - if (th_str_empty(path) || path.ptr[0] != '/') - return TH_ERR_INVALID_ARG; - th_str trail = th_str_substr(path, 1, th_str_npos); - th_route_segment** list = &router->routes; - th_route_segment* route = *list; - - // find a matching route - bool last = false; - while (!last) { - th_str name = {0}; - th_capture_type type = TH_CAPTURE_TYPE_NONE; - th_err err = TH_ERR_OK; - if ((err = th_route_parse_trail(&trail, &name, &type)) != TH_ERR_OK) - return err; - last = th_str_empty(trail); - if (type == TH_CAPTURE_TYPE_PATH && !last) - return TH_ERR_INVALID_ARG; - while (1) { - if (route == NULL) { - if ((err = th_route_create(&route, type, name, router->allocator)) != TH_ERR_OK) - return err; - th_route_insert_sorted(list, route); - route = *list; // restart - } - if ((type == TH_CAPTURE_TYPE_NONE - && th_str_eq(th_string_view(&route->name), name)) - || (type != TH_CAPTURE_TYPE_NONE && type == route->type)) { - if (last) - break; - list = &route->children; - route = *list; - break; - } else { - route = route->next; - } - } + th_arena_allocator* allocator = self; + if ((uint8_t*)ptr == (uint8_t*)allocator->buf + allocator->prev_pos) { + allocator->pos = allocator->prev_pos; + return; + } + if ((char*)ptr < (char*)allocator->buf || (char*)ptr >= (char*)allocator->buf + allocator->size) { + th_allocator_free(allocator->allocator, ptr); + return; } +} + +TH_PRIVATE(void) +th_arena_allocator_init_with_alignment(th_arena_allocator* allocator, void* buf, size_t size, size_t alignment, th_allocator* fallback) +{ + allocator->base.alloc = th_arena_allocator_alloc; + allocator->base.realloc = th_arena_allocator_realloc; + allocator->base.free = th_arena_allocator_free; + allocator->allocator = fallback; + allocator->alignment = (uint16_t)alignment; + void* aligned = TH_ALIGNAS(alignment, buf); + allocator->size = size - (size_t)((uint8_t*)aligned - (uint8_t*)buf); + allocator->buf = aligned; + allocator->pos = 0; + allocator->prev_pos = 0; +} - if (route->handler[TH_METHOD_ANY].handler != NULL - || route->handler[method].handler != NULL) - return TH_ERR_INVALID_ARG; // Route already exists - route->handler[method].handler = handler; - route->handler[method].user_data = user_data; - return TH_ERR_OK; +TH_PRIVATE(void) +th_arena_allocator_init(th_arena_allocator* allocator, void* buf, size_t size, th_allocator* fallback) +{ + th_arena_allocator_init_with_alignment(allocator, buf, size, TH_ALIGNOF(th_max_align), fallback); } -/* End of src/th_router.c */ -/* Start of src/th_mime.c */ -/* ANSI-C code produced by gperf version 3.2.1 */ -/* Computed positions: -k'1,$' */ -#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ - && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ - && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ - && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ - && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ - && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ - && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ - && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ - && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ - && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ - && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ - && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ - && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ - && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ - && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ - && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ - && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ - && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ - && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ - && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ - && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ - && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ - && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) -/* The character set is not based on ISO-646. */ -#error "gperf generated tables don't work with this execution character set. Please report a bug to ." -#endif +/* th_arena_allocator implementation end */ +/* End of src/th_allocator.c */ +/* Start of src/th_task.c */ +#include +#include -#include +/* th_task functions begin */ + +TH_PRIVATE(void) +th_task_init(th_task* task, void (*fn)(void*)) +{ + TH_ASSERT(task); + task->fn = fn; + task->next = NULL; +} + +TH_PRIVATE(void) +th_task_complete(th_task* task) +{ + if (task->fn) + task->fn(task); +} + +/* th_task functions end */ +/* End of src/th_task.c */ +/* Start of src/th_poll.c */ + +#if !defined(TH_CONFIG_OS_WIN) + +#include #include -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__clang__) -#pragma clang diagnostic ignored "-Wshorten-64-to-32" -#endif -struct th_mime_mapping; +#include -#define TH_MIME_TOTAL_KEYWORDS 33 -#define TH_MIME_MIN_WORD_LENGTH 2 -#define TH_MIME_MAX_WORD_LENGTH 5 -#define TH_MIME_MIN_HASH_VALUE 3 -#define TH_MIME_MAX_HASH_VALUE 88 -/* maximum key range = 86, duplicates = 0 */ +#undef TH_LOG_TAG +#define TH_LOG_TAG "poll" -#ifdef __GNUC__ -__inline -#else -#ifdef __cplusplus -inline -#endif -#endif -static unsigned int -th_mime_hash (register const char *str, register size_t len) +/* th_pollops_os begin */ + +TH_LOCAL(int) +th_pollops_os_poll(void* self, struct pollfd* fds, nfds_t nfds, int timeout_ms) { - static unsigned char asso_values[] = - { - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 0, 45, 40, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 15, 89, 40, - 0, 89, 25, 10, 0, 1, 5, 89, 35, 40, - 0, 0, 20, 89, 89, 10, 35, 89, 0, 5, - 30, 89, 55, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, - 89, 89, 89, 89, 89, 89 - }; - return len + asso_values[(unsigned char)str[len - 1]] + asso_values[(unsigned char)str[0]]; + (void)self; + return poll(fds, nfds, timeout_ms); } -struct th_mime_mapping * -th_mime_mapping_find (register const char *str, register size_t len) +TH_PRIVATE(th_pollops*) +th_pollops_os(void) { -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif - static struct th_mime_mapping wordlist[] = - { - {""}, {""}, {""}, - {"ogv", TH_STR_INIT("video/ogg")}, - {"ico", TH_STR_INIT("image/x-icon")}, - {""}, {""}, {""}, - {"wav", TH_STR_INIT("audio/wav")}, - {"json", TH_STR_INIT("application/json")}, - {"woff2",TH_STR_INIT("font/woff2")}, - {""}, {""}, - {"ogg", TH_STR_INIT("audio/ogg")}, - {"opus", TH_STR_INIT("audio/opus")}, - {""}, {""}, - {"js", TH_STR_INIT("text/javascript")}, - {"jpg", TH_STR_INIT("image/jpeg")}, - {"jpeg", TH_STR_INIT("image/jpeg")}, - {""}, {""}, {""}, - {"svg", TH_STR_INIT("image/svg+xml")}, - {"weba", TH_STR_INIT("audio/webm")}, - {""}, {""}, {""}, - {"otf", TH_STR_INIT("font/otf")}, - {"webp", TH_STR_INIT("image/webp")}, - {""}, {""}, {""}, - {"png", TH_STR_INIT("image/png")}, - {"woff", TH_STR_INIT("font/woff")}, - {""}, {""}, {""}, - {"gif", TH_STR_INIT("image/gif")}, - {"html", TH_STR_INIT("text/html")}, - {""}, {""}, - {"md", TH_STR_INIT("text/markdown")}, - {"csv", TH_STR_INIT("text/csv")}, - {"avif", TH_STR_INIT("image/avif")}, - {""}, {""}, {""}, - {"pdf", TH_STR_INIT("application/pdf")}, - {"webm", TH_STR_INIT("video/webm")}, - {""}, {""}, {""}, - {"css", TH_STR_INIT("text/css")}, - {"mpeg", TH_STR_INIT("video/mpeg")}, - {""}, {""}, {""}, - {"aac", TH_STR_INIT("audio/aac")}, - {""}, {""}, {""}, {""}, - {"ttf", TH_STR_INIT("font/ttf")}, - {""}, {""}, {""}, {""}, - {"xml", TH_STR_INIT("application/xml")}, - {""}, - {"xhtml",TH_STR_INIT("application/xhtml+xml")}, - {""}, {""}, - {"txt", TH_STR_INIT("text/plain")}, - {""}, {""}, {""}, {""}, - {"zip", TH_STR_INIT("application/zip")}, - {""}, {""}, {""}, {""}, - {"mp4", TH_STR_INIT("video/mp4")}, - {""}, {""}, {""}, {""}, - {"mp3", TH_STR_INIT("audio/mpeg")} + static th_pollops ops = { + .poll = th_pollops_os_poll, }; -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic pop -#endif + return &ops; +} + +/* th_pollops_os end */ +/* Forward declarations begin */ + +typedef struct th_poll_reactor th_poll_reactor; +typedef struct th_poll_handle th_poll_handle; +typedef struct th_poll_handle_map th_poll_handle_map; + +/* Forward declarations end */ +/* th_poll_fd_to_idx_map begin */ + +TH_INLINE(uint32_t) +th_poll_fd_hash(int fd) +{ + return (uint32_t)fd; +} + +TH_INLINE(bool) +th_poll_fd_eq(int a, int b) +{ + return a == b; +} + +TH_DEFINE_HASHMAP(th_poll_fd_to_idx_map, int, size_t, th_poll_fd_hash, th_poll_fd_eq, -1) + +/* th_poll_fd_to_idx_map end */ +/* th_poll_handle begin */ + +struct th_poll_handle { + th_handle base; + th_timer timer; + th_poll_handle* next; + th_poll_handle* prev; + th_allocator* allocator; + th_poll_reactor* reactor; + th_op* pending[TH_OP_MAX]; + int fd; + bool timeout_enabled; +}; + +TH_DEFINE_POOL_ALLOCATOR(th_poll_handle_pool, th_poll_handle, prev, next) +TH_DEFINE_VEC(th_pollfd_vec, struct pollfd, (void)) + +/* th_poll_handle end */ +/* th_poll_handle_map begin */ + +struct th_poll_handle_map { + th_poll_fd_to_idx_map fd_to_idx_map; + th_allocator* allocator; + th_poll_handle** handles; + size_t size; + size_t capacity; +}; - if (len <= TH_MIME_MAX_WORD_LENGTH && len >= TH_MIME_MIN_WORD_LENGTH) - { - register unsigned int key = th_mime_hash (str, len); +TH_LOCAL(void) +th_poll_handle_map_init(th_poll_handle_map* map, th_allocator* allocator) +{ + th_poll_fd_to_idx_map_init(&map->fd_to_idx_map, allocator); + map->allocator = allocator; + map->handles = NULL; + map->size = 0; + map->capacity = 0; +} - if (key <= TH_MIME_MAX_HASH_VALUE) - { - register const char *s = wordlist[key].name; +TH_LOCAL(void) +th_poll_handle_map_deinit(th_poll_handle_map* map) +{ + th_poll_fd_to_idx_map_deinit(&map->fd_to_idx_map); + th_allocator_free(map->allocator, map->handles); +} - if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') - return &wordlist[key]; +TH_LOCAL(void) +th_poll_handle_map_set(th_poll_handle_map* map, int fd, th_poll_handle* handle) +{ + size_t idx = 0; + th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); + if (iter == NULL) { + if (map->size == map->capacity) { + size_t new_capacity = (map->capacity == 0) ? 16 : map->capacity * 2; + th_poll_handle** new_handles = th_allocator_realloc(map->allocator, map->handles, new_capacity * sizeof(th_poll_handle*)); + if (!new_handles) { + return; + } + map->handles = new_handles; + map->capacity = new_capacity; } + idx = map->size++; + th_poll_fd_to_idx_map_set(&map->fd_to_idx_map, fd, idx); + } else { + idx = iter->value; } - return (struct th_mime_mapping *) 0; + map->handles[idx] = handle; } -#pragma GCC diagnostic pop -/* End of src/th_mime.c */ -/* Start of src/th_method.c */ -/* ANSI-C code produced by gperf version 3.2.1 */ -/* Computed positions: -k'1' */ +TH_LOCAL(th_poll_handle*) +th_poll_handle_map_try_get(th_poll_handle_map* map, int fd) +{ + th_poll_handle* handle = NULL; + th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); + if (iter) { + handle = map->handles[iter->value]; + } + return handle; +} -#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ - && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ - && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ - && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ - && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ - && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ - && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ - && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ - && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ - && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ - && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ - && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ - && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ - && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ - && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ - && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ - && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ - && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ - && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ - && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ - && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ - && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ - && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) -/* The character set is not based on ISO-646. */ -#error "gperf generated tables don't work with this execution character set. Please report a bug to ." -#endif +TH_LOCAL(void) +th_poll_handle_map_remove(th_poll_handle_map* map, int fd) +{ + th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); + TH_ASSERT(iter && "Must not remove a non-existent handle"); + if (iter) { + size_t idx = iter->value; + th_poll_fd_to_idx_map_erase(&map->fd_to_idx_map, iter); + if (idx != map->size - 1) { + th_poll_fd_to_idx_map_iter last = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, map->handles[map->size - 1]->fd); + last->value = idx; + map->handles[idx] = map->handles[map->size - 1]; + } + --map->size; + } +} +/* th_poll_handle_map implementation end */ +/* th_poll_reactor begin */ -#include -#include -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__clang__) -#pragma clang diagnostic ignored "-Wshorten-64-to-32" -#endif -struct th_method_mapping; +struct th_poll_reactor { + th_reactor base; + th_loop* loop; + th_allocator* allocator; + th_clock* clock; + th_pollops* ops; + th_poll_handle_pool handle_allocator; + th_poll_handle_map handles; + th_pollfd_vec fds; +}; -#define TH_METHOD_TOTAL_KEYWORDS 9 -#define TH_METHOD_MIN_WORD_LENGTH 3 -#define TH_METHOD_MAX_WORD_LENGTH 7 -#define TH_METHOD_MIN_HASH_VALUE 3 -#define TH_METHOD_MAX_HASH_VALUE 12 -/* maximum key range = 10, duplicates = 0 */ +/* th_poll_reactor end */ +/* th_poll_handle implementation begin */ -#ifdef __GNUC__ -__inline -#else -#ifdef __cplusplus -inline -#endif -#endif -static unsigned int -th_method_hash (register const char *str, register size_t len) +TH_LOCAL(th_err) +th_poll_handle_submit(void* self, th_op* op) { - static unsigned char asso_values[] = - { - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 5, 0, 13, - 13, 0, 0, 13, 13, 13, 13, 13, 13, 0, - 5, 13, 13, 13, 0, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13 - }; - return len + asso_values[(unsigned char)str[0]]; + th_poll_handle* handle = (th_poll_handle*)self; + th_poll_reactor* reactor = handle->reactor; + TH_ASSERT(handle->pending[op->type] == NULL && "Handle already has a pending op for this op type"); + if (th_op_get_flags(op) & TH_OP_IMMEDIATE) { + th_op_perform(op); + return TH_ERR_OK; + } + handle->pending[op->type] = op; + struct pollfd pfd = {.fd = handle->fd, .events = (op->type == TH_OP_READ) ? POLLIN : POLLOUT}; + if (handle->timeout_enabled) { + th_timer_set(&handle->timer, th_seconds(TH_CONFIG_IO_TIMEOUT)); + } + th_err err = TH_ERR_OK; + if ((err = th_pollfd_vec_push_back(&reactor->fds, pfd)) != TH_ERR_OK) { + handle->pending[op->type] = NULL; + return err; + } + th_loop_increase_task_count(reactor->loop); + return TH_ERR_OK; } -struct th_method_mapping * -th_method_mapping_find (register const char *str, register size_t len) +TH_LOCAL(void) +th_poll_handle_cancel(void* self) { -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif - static struct th_method_mapping wordlist[] = - { - {""}, {""}, {""}, - {"GET", TH_METHOD_GET}, - {"HEAD", TH_METHOD_HEAD}, - {"TRACE", TH_METHOD_TRACE}, - {"DELETE", TH_METHOD_DELETE}, - {"OPTIONS", TH_METHOD_OPTIONS}, - {"PUT", TH_METHOD_PUT}, - {"POST", TH_METHOD_POST}, - {"PATCH", TH_METHOD_PATCH}, - {""}, - {"CONNECT", TH_METHOD_CONNECT} - }; -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic pop -#endif + th_poll_handle* handle = (th_poll_handle*)self; + for (int i = 0; i < TH_OP_MAX; ++i) { + th_op* op = handle->pending[i]; + if (op) { + handle->pending[i] = NULL; + th_op_abort(op, TH_ERR_SYSTEM(TH_ECANCELED)); + th_loop_decrease_task_count(handle->reactor->loop); + } + } +} + +TH_LOCAL(int) +th_poll_handle_get_fd(const void* self) +{ + const th_poll_handle* handle = (const th_poll_handle*)self; + return handle->fd; +} + +TH_LOCAL(void) +th_poll_handle_enable_timeout(void* self, bool enable) +{ + th_poll_handle* handle = (th_poll_handle*)self; + handle->timeout_enabled = enable; +} + +TH_LOCAL(void) +th_poll_handle_destroy(void* self) +{ + th_poll_handle* handle = (th_poll_handle*)self; + th_poll_handle_map_remove(&handle->reactor->handles, handle->fd); + close(handle->fd); + th_allocator_free(handle->allocator, handle); +} + +static const th_handle_methods th_poll_handle_methods = { + .cancel = th_poll_handle_cancel, + .submit = th_poll_handle_submit, + .enable_timeout = th_poll_handle_enable_timeout, + .get_fd = th_poll_handle_get_fd, + .destroy = th_poll_handle_destroy, +}; - if (len <= TH_METHOD_MAX_WORD_LENGTH && len >= TH_METHOD_MIN_WORD_LENGTH) - { - register unsigned int key = th_method_hash (str, len); +TH_LOCAL(void) +th_poll_handle_init(th_poll_handle* handle, th_poll_reactor* reactor, int fd, th_allocator* allocator) +{ + handle->base.methods = &th_poll_handle_methods; + th_timer_init(&handle->timer, reactor->clock); + handle->pending[TH_OP_READ] = NULL; + handle->pending[TH_OP_WRITE] = NULL; + handle->allocator = allocator; + handle->reactor = reactor; + handle->fd = fd; + handle->timeout_enabled = false; +} - if (key <= TH_METHOD_MAX_HASH_VALUE) - { - register const char *s = wordlist[key].name; +/* th_poll_handle implementation end */ +/* th_poll_reactor implementation begin */ - if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') - return &wordlist[key]; - } +TH_LOCAL(th_err) +th_poll_reactor_create_handle(void* self, th_handle** out, int fd) +{ + th_poll_reactor* reactor = (th_poll_reactor*)self; + th_poll_handle* handle = th_poll_handle_pool_alloc(&reactor->handle_allocator, sizeof(th_poll_handle)); + if (!handle) { + return TH_ERR_BAD_ALLOC; } - return (struct th_method_mapping *) 0; + th_poll_handle_init(handle, reactor, fd, &reactor->handle_allocator.base); + th_poll_handle_map_set(&reactor->handles, handle->fd, handle); + *out = (th_handle*)handle; + return TH_ERR_OK; } -#pragma GCC diagnostic pop -/* End of src/th_method.c */ -/* Start of src/th_allocator.c */ - -#include -#include -#include -#include - -typedef struct th_default_allocator { - th_allocator base; -} th_default_allocator; - -TH_LOCAL(void*) -th_default_allocator_alloc(void* self, size_t size) +TH_LOCAL(void) +th_poll_reactor_run(void* self, int timeout_ms) { - (void)self; - void* ptr = malloc(size); - return ptr; + th_poll_reactor* reactor = (th_poll_reactor*)self; + nfds_t nfds = (nfds_t)th_pollfd_vec_size(&reactor->fds); + int ret = reactor->ops->poll(reactor->ops, th_pollfd_vec_begin(&reactor->fds), nfds, timeout_ms); + if (ret == -1) { + TH_LOG_WARN("poll failed: %s", strerror(errno)); + return; + } + + size_t reenqueue = 0; + for (size_t i = 0; i < nfds; ++i) { + struct pollfd* pfd = th_pollfd_vec_at(&reactor->fds, i); + th_poll_handle* handle = th_poll_handle_map_try_get(&reactor->handles, pfd->fd); + if (!handle) // handle was removed + continue; + short revents = pfd->revents; + th_op_type type = (pfd->events & POLLIN) ? TH_OP_READ : TH_OP_WRITE; + th_op* op = handle->pending[type]; + if (revents && op) { + handle->pending[type] = NULL; + th_loop_decrease_task_count(reactor->loop); + if (revents & pfd->events) { + th_op_perform(op); + } else if (revents & POLLHUP) { + th_op_abort(op, TH_ERR_EOF); + } else if (revents & (POLLERR | POLLPRI)) { + th_op_abort(op, TH_ERR_SYSTEM(TH_EIO)); + } else if (revents & POLLNVAL) { + th_op_abort(op, TH_ERR_SYSTEM(TH_EBADF)); + } else { + TH_LOG_ERROR("Unknown poll event: %d", revents); + th_op_abort(op, TH_ERR_UNKNOWN); + } + } else if (op) { // reenqueue + if (handle->timeout_enabled && th_timer_expired(&handle->timer)) { + handle->pending[type] = NULL; + th_loop_decrease_task_count(reactor->loop); + th_op_abort(op, TH_ERR_SYSTEM(TH_ETIMEDOUT)); + } else { + if (reenqueue < i) + *th_pollfd_vec_at(&reactor->fds, reenqueue) = *pfd; + ++reenqueue; + } + } + // handles without a pending op were cancelled, don't reenqueue + } + /* th_op_perform above may have synchronously resubmitted an op, + * pushing a new pollfd past index nfds (the size we polled on). + * Those entries must survive the compaction below, not just the + * ones inside [0, nfds). */ + size_t total = th_pollfd_vec_size(&reactor->fds); + for (size_t i = nfds; i < total; ++i, ++reenqueue) { + if (reenqueue < i) + *th_pollfd_vec_at(&reactor->fds, reenqueue) = *th_pollfd_vec_at(&reactor->fds, i); + } + th_pollfd_vec_resize(&reactor->fds, reenqueue); } -TH_LOCAL(void*) -th_default_allocator_realloc(void* self, void* ptr, size_t size) +TH_LOCAL(void) +th_poll_reactor_deinit(th_poll_reactor* reactor) { - (void)self; - return realloc(ptr, size); + th_poll_handle_map_deinit(&reactor->handles); + th_poll_handle_pool_deinit(&reactor->handle_allocator); + th_pollfd_vec_deinit(&reactor->fds); } TH_LOCAL(void) -th_default_allocator_free(void* self, void* ptr) +th_poll_reactor_destroy(void* self) { - (void)self; - free(ptr); + th_poll_reactor* reactor = (th_poll_reactor*)self; + th_allocator* allocator = reactor->allocator; + th_poll_reactor_deinit(reactor); + th_allocator_free(allocator, reactor); } -static th_default_allocator default_allocator = { - .base = { - .alloc = th_default_allocator_alloc, - .realloc = th_default_allocator_realloc, - .free = th_default_allocator_free, - }, +static const th_reactor_methods th_poll_reactor_methods = { + .run = th_poll_reactor_run, + .create_handle = th_poll_reactor_create_handle, + .destroy = th_poll_reactor_destroy, }; -static th_allocator* user_default_allocator = NULL; - -TH_PUBLIC(th_allocator*) -th_default_allocator_get(void) +TH_LOCAL(void) +th_poll_reactor_init(th_poll_reactor* reactor, th_loop* loop, th_allocator* allocator, th_clock* clock, th_pollops* ops) { - if (user_default_allocator) - return user_default_allocator; - return &default_allocator.base; + reactor->base.methods = &th_poll_reactor_methods; + reactor->loop = loop; + reactor->allocator = allocator; + reactor->clock = clock; + reactor->ops = ops; + th_pollfd_vec_init(&reactor->fds, allocator); + th_poll_handle_map_init(&reactor->handles, allocator); + th_poll_handle_pool_init(&reactor->handle_allocator, allocator, 16, 8 * 1024); } -TH_PUBLIC(void) -th_default_allocator_set(th_allocator* allocator) +TH_PRIVATE(th_err) +th_poll_create(th_reactor** out, th_loop* loop, th_allocator* allocator, th_clock* clock, th_pollops* ops) { - user_default_allocator = allocator; + allocator = allocator ? allocator : th_default_allocator_get(); + th_poll_reactor* reactor = th_allocator_alloc(allocator, sizeof(th_poll_reactor)); + if (!reactor) { + return TH_ERR_BAD_ALLOC; + } + th_poll_reactor_init(reactor, loop, allocator, clock, ops); + *out = &reactor->base; + return TH_ERR_OK; } -/* th_arena_allocator implementation begin */ +/* th_poll_reactor implementation end */ -TH_LOCAL(void*) -th_arena_allocator_alloc(void* self, size_t size) +#endif /* !TH_CONFIG_OS_WIN */ +/* End of src/th_poll.c */ +/* Start of src/th_loop.c */ + +TH_PRIVATE(void) +th_loop_init(th_loop* loop, th_reactor* reactor) { - th_arena_allocator* allocator = self; - if (allocator->pos + size > allocator->size) { - if (!allocator->allocator) - return NULL; - return th_allocator_alloc(allocator->allocator, size); - } - void* ptr = (char*)allocator->buf + allocator->pos; - allocator->prev_pos = allocator->pos; - allocator->pos += (size_t)TH_ALIGNAS(allocator->alignment, size); - return ptr; + loop->reactor = reactor; + loop->queue = th_task_queue_make(); + loop->num_tasks = 0; + th_task_init(&loop->reactor_task, NULL); + th_task_queue_push(&loop->queue, &loop->reactor_task); } -TH_LOCAL(void*) -th_arena_allocator_realloc(void* self, void* ptr, size_t size) +TH_PRIVATE(void) +th_loop_push_task(th_loop* loop, th_task* task) { - th_arena_allocator* allocator = self; - if (ptr == NULL) - return th_arena_allocator_alloc(self, size); - if ((char*)ptr < (char*)allocator->buf || (char*)ptr >= (char*)allocator->buf + allocator->size) - return th_allocator_realloc(allocator->allocator, ptr, size); - if (ptr == (char*)allocator->buf + allocator->prev_pos) { - if (allocator->prev_pos + size > allocator->size) { - if (!allocator->allocator) - return NULL; - void* newp = th_allocator_alloc(allocator->allocator, size); - if (!newp) - return NULL; - memcpy(newp, ptr, allocator->pos - allocator->prev_pos); - allocator->pos = allocator->prev_pos; - return newp; - } - allocator->pos = allocator->prev_pos + size; - return ptr; - } - void* newp = th_allocator_alloc(self, size); - if (!newp) - return NULL; - size_t max_possible = (size_t)(((uint8_t*)allocator->buf + allocator->prev_pos) - (uint8_t*)ptr); - memcpy(newp, ptr, max_possible); - return newp; + ++loop->num_tasks; + th_task_queue_push(&loop->queue, task); } -TH_LOCAL(void) -th_arena_allocator_free(void* self, void* ptr) +TH_PRIVATE(void) +th_loop_push_uncounted_task(th_loop* loop, th_task* task) { - th_arena_allocator* allocator = self; - if ((uint8_t*)ptr == (uint8_t*)allocator->buf + allocator->prev_pos) { - allocator->pos = allocator->prev_pos; - return; - } - if ((char*)ptr < (char*)allocator->buf || (char*)ptr >= (char*)allocator->buf + allocator->size) { - th_allocator_free(allocator->allocator, ptr); - return; - } + th_task_queue_push(&loop->queue, task); } TH_PRIVATE(void) -th_arena_allocator_init_with_alignment(th_arena_allocator* allocator, void* buf, size_t size, size_t alignment, th_allocator* fallback) +th_loop_increase_task_count(th_loop* loop) { - allocator->base.alloc = th_arena_allocator_alloc; - allocator->base.realloc = th_arena_allocator_realloc; - allocator->base.free = th_arena_allocator_free; - allocator->allocator = fallback; - allocator->alignment = (uint16_t)alignment; - void* aligned = TH_ALIGNAS(alignment, buf); - allocator->size = size - (size_t)((uint8_t*)aligned - (uint8_t*)buf); - allocator->buf = aligned; - allocator->pos = 0; - allocator->prev_pos = 0; + ++loop->num_tasks; +} + +TH_PRIVATE(void) +th_loop_decrease_task_count(th_loop* loop) +{ + --loop->num_tasks; } -TH_PRIVATE(void) -th_arena_allocator_init(th_arena_allocator* allocator, void* buf, size_t size, th_allocator* fallback) +TH_PRIVATE(th_err) +th_loop_poll(th_loop* loop, int timeout_ms) { - th_arena_allocator_init_with_alignment(allocator, buf, size, TH_ALIGNOF(th_max_align), fallback); + if (loop->num_tasks == 0) { + return TH_ERR_EOF; + } + while (1) { + th_task* task = th_task_queue_pop(&loop->queue); + TH_ASSERT(task && "Task queue must never be empty"); + bool empty = th_task_queue_empty(&loop->queue); + if (task == &loop->reactor_task) { + th_reactor_run(loop->reactor, empty ? timeout_ms : 0); + th_task_queue_push(&loop->queue, &loop->reactor_task); + if (empty) + return TH_ERR_OK; + } else { + th_task_complete(task); + --loop->num_tasks; + return TH_ERR_OK; + } + } } -/* th_arena_allocator implementation end */ -/* End of src/th_allocator.c */ -/* Start of src/th_task.c */ - -#include -#include - -/* th_task functions begin */ - TH_PRIVATE(void) -th_task_init(th_task* task, void (*fn)(void*)) +th_loop_run(th_loop* loop) { - TH_ASSERT(task); - task->fn = fn; - task->next = NULL; + while (th_loop_poll(loop, 0) == TH_ERR_OK) { + } } TH_PRIVATE(void) -th_task_complete(th_task* task) +th_loop_deinit(th_loop* loop) { - if (task->fn) - task->fn(task); + while (th_task_queue_pop(&loop->queue)) { + } } +/* End of src/th_loop.c */ +/* Start of src/th_error.c */ +#include -/* th_task functions end */ -/* End of src/th_task.c */ -/* Start of src/th_poll.c */ -#if !defined(TH_CONFIG_OS_WIN) +TH_PUBLIC(const char*) +th_strerror(th_err err) +{ + switch (TH_ERR_CATEGORY(err)) { + case TH_ERR_CATEGORY_OTHER: + switch (TH_ERR_CODE(err)) { + case 0: + return "success"; + case TH_ERRC_BAD_ALLOC: + return "out of memory"; + case TH_ERRC_INVALID_ARG: + return "invalid argument"; + case TH_ERRC_EOF: + return "end of file"; + case TH_ERRC_BUSY: + return "busy"; + default: + return "unknown error"; + } + break; + case TH_ERR_CATEGORY_SYSTEM: + return strerror(TH_ERR_CODE(err)); + case TH_ERR_CATEGORY_HTTP: + return th_http_strerror(TH_ERR_CODE(err)); + case TH_ERR_CATEGORY_SSL: +#if TH_WITH_SSL + return th_ssl_strerror(TH_ERR_CODE(err)); +#else + TH_ASSERT(0 && "SSL not enabled"); + return NULL; +#endif + default: + break; + } + return "Unknown error category"; +} +/* End of src/th_error.c */ +/* Start of src/th_socket.c */ +#if defined(TH_CONFIG_OS_POSIX) #include -#include +#include +#include #include +#elif defined(TH_CONFIG_OS_WIN) +#include +#endif -#undef TH_LOG_TAG -#define TH_LOG_TAG "poll" +#if defined(TH_CONFIG_OS_OSX) +#include +#endif -/* th_pollops_os begin */ +#if defined(TH_CONFIG_OS_POSIX) -TH_LOCAL(int) -th_pollops_os_poll(void* self, struct pollfd* fds, nfds_t nfds, int timeout_ms) +TH_LOCAL(th_err) +th_socket_ops_os_send(void* self, int fd, const void* addr, size_t len, size_t* result) { (void)self; - return poll(fds, nfds, timeout_ms); + int flags = 0; +#if defined(MSG_NOSIGNAL) + flags |= MSG_NOSIGNAL; +#endif + ssize_t ret = send(fd, addr, len, flags); + if (ret < 0) + return TH_ERR_SYSTEM(errno); + *result = (size_t)ret; + return TH_ERR_OK; } -TH_PRIVATE(th_pollops*) -th_pollops_os(void) +TH_LOCAL(th_err) +th_socket_ops_os_sendvec(void* self, int fd, const th_iov* iov, size_t iovcnt, size_t* result) { - static th_pollops ops = { - .poll = th_pollops_os_poll, - }; - return &ops; + (void)self; + int flags = 0; +#if defined(MSG_NOSIGNAL) + flags |= MSG_NOSIGNAL; +#endif + struct msghdr msg = {0}; + msg.msg_iov = (struct iovec*)iov; +#if defined(TH_CONFIG_OS_OSX) + TH_ASSERT(iovcnt <= INT_MAX); + msg.msg_iovlen = (int)iovcnt; +#else + msg.msg_iovlen = iovcnt; +#endif + ssize_t ret = sendmsg(fd, &msg, flags); + if (ret < 0) + return TH_ERR_SYSTEM(errno); + *result = (size_t)ret; + return TH_ERR_OK; } -/* th_pollops_os end */ -/* Forward declarations begin */ - -typedef struct th_poll_reactor th_poll_reactor; -typedef struct th_poll_handle th_poll_handle; -typedef struct th_poll_handle_map th_poll_handle_map; - -/* Forward declarations end */ -/* th_poll_fd_to_idx_map begin */ - -TH_INLINE(uint32_t) -th_poll_fd_hash(int fd) +TH_LOCAL(th_err) +th_socket_ops_os_recv(void* self, int fd, void* addr, size_t len, size_t* result) { - return (uint32_t)fd; + (void)self; + ssize_t ret = recv(fd, addr, len, 0); + if (ret < 0) + return TH_ERR_SYSTEM(errno); + if (ret == 0) + return TH_ERR_EOF; + *result = (size_t)ret; + return TH_ERR_OK; } -TH_INLINE(bool) -th_poll_fd_eq(int a, int b) +/* Builds header iov + one trailing iov (extra) into vec, capped at + * TH_SOCKET_SENDFILE_MAX_IOV entries; returns the combined iovec count. */ +#define TH_SOCKET_SENDFILE_MAX_IOV 64 + +TH_LOCAL(size_t) +th_socket_build_sendfile_iov(struct iovec* vec, const th_iov* iov, size_t iovcnt, void* extra_base, size_t extra_len) { - return a == b; + size_t veclen = 0; + for (size_t i = 0; i < iovcnt && veclen < TH_SOCKET_SENDFILE_MAX_IOV - 1; ++i, ++veclen) { + vec[veclen].iov_base = iov[i].base; + vec[veclen].iov_len = iov[i].len; + } + vec[veclen].iov_base = extra_base; + vec[veclen].iov_len = extra_len; + ++veclen; + return veclen; } -TH_DEFINE_HASHMAP(th_poll_fd_to_idx_map, int, size_t, th_poll_fd_hash, th_poll_fd_eq, -1) - -/* th_poll_fd_to_idx_map end */ -/* th_poll_handle begin */ - -struct th_poll_handle { - th_handle base; - th_timer timer; - th_poll_handle* next; - th_poll_handle* prev; - th_allocator* allocator; - th_poll_reactor* reactor; - th_op* pending[TH_OP_MAX]; - int fd; - bool timeout_enabled; -}; - -TH_DEFINE_POOL_ALLOCATOR(th_poll_handle_pool, th_poll_handle, prev, next) -TH_DEFINE_VEC(th_pollfd_vec, struct pollfd, (void)) - -/* th_poll_handle end */ -/* th_poll_handle_map begin */ - -struct th_poll_handle_map { - th_poll_fd_to_idx_map fd_to_idx_map; - th_allocator* allocator; - th_poll_handle** handles; - size_t size; - size_t capacity; -}; +#define TH_SOCKET_SENDFILE_BUFFERED_MAX (8 * 1024) -TH_LOCAL(void) -th_poll_handle_map_init(th_poll_handle_map* map, th_allocator* allocator) +/* Read a chunk of the file into a stack buffer, then send header + + * buffer in one sendmsg. The chunk is capped at + * TH_SOCKET_SENDFILE_BUFFERED_MAX regardless of len - th_sendfile_op + * drives further chunks via its own retry loop. */ +TH_LOCAL(th_err) +th_socket_ops_os_sendfile(void* self, int fd, const th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, size_t* result) { - th_poll_fd_to_idx_map_init(&map->fd_to_idx_map, allocator); - map->allocator = allocator; - map->handles = NULL; - map->size = 0; - map->capacity = 0; -} + (void)self; + uint8_t buffer[TH_SOCKET_SENDFILE_BUFFERED_MAX]; + size_t toread = TH_MIN(sizeof(buffer), len); + ssize_t readlen = pread(file->fd, buffer, toread, (off_t)offset); + if (readlen < 0) + return TH_ERR_SYSTEM(errno); -TH_LOCAL(void) -th_poll_handle_map_deinit(th_poll_handle_map* map) -{ - th_poll_fd_to_idx_map_deinit(&map->fd_to_idx_map); - th_allocator_free(map->allocator, map->handles); + struct iovec vec[TH_SOCKET_SENDFILE_MAX_IOV]; + size_t veclen = th_socket_build_sendfile_iov(vec, iov, iovcnt, buffer, (size_t)readlen); + + int flags = 0; +#if defined(MSG_NOSIGNAL) + flags |= MSG_NOSIGNAL; +#endif + struct msghdr msg = {0}; + msg.msg_iov = vec; +#if defined(TH_CONFIG_OS_OSX) + TH_ASSERT(veclen <= INT_MAX); + msg.msg_iovlen = (int)veclen; +#else + msg.msg_iovlen = veclen; +#endif + ssize_t ret = sendmsg(fd, &msg, flags); + if (ret < 0) + return TH_ERR_SYSTEM(errno); + *result = (size_t)ret; + return TH_ERR_OK; } -TH_LOCAL(void) -th_poll_handle_map_set(th_poll_handle_map* map, int fd, th_poll_handle* handle) +TH_PRIVATE(th_socket_ops*) +th_socket_ops_os(void) { - size_t idx = 0; - th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); - if (iter == NULL) { - if (map->size == map->capacity) { - size_t new_capacity = (map->capacity == 0) ? 16 : map->capacity * 2; - th_poll_handle** new_handles = th_allocator_realloc(map->allocator, map->handles, new_capacity * sizeof(th_poll_handle*)); - if (!new_handles) { - return; - } - map->handles = new_handles; - map->capacity = new_capacity; - } - idx = map->size++; - th_poll_fd_to_idx_map_set(&map->fd_to_idx_map, fd, idx); - } else { - idx = iter->value; - } - map->handles[idx] = handle; + static th_socket_ops ops = { + .send = th_socket_ops_os_send, + .sendvec = th_socket_ops_os_sendvec, + .recv = th_socket_ops_os_recv, + .sendfile = th_socket_ops_os_sendfile, + }; + return &ops; } -TH_LOCAL(th_poll_handle*) -th_poll_handle_map_try_get(th_poll_handle_map* map, int fd) -{ - th_poll_handle* handle = NULL; - th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); - if (iter) { - handle = map->handles[iter->value]; - } - return handle; -} +#endif /* TH_CONFIG_OS_POSIX */ -TH_LOCAL(void) -th_poll_handle_map_remove(th_poll_handle_map* map, int fd) +TH_PRIVATE(void) +th_socket_init(th_socket* socket, th_loop* loop, th_socket_ops* ops) { - th_poll_fd_to_idx_map_iter iter = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, fd); - TH_ASSERT(iter && "Must not remove a non-existent handle"); - if (iter) { - size_t idx = iter->value; - th_poll_fd_to_idx_map_erase(&map->fd_to_idx_map, iter); - if (idx != map->size - 1) { - th_poll_fd_to_idx_map_iter last = th_poll_fd_to_idx_map_find(&map->fd_to_idx_map, map->handles[map->size - 1]->fd); - last->value = idx; - map->handles[idx] = map->handles[map->size - 1]; - } - --map->size; - } + socket->loop = loop; + socket->handle = NULL; + socket->ops = ops; } -/* th_poll_handle_map implementation end */ -/* th_poll_reactor begin */ - -struct th_poll_reactor { - th_reactor base; - th_loop* loop; - th_allocator* allocator; - th_clock* clock; - th_pollops* ops; - th_poll_handle_pool handle_allocator; - th_poll_handle_map handles; - th_pollfd_vec fds; -}; - -/* th_poll_reactor end */ -/* th_poll_handle implementation begin */ - -TH_LOCAL(th_err) -th_poll_handle_submit(void* self, th_op* op) +TH_PRIVATE(th_err) +th_socket_set_fd(th_socket* socket, int fd) { - th_poll_handle* handle = (th_poll_handle*)self; - th_poll_reactor* reactor = handle->reactor; - TH_ASSERT(handle->pending[op->type] == NULL && "Handle already has a pending op for this op type"); - if (th_op_get_flags(op) & TH_OP_IMMEDIATE) { - th_op_perform(op); - return TH_ERR_OK; - } - handle->pending[op->type] = op; - struct pollfd pfd = {.fd = handle->fd, .events = (op->type == TH_OP_READ) ? POLLIN : POLLOUT}; - if (handle->timeout_enabled) { - th_timer_set(&handle->timer, th_seconds(TH_CONFIG_IO_TIMEOUT)); - } - th_err err = TH_ERR_OK; - if ((err = th_pollfd_vec_push_back(&reactor->fds, pfd)) != TH_ERR_OK) { - handle->pending[op->type] = NULL; + th_socket_close(socket); + th_err err = th_reactor_create_handle(socket->loop->reactor, &socket->handle, fd); + if (err != TH_ERR_OK) return err; - } - th_loop_increase_task_count(reactor->loop); + th_handle_enable_timeout(socket->handle, true); return TH_ERR_OK; } -TH_LOCAL(void) -th_poll_handle_cancel(void* self) +TH_PRIVATE(void) +th_socket_close(th_socket* socket) { - th_poll_handle* handle = (th_poll_handle*)self; - for (int i = 0; i < TH_OP_MAX; ++i) { - th_op* op = handle->pending[i]; - if (op) { - handle->pending[i] = NULL; - th_op_abort(op, TH_ERR_SYSTEM(TH_ECANCELED)); - th_loop_decrease_task_count(handle->reactor->loop); - } + if (socket->handle) { + th_handle_destroy(socket->handle); + socket->handle = NULL; } } -TH_LOCAL(int) -th_poll_handle_get_fd(const void* self) +TH_PRIVATE(void) +th_socket_deinit(th_socket* socket) { - const th_poll_handle* handle = (const th_poll_handle*)self; - return handle->fd; + th_socket_close(socket); } +/* End of src/th_socket.c */ +/* Start of src/th_recv.c */ -TH_LOCAL(void) -th_poll_handle_enable_timeout(void* self, bool enable) +TH_LOCAL(bool) +th_recv_op_is_retryable(th_err err) { - th_poll_handle* handle = (th_poll_handle*)self; - handle->timeout_enabled = enable; + return err == TH_ERR_SYSTEM(TH_EAGAIN) + || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); } TH_LOCAL(void) -th_poll_handle_destroy(void* self) +th_recv_op_finalize(th_recv_op* op) { - th_poll_handle* handle = (th_poll_handle*)self; - th_poll_handle_map_remove(&handle->reactor->handles, handle->fd); - close(handle->fd); - th_allocator_free(handle->allocator, handle); + op->callback(op->user_data, op->pos, op->err); } -static const th_handle_methods th_poll_handle_methods = { - .cancel = th_poll_handle_cancel, - .submit = th_poll_handle_submit, - .enable_timeout = th_poll_handle_enable_timeout, - .get_fd = th_poll_handle_get_fd, - .destroy = th_poll_handle_destroy, -}; - TH_LOCAL(void) -th_poll_handle_init(th_poll_handle* handle, th_poll_reactor* reactor, int fd, th_allocator* allocator) +th_recv_op_complete(th_recv_op* op, th_err err) { - handle->base.methods = &th_poll_handle_methods; - th_timer_init(&handle->timer, reactor->clock); - handle->pending[TH_OP_READ] = NULL; - handle->pending[TH_OP_WRITE] = NULL; - handle->allocator = allocator; - handle->reactor = reactor; - handle->fd = fd; - handle->timeout_enabled = false; + op->err = err; + th_op_set_flags(&op->base, TH_OP_COMPLETED); + th_socket_post(op->socket, &op->base.base); } -/* th_poll_handle implementation end */ -/* th_poll_reactor implementation begin */ - TH_LOCAL(th_err) -th_poll_reactor_create_handle(void* self, th_handle** out, int fd) +th_recv_op_perform(th_recv_op* op) { - th_poll_reactor* reactor = (th_poll_reactor*)self; - th_poll_handle* handle = th_poll_handle_pool_alloc(&reactor->handle_allocator, sizeof(th_poll_handle)); - if (!handle) { - return TH_ERR_BAD_ALLOC; - } - th_poll_handle_init(handle, reactor, fd, &reactor->handle_allocator.base); - th_poll_handle_map_set(&reactor->handles, handle->fd, handle); - *out = (th_handle*)handle; - return TH_ERR_OK; + th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); + size_t result = 0; + th_err err = th_socket_recv(op->socket, (char*)op->addr + op->pos, op->len - op->pos, &result); + if (err != TH_ERR_OK) + return err; + op->pos += result; + if (!op->exact || op->pos == op->len) + return TH_ERR_OK; + return TH_ERR_SYSTEM(TH_EAGAIN); } TH_LOCAL(void) -th_poll_reactor_run(void* self, int timeout_ms) +th_recv_op_fn(void* self) { - th_poll_reactor* reactor = (th_poll_reactor*)self; - nfds_t nfds = (nfds_t)th_pollfd_vec_size(&reactor->fds); - int ret = reactor->ops->poll(reactor->ops, th_pollfd_vec_begin(&reactor->fds), nfds, timeout_ms); - if (ret == -1) { - TH_LOG_WARN("poll failed: %s", strerror(errno)); + th_recv_op* op = self; + if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { + th_recv_op_finalize(op); return; } - - size_t reenqueue = 0; - for (size_t i = 0; i < nfds; ++i) { - struct pollfd* pfd = th_pollfd_vec_at(&reactor->fds, i); - th_poll_handle* handle = th_poll_handle_map_try_get(&reactor->handles, pfd->fd); - if (!handle) // handle was removed - continue; - short revents = pfd->revents; - th_op_type type = (pfd->events & POLLIN) ? TH_OP_READ : TH_OP_WRITE; - th_op* op = handle->pending[type]; - if (revents && op) { - handle->pending[type] = NULL; - th_loop_decrease_task_count(reactor->loop); - if (revents & pfd->events) { - th_op_perform(op); - } else if (revents & POLLHUP) { - th_op_abort(op, TH_ERR_EOF); - } else if (revents & (POLLERR | POLLPRI)) { - th_op_abort(op, TH_ERR_SYSTEM(TH_EIO)); - } else if (revents & POLLNVAL) { - th_op_abort(op, TH_ERR_SYSTEM(TH_EBADF)); - } else { - TH_LOG_ERROR("Unknown poll event: %d", revents); - th_op_abort(op, TH_ERR_UNKNOWN); - } - } else if (op) { // reenqueue - if (handle->timeout_enabled && th_timer_expired(&handle->timer)) { - handle->pending[type] = NULL; - th_loop_decrease_task_count(reactor->loop); - th_op_abort(op, TH_ERR_SYSTEM(TH_ETIMEDOUT)); - } else { - if (reenqueue < i) - *th_pollfd_vec_at(&reactor->fds, reenqueue) = *pfd; - ++reenqueue; - } - } - // handles without a pending op were cancelled, don't reenqueue - } - /* th_op_perform above may have synchronously resubmitted an op, - * pushing a new pollfd past index nfds (the size we polled on). - * Those entries must survive the compaction below, not just the - * ones inside [0, nfds). */ - size_t total = th_pollfd_vec_size(&reactor->fds); - for (size_t i = nfds; i < total; ++i, ++reenqueue) { - if (reenqueue < i) - *th_pollfd_vec_at(&reactor->fds, reenqueue) = *th_pollfd_vec_at(&reactor->fds, i); + th_err err = th_recv_op_perform(op); + if (th_recv_op_is_retryable(err)) { + err = th_socket_submit(op->socket, &op->base); + if (err == TH_ERR_OK) + return; } - th_pollfd_vec_resize(&reactor->fds, reenqueue); + th_recv_op_complete(op, err); +} + +TH_LOCAL(void) +th_recv_op_abort(void* self, th_err err) +{ + th_recv_op_complete(self, err); } -TH_LOCAL(void) -th_poll_reactor_deinit(th_poll_reactor* reactor) +TH_PRIVATE(void) +th_recv_op_init(th_recv_op* op, th_socket* socket, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data) { - th_poll_handle_map_deinit(&reactor->handles); - th_poll_handle_pool_deinit(&reactor->handle_allocator); - th_pollfd_vec_deinit(&reactor->fds); + th_op_init(&op->base, TH_OP_READ, th_recv_op_fn, th_recv_op_abort); + op->socket = socket; + op->addr = addr; + op->len = len; + op->pos = 0; + op->exact = exact; + op->callback = callback; + op->user_data = user_data; + op->err = TH_ERR_OK; } +/* End of src/th_recv.c */ +/* Start of src/th_send.c */ -TH_LOCAL(void) -th_poll_reactor_destroy(void* self) +TH_LOCAL(bool) +th_send_op_is_retryable(th_err err) { - th_poll_reactor* reactor = (th_poll_reactor*)self; - th_allocator* allocator = reactor->allocator; - th_poll_reactor_deinit(reactor); - th_allocator_free(allocator, reactor); + return err == TH_ERR_SYSTEM(TH_EAGAIN) + || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); } -static const th_reactor_methods th_poll_reactor_methods = { - .run = th_poll_reactor_run, - .create_handle = th_poll_reactor_create_handle, - .destroy = th_poll_reactor_destroy, -}; - TH_LOCAL(void) -th_poll_reactor_init(th_poll_reactor* reactor, th_loop* loop, th_allocator* allocator, th_clock* clock, th_pollops* ops) +th_send_op_finalize(th_send_op* op) { - reactor->base.methods = &th_poll_reactor_methods; - reactor->loop = loop; - reactor->allocator = allocator; - reactor->clock = clock; - reactor->ops = ops; - th_pollfd_vec_init(&reactor->fds, allocator); - th_poll_handle_map_init(&reactor->handles, allocator); - th_poll_handle_pool_init(&reactor->handle_allocator, allocator, 16, 8 * 1024); + op->callback(op->user_data, op->pos, op->err); } -TH_PRIVATE(th_err) -th_poll_create(th_reactor** out, th_loop* loop, th_allocator* allocator, th_clock* clock, th_pollops* ops) +TH_LOCAL(void) +th_send_op_complete(th_send_op* op, th_err err) { - allocator = allocator ? allocator : th_default_allocator_get(); - th_poll_reactor* reactor = th_allocator_alloc(allocator, sizeof(th_poll_reactor)); - if (!reactor) { - return TH_ERR_BAD_ALLOC; - } - th_poll_reactor_init(reactor, loop, allocator, clock, ops); - *out = &reactor->base; - return TH_ERR_OK; + op->err = err; + th_op_set_flags(&op->base, TH_OP_COMPLETED); + th_socket_post(op->socket, &op->base.base); } -/* th_poll_reactor implementation end */ - -#endif /* !TH_CONFIG_OS_WIN */ -/* End of src/th_poll.c */ -/* Start of src/th_loop.c */ - -TH_PRIVATE(void) -th_loop_init(th_loop* loop, th_reactor* reactor) +TH_LOCAL(th_err) +th_send_op_perform(th_send_op* op) { - loop->reactor = reactor; - loop->queue = th_task_queue_make(); - loop->num_tasks = 0; - th_task_init(&loop->reactor_task, NULL); - th_task_queue_push(&loop->queue, &loop->reactor_task); + th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); + size_t result = 0; + th_err err = th_socket_send(op->socket, (const char*)op->addr + op->pos, op->len - op->pos, &result); + if (err != TH_ERR_OK) + return err; + op->pos += result; + if (op->pos == op->len) + return TH_ERR_OK; + return TH_ERR_SYSTEM(TH_EAGAIN); } -TH_PRIVATE(void) -th_loop_push_task(th_loop* loop, th_task* task) +TH_LOCAL(void) +th_send_op_fn(void* self) { - ++loop->num_tasks; - th_task_queue_push(&loop->queue, task); + th_send_op* op = self; + if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { + th_send_op_finalize(op); + return; + } + th_err err = th_send_op_perform(op); + if (th_send_op_is_retryable(err)) { + err = th_socket_submit(op->socket, &op->base); + if (err == TH_ERR_OK) + return; + } + th_send_op_complete(op, err); } -TH_PRIVATE(void) -th_loop_push_uncounted_task(th_loop* loop, th_task* task) +TH_LOCAL(void) +th_send_op_abort(void* self, th_err err) { - th_task_queue_push(&loop->queue, task); + th_send_op_complete(self, err); } TH_PRIVATE(void) -th_loop_increase_task_count(th_loop* loop) +th_send_op_init(th_send_op* op, th_socket* socket, const void* addr, size_t len, th_send_cb callback, void* user_data) { - ++loop->num_tasks; + th_op_init(&op->base, TH_OP_WRITE, th_send_op_fn, th_send_op_abort); + op->socket = socket; + op->addr = addr; + op->len = len; + op->pos = 0; + op->callback = callback; + op->user_data = user_data; + op->err = TH_ERR_OK; } +/* End of src/th_send.c */ +/* Start of src/th_sendvec.c */ -TH_PRIVATE(void) -th_loop_decrease_task_count(th_loop* loop) +TH_LOCAL(bool) +th_sendvec_op_is_retryable(th_err err) { - --loop->num_tasks; + return err == TH_ERR_SYSTEM(TH_EAGAIN) + || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); } -TH_PRIVATE(th_err) -th_loop_poll(th_loop* loop, int timeout_ms) +TH_LOCAL(void) +th_sendvec_op_finalize(th_sendvec_op* op) { - if (loop->num_tasks == 0) { - return TH_ERR_EOF; - } - while (1) { - th_task* task = th_task_queue_pop(&loop->queue); - TH_ASSERT(task && "Task queue must never be empty"); - bool empty = th_task_queue_empty(&loop->queue); - if (task == &loop->reactor_task) { - th_reactor_run(loop->reactor, empty ? timeout_ms : 0); - th_task_queue_push(&loop->queue, &loop->reactor_task); - if (empty) - return TH_ERR_OK; - } else { - th_task_complete(task); - --loop->num_tasks; - return TH_ERR_OK; - } - } + op->callback(op->user_data, op->pos, op->err); } -TH_PRIVATE(void) -th_loop_run(th_loop* loop) +TH_LOCAL(void) +th_sendvec_op_complete(th_sendvec_op* op, th_err err) { - while (th_loop_poll(loop, 0) == TH_ERR_OK) { - } + op->err = err; + th_op_set_flags(&op->base, TH_OP_COMPLETED); + th_socket_post(op->socket, &op->base.base); } -TH_PRIVATE(void) -th_loop_deinit(th_loop* loop) +TH_LOCAL(th_err) +th_sendvec_op_perform(th_sendvec_op* op) { - while (th_task_queue_pop(&loop->queue)) { - } + th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); + size_t result = 0; + th_err err = th_socket_sendvec(op->socket, op->iov, op->iovcnt, &result); + if (err != TH_ERR_OK) + return err; + op->pos += result; + th_iov_consume(&op->iov, &op->iovcnt, result); + if (op->iovcnt == 0) + return TH_ERR_OK; + return TH_ERR_SYSTEM(TH_EAGAIN); } -/* End of src/th_loop.c */ -/* Start of src/th_error.c */ -#include - -TH_PUBLIC(const char*) -th_strerror(th_err err) +TH_LOCAL(void) +th_sendvec_op_fn(void* self) { - switch (TH_ERR_CATEGORY(err)) { - case TH_ERR_CATEGORY_OTHER: - switch (TH_ERR_CODE(err)) { - case 0: - return "success"; - case TH_ERRC_BAD_ALLOC: - return "out of memory"; - case TH_ERRC_INVALID_ARG: - return "invalid argument"; - case TH_ERRC_EOF: - return "end of file"; - default: - return "unknown error"; - } - break; - case TH_ERR_CATEGORY_SYSTEM: - return strerror(TH_ERR_CODE(err)); - case TH_ERR_CATEGORY_HTTP: - return th_http_strerror(TH_ERR_CODE(err)); - case TH_ERR_CATEGORY_SSL: -#if TH_WITH_SSL - return th_ssl_strerror(TH_ERR_CODE(err)); -#else - TH_ASSERT(0 && "SSL not enabled"); - return NULL; -#endif - default: - break; + th_sendvec_op* op = self; + if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { + th_sendvec_op_finalize(op); + return; } - return "Unknown error category"; + th_err err = th_sendvec_op_perform(op); + if (th_sendvec_op_is_retryable(err)) { + err = th_socket_submit(op->socket, &op->base); + if (err == TH_ERR_OK) + return; + } + th_sendvec_op_complete(op, err); } -/* End of src/th_error.c */ -/* Start of src/th_socket.c */ - -#if defined(TH_CONFIG_OS_POSIX) -#include -#include -#include -#include -#elif defined(TH_CONFIG_OS_WIN) -#include -#endif - -#if defined(TH_CONFIG_OS_OSX) -#include -#endif -#if defined(TH_CONFIG_OS_POSIX) - -TH_LOCAL(th_err) -th_socket_ops_os_send(void* self, int fd, const void* addr, size_t len, size_t* result) +TH_LOCAL(void) +th_sendvec_op_abort(void* self, th_err err) { - (void)self; - int flags = 0; -#if defined(MSG_NOSIGNAL) - flags |= MSG_NOSIGNAL; -#endif - ssize_t ret = send(fd, addr, len, flags); - if (ret < 0) - return TH_ERR_SYSTEM(errno); - *result = (size_t)ret; - return TH_ERR_OK; + th_sendvec_op_complete(self, err); } -TH_LOCAL(th_err) -th_socket_ops_os_sendvec(void* self, int fd, const th_iov* iov, size_t iovcnt, size_t* result) +TH_PRIVATE(void) +th_sendvec_op_init(th_sendvec_op* op, th_socket* socket, th_iov* iov, size_t iovcnt, th_send_cb callback, void* user_data) { - (void)self; - int flags = 0; -#if defined(MSG_NOSIGNAL) - flags |= MSG_NOSIGNAL; -#endif - struct msghdr msg = {0}; - msg.msg_iov = (struct iovec*)iov; -#if defined(TH_CONFIG_OS_OSX) - TH_ASSERT(iovcnt <= INT_MAX); - msg.msg_iovlen = (int)iovcnt; -#else - msg.msg_iovlen = iovcnt; -#endif - ssize_t ret = sendmsg(fd, &msg, flags); - if (ret < 0) - return TH_ERR_SYSTEM(errno); - *result = (size_t)ret; - return TH_ERR_OK; + th_op_init(&op->base, TH_OP_WRITE, th_sendvec_op_fn, th_sendvec_op_abort); + op->socket = socket; + op->iov = iov; + op->iovcnt = iovcnt; + op->pos = 0; + op->callback = callback; + op->user_data = user_data; + op->err = TH_ERR_OK; } +/* End of src/th_sendvec.c */ +/* Start of src/th_sendfile.c */ -TH_LOCAL(th_err) -th_socket_ops_os_recv(void* self, int fd, void* addr, size_t len, size_t* result) +TH_LOCAL(bool) +th_sendfile_op_is_retryable(th_err err) { - (void)self; - ssize_t ret = recv(fd, addr, len, 0); - if (ret < 0) - return TH_ERR_SYSTEM(errno); - if (ret == 0) - return TH_ERR_EOF; - *result = (size_t)ret; - return TH_ERR_OK; + return err == TH_ERR_SYSTEM(TH_EAGAIN) + || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); } -/* Builds header iov + one trailing iov (extra) into vec, capped at - * TH_SOCKET_SENDFILE_MAX_IOV entries; returns the combined iovec count. */ -#define TH_SOCKET_SENDFILE_MAX_IOV 64 - -TH_LOCAL(size_t) -th_socket_build_sendfile_iov(struct iovec* vec, const th_iov* iov, size_t iovcnt, void* extra_base, size_t extra_len) +TH_LOCAL(void) +th_sendfile_op_finalize(th_sendfile_op* op) { - size_t veclen = 0; - for (size_t i = 0; i < iovcnt && veclen < TH_SOCKET_SENDFILE_MAX_IOV - 1; ++i, ++veclen) { - vec[veclen].iov_base = iov[i].base; - vec[veclen].iov_len = iov[i].len; - } - vec[veclen].iov_base = extra_base; - vec[veclen].iov_len = extra_len; - ++veclen; - return veclen; + op->callback(op->user_data, op->pos, op->err); } -#define TH_SOCKET_SENDFILE_BUFFERED_MAX (8 * 1024) +TH_LOCAL(void) +th_sendfile_op_complete(th_sendfile_op* op, th_err err) +{ + op->err = err; + th_op_set_flags(&op->base, TH_OP_COMPLETED); + th_socket_post(op->socket, &op->base.base); +} -/* Read a chunk of the file into a stack buffer, then send header + - * buffer in one sendmsg. The chunk is capped at - * TH_SOCKET_SENDFILE_BUFFERED_MAX regardless of len - th_sendfile_op - * drives further chunks via its own retry loop. */ TH_LOCAL(th_err) -th_socket_ops_os_sendfile(void* self, int fd, const th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, size_t* result) +th_sendfile_op_perform(th_sendfile_op* op) { - (void)self; - uint8_t buffer[TH_SOCKET_SENDFILE_BUFFERED_MAX]; - size_t toread = TH_MIN(sizeof(buffer), len); - ssize_t readlen = pread(file->fd, buffer, toread, (off_t)offset); - if (readlen < 0) - return TH_ERR_SYSTEM(errno); + th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); + size_t file_pos = op->pos > op->header_len ? op->pos - op->header_len : 0; + size_t remaining = op->len - file_pos; + size_t chunk = TH_MIN(remaining, TH_CONFIG_SENDFILE_CHUNK_LEN); - struct iovec vec[TH_SOCKET_SENDFILE_MAX_IOV]; - size_t veclen = th_socket_build_sendfile_iov(vec, iov, iovcnt, buffer, (size_t)readlen); + size_t result = 0; + th_err err = th_socket_sendfile(op->socket, op->iov, op->iovcnt, op->file, op->offset + file_pos, chunk, &result); + if (err != TH_ERR_OK) + return err; - int flags = 0; -#if defined(MSG_NOSIGNAL) - flags |= MSG_NOSIGNAL; -#endif - struct msghdr msg = {0}; - msg.msg_iov = vec; -#if defined(TH_CONFIG_OS_OSX) - TH_ASSERT(veclen <= INT_MAX); - msg.msg_iovlen = (int)veclen; -#else - msg.msg_iovlen = veclen; -#endif - ssize_t ret = sendmsg(fd, &msg, flags); - if (ret < 0) - return TH_ERR_SYSTEM(errno); - *result = (size_t)ret; - return TH_ERR_OK; + op->pos += result; + th_iov_consume(&op->iov, &op->iovcnt, result); + if (op->pos == op->header_len + op->len) + return TH_ERR_OK; + return TH_ERR_SYSTEM(TH_EAGAIN); } -TH_PRIVATE(th_socket_ops*) -th_socket_ops_os(void) +TH_LOCAL(void) +th_sendfile_op_fn(void* self) { - static th_socket_ops ops = { - .send = th_socket_ops_os_send, - .sendvec = th_socket_ops_os_sendvec, - .recv = th_socket_ops_os_recv, - .sendfile = th_socket_ops_os_sendfile, - }; - return &ops; + th_sendfile_op* op = self; + if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { + th_sendfile_op_finalize(op); + return; + } + th_err err = th_sendfile_op_perform(op); + if (th_sendfile_op_is_retryable(err)) { + err = th_socket_submit(op->socket, &op->base); + if (err == TH_ERR_OK) + return; + } + th_sendfile_op_complete(op, err); } -#endif /* TH_CONFIG_OS_POSIX */ +TH_LOCAL(void) +th_sendfile_op_abort(void* self, th_err err) +{ + th_sendfile_op_complete(self, err); +} TH_PRIVATE(void) -th_socket_init(th_socket* socket, th_loop* loop, th_socket_ops* ops) +th_sendfile_op_init(th_sendfile_op* op, th_socket* socket, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) { - socket->loop = loop; - socket->handle = NULL; - socket->ops = ops; + th_op_init(&op->base, TH_OP_WRITE, th_sendfile_op_fn, th_sendfile_op_abort); + op->socket = socket; + op->iov = iov; + op->iovcnt = iovcnt; + op->file = file; + op->offset = offset; + op->len = len; + op->header_len = th_iov_bytes(iov, iovcnt); + op->pos = 0; + op->callback = callback; + op->user_data = user_data; + op->err = TH_ERR_OK; } +/* End of src/th_sendfile.c */ +/* Start of src/th_acceptor.c */ -TH_PRIVATE(th_err) -th_socket_set_fd(th_socket* socket, int fd) + +#if defined(TH_CONFIG_OS_POSIX) +#include +#include +#include +#include +#include +#include +#include +#include + +TH_LOCAL(th_err) +th_acceptor_ops_os_set_nonblocking(int fd) { - th_socket_close(socket); - th_err err = th_reactor_create_handle(socket->loop->reactor, &socket->handle, fd); - if (err != TH_ERR_OK) - return err; - th_handle_enable_timeout(socket->handle, true); + if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) < 0) + return TH_ERR_SYSTEM(errno); return TH_ERR_OK; } -TH_PRIVATE(void) -th_socket_close(th_socket* socket) +TH_LOCAL(th_err) +th_acceptor_ops_os_open(void* self, const char* addr, const char* port, int* out_fd) { - if (socket->handle) { - th_handle_destroy(socket->handle); - socket->handle = NULL; - } -} + (void)self; + struct addrinfo hints = {0}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + struct addrinfo* res = NULL; + if (getaddrinfo(addr, port, &hints, &res) != 0) + return TH_ERR_SYSTEM(errno); -TH_PRIVATE(void) -th_socket_deinit(th_socket* socket) -{ - th_socket_close(socket); + th_err err = TH_ERR_OK; + int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (fd < 0) { + err = TH_ERR_SYSTEM(errno); + goto cleanup_addrinfo; + } +#if TH_CONFIG_REUSE_ADDR + { + int optval = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) { + err = TH_ERR_SYSTEM(errno); + goto cleanup_fd; + } + } +#endif +#if TH_CONFIG_REUSE_PORT + { +#if defined(SO_REUSEPORT) + int optval = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0) { + err = TH_ERR_SYSTEM(errno); + goto cleanup_fd; + } +#else + TH_LOG_FATAL("SO_REUSEPORT is not supported on this platform"); + err = TH_ERR_NOSUPPORT; + goto cleanup_fd; +#endif + } +#endif + if ((err = th_acceptor_ops_os_set_nonblocking(fd)) != TH_ERR_OK) + goto cleanup_fd; + if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) { + err = TH_ERR_SYSTEM(errno); + goto cleanup_fd; + } + if (listen(fd, 1024) < 0) { + err = TH_ERR_SYSTEM(errno); + goto cleanup_fd; + } + freeaddrinfo(res); + *out_fd = fd; + return TH_ERR_OK; +cleanup_fd: + close(fd); +cleanup_addrinfo: + freeaddrinfo(res); + return err; } -/* End of src/th_socket.c */ -/* Start of src/th_recv.c */ -TH_LOCAL(bool) -th_recv_op_is_retryable(th_err err) +TH_LOCAL(th_err) +th_acceptor_ops_os_accept(void* self, int fd, th_address* addr, int* out_fd) { - return err == TH_ERR_SYSTEM(TH_EAGAIN) - || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); + (void)self; + int conn_fd = accept(fd, (struct sockaddr*)&addr->addr, &addr->addrlen); + if (conn_fd < 0) + return TH_ERR_SYSTEM(errno); + th_err err = th_acceptor_ops_os_set_nonblocking(conn_fd); + if (err != TH_ERR_OK) { + close(conn_fd); + return err; + } + *out_fd = conn_fd; + return TH_ERR_OK; } -TH_LOCAL(void) -th_recv_op_finalize(th_recv_op* op) +TH_PRIVATE(th_acceptor_ops*) +th_acceptor_ops_os(void) { - op->callback(op->user_data, op->pos, op->err); + static th_acceptor_ops ops = { + .open = th_acceptor_ops_os_open, + .accept = th_acceptor_ops_os_accept, + }; + return &ops; } -TH_LOCAL(void) -th_recv_op_complete(th_recv_op* op, th_err err) +#endif /* TH_CONFIG_OS_POSIX */ + +TH_PRIVATE(void) +th_acceptor_init(th_acceptor* acceptor, th_loop* loop, th_acceptor_ops* ops) { - op->err = err; - th_op_set_flags(&op->base, TH_OP_COMPLETED); - th_socket_post(op->socket, &op->base.base); + acceptor->loop = loop; + acceptor->handle = NULL; + acceptor->ops = ops; } -TH_LOCAL(th_err) -th_recv_op_perform(th_recv_op* op) +TH_PRIVATE(th_err) +th_acceptor_open(th_acceptor* acceptor, const char* addr, const char* port) { - th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); - size_t result = 0; - th_err err = th_socket_recv(op->socket, (char*)op->addr + op->pos, op->len - op->pos, &result); + int fd = -1; + th_err err = acceptor->ops->open(acceptor->ops, addr, port, &fd); if (err != TH_ERR_OK) return err; - op->pos += result; - if (!op->exact || op->pos == op->len) - return TH_ERR_OK; - return TH_ERR_SYSTEM(TH_EAGAIN); + th_acceptor_close(acceptor); + err = th_reactor_create_handle(acceptor->loop->reactor, &acceptor->handle, fd); + if (err != TH_ERR_OK) { +#if defined(TH_CONFIG_OS_POSIX) + close(fd); +#endif + return err; + } + th_handle_enable_timeout(acceptor->handle, false); + return TH_ERR_OK; } -TH_LOCAL(void) -th_recv_op_fn(void* self) +TH_PRIVATE(void) +th_acceptor_close(th_acceptor* acceptor) { - th_recv_op* op = self; - if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { - th_recv_op_finalize(op); - return; - } - th_err err = th_recv_op_perform(op); - if (th_recv_op_is_retryable(err)) { - err = th_socket_submit(op->socket, &op->base); - if (err == TH_ERR_OK) - return; + if (acceptor->handle) { + th_handle_destroy(acceptor->handle); + acceptor->handle = NULL; } - th_recv_op_complete(op, err); } -TH_LOCAL(void) -th_recv_op_abort(void* self, th_err err) +TH_PRIVATE(void) +th_acceptor_deinit(th_acceptor* acceptor) { - th_recv_op_complete(self, err); + th_acceptor_close(acceptor); } -TH_PRIVATE(void) -th_recv_op_init(th_recv_op* op, th_socket* socket, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data) +TH_PRIVATE(th_err) +th_acceptor_accept(th_acceptor* acceptor, th_address* addr, th_socket* out_socket) { - th_op_init(&op->base, TH_OP_READ, th_recv_op_fn, th_recv_op_abort); - op->socket = socket; - op->addr = addr; - op->len = len; - op->pos = 0; - op->exact = exact; - op->callback = callback; - op->user_data = user_data; - op->err = TH_ERR_OK; + int fd = -1; + th_err err = acceptor->ops->accept(acceptor->ops, th_acceptor_get_fd(acceptor), addr, &fd); + if (err != TH_ERR_OK) + return err; + return th_socket_set_fd(out_socket, fd); } -/* End of src/th_recv.c */ -/* Start of src/th_send.c */ +/* End of src/th_acceptor.c */ +/* Start of src/th_accept.c */ TH_LOCAL(bool) -th_send_op_is_retryable(th_err err) +th_accept_op_is_retryable(th_err err) { return err == TH_ERR_SYSTEM(TH_EAGAIN) || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); } TH_LOCAL(void) -th_send_op_finalize(th_send_op* op) +th_accept_op_finalize(th_accept_op* op) { - op->callback(op->user_data, op->pos, op->err); + op->callback(op->user_data, op->err); } TH_LOCAL(void) -th_send_op_complete(th_send_op* op, th_err err) +th_accept_op_complete(th_accept_op* op, th_err err) { op->err = err; th_op_set_flags(&op->base, TH_OP_COMPLETED); - th_socket_post(op->socket, &op->base.base); + th_acceptor_post(op->acceptor, &op->base.base); } TH_LOCAL(th_err) -th_send_op_perform(th_send_op* op) +th_accept_op_perform(th_accept_op* op) { th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); - size_t result = 0; - th_err err = th_socket_send(op->socket, (const char*)op->addr + op->pos, op->len - op->pos, &result); - if (err != TH_ERR_OK) - return err; - op->pos += result; - if (op->pos == op->len) - return TH_ERR_OK; - return TH_ERR_SYSTEM(TH_EAGAIN); + th_address_init(op->addr); + return th_acceptor_accept(op->acceptor, op->addr, op->socket); } TH_LOCAL(void) -th_send_op_fn(void* self) +th_accept_op_fn(void* self) { - th_send_op* op = self; + th_accept_op* op = self; if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { - th_send_op_finalize(op); + th_accept_op_finalize(op); return; } - th_err err = th_send_op_perform(op); - if (th_send_op_is_retryable(err)) { - err = th_socket_submit(op->socket, &op->base); + th_err err = th_accept_op_perform(op); + if (th_accept_op_is_retryable(err)) { + err = th_acceptor_submit(op->acceptor, &op->base); if (err == TH_ERR_OK) return; } - th_send_op_complete(op, err); + th_accept_op_complete(op, err); } TH_LOCAL(void) -th_send_op_abort(void* self, th_err err) +th_accept_op_abort(void* self, th_err err) { - th_send_op_complete(self, err); + th_accept_op_complete(self, err); } TH_PRIVATE(void) -th_send_op_init(th_send_op* op, th_socket* socket, const void* addr, size_t len, th_send_cb callback, void* user_data) +th_accept_op_init(th_accept_op* op, th_acceptor* acceptor, th_address* addr, + th_socket* socket, th_accept_cb callback, void* user_data) { - th_op_init(&op->base, TH_OP_WRITE, th_send_op_fn, th_send_op_abort); - op->socket = socket; + th_op_init(&op->base, TH_OP_READ, th_accept_op_fn, th_accept_op_abort); + op->acceptor = acceptor; op->addr = addr; - op->len = len; - op->pos = 0; + op->socket = socket; op->callback = callback; op->user_data = user_data; op->err = TH_ERR_OK; } -/* End of src/th_send.c */ -/* Start of src/th_sendvec.c */ - -TH_LOCAL(bool) -th_sendvec_op_is_retryable(th_err err) -{ - return err == TH_ERR_SYSTEM(TH_EAGAIN) - || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); -} - -TH_LOCAL(void) -th_sendvec_op_finalize(th_sendvec_op* op) -{ - op->callback(op->user_data, op->pos, op->err); -} +/* End of src/th_accept.c */ +/* Start of src/th_tcp_conn.c */ -TH_LOCAL(void) -th_sendvec_op_complete(th_sendvec_op* op, th_err err) -{ - op->err = err; - th_op_set_flags(&op->base, TH_OP_COMPLETED); - th_socket_post(op->socket, &op->base.base); -} -TH_LOCAL(th_err) -th_sendvec_op_perform(th_sendvec_op* op) -{ - th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); - size_t result = 0; - th_err err = th_socket_sendvec(op->socket, op->iov, op->iovcnt, &result); - if (err != TH_ERR_OK) - return err; - op->pos += result; - th_iov_consume(&op->iov, &op->iovcnt, result); - if (op->iovcnt == 0) - return TH_ERR_OK; - return TH_ERR_SYSTEM(TH_EAGAIN); -} +#undef TH_LOG_TAG +#define TH_LOG_TAG "tcp_conn" -TH_LOCAL(void) -th_sendvec_op_fn(void* self) -{ - th_sendvec_op* op = self; - if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { - th_sendvec_op_finalize(op); - return; - } - th_err err = th_sendvec_op_perform(op); - if (th_sendvec_op_is_retryable(err)) { - err = th_socket_submit(op->socket, &op->base); - if (err == TH_ERR_OK) - return; - } - th_sendvec_op_complete(op, err); -} +/** th_tcp_conn_op + * @brief At most one recv and one send are ever in flight at a time on + * an HTTP connection (request read, then response write), so a single + * union covers every th_conn_methods.recv/send call without allocating. + */ +typedef union th_tcp_conn_op { + th_recv_op recv; + th_sendvec_op sendvec; + th_sendfile_op sendfile; +} th_tcp_conn_op; -TH_LOCAL(void) -th_sendvec_op_abort(void* self, th_err err) -{ - th_sendvec_op_complete(self, err); -} +typedef struct th_tcp_conn { + th_conn_observable base; + th_socket socket; + th_address addr; + th_tcp_conn_op recv_op; + th_tcp_conn_op send_op; + th_conn_upgrader* upgrader; + th_allocator* allocator; +} th_tcp_conn; -TH_PRIVATE(void) -th_sendvec_op_init(th_sendvec_op* op, th_socket* socket, th_iov* iov, size_t iovcnt, th_send_cb callback, void* user_data) +TH_LOCAL(th_address*) +th_tcp_conn_get_address(void* self) { - th_op_init(&op->base, TH_OP_WRITE, th_sendvec_op_fn, th_sendvec_op_abort); - op->socket = socket; - op->iov = iov; - op->iovcnt = iovcnt; - op->pos = 0; - op->callback = callback; - op->user_data = user_data; - op->err = TH_ERR_OK; + th_tcp_conn* conn = self; + return &conn->addr; } -/* End of src/th_sendvec.c */ -/* Start of src/th_sendfile.c */ -TH_LOCAL(bool) -th_sendfile_op_is_retryable(th_err err) +TH_LOCAL(th_socket*) +th_tcp_conn_get_socket(void* self) { - return err == TH_ERR_SYSTEM(TH_EAGAIN) - || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); + th_tcp_conn* conn = self; + return &conn->socket; } TH_LOCAL(void) -th_sendfile_op_finalize(th_sendfile_op* op) +th_tcp_conn_start(void* self) { - op->callback(op->user_data, op->pos, op->err); + th_tcp_conn* conn = self; + TH_LOG_TRACE("%p: Starting", conn); + th_conn_upgrader_upgrade(conn->upgrader, (th_conn*)conn); } TH_LOCAL(void) -th_sendfile_op_complete(th_sendfile_op* op, th_err err) +th_tcp_conn_recv(void* self, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data) { - op->err = err; - th_op_set_flags(&op->base, TH_OP_COMPLETED); - th_socket_post(op->socket, &op->base.base); + th_tcp_conn* conn = self; + th_recv_op_init(&conn->recv_op.recv, &conn->socket, addr, len, exact, callback, user_data); + th_op_perform(&conn->recv_op.recv.base); } -TH_LOCAL(th_err) -th_sendfile_op_perform(th_sendfile_op* op) +TH_LOCAL(void) +th_tcp_conn_send(void* self, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) { - th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); - size_t file_pos = op->pos > op->header_len ? op->pos - op->header_len : 0; - size_t remaining = op->len - file_pos; - size_t chunk = TH_MIN(remaining, TH_CONFIG_SENDFILE_CHUNK_LEN); - - size_t result = 0; - th_err err = th_socket_sendfile(op->socket, op->iov, op->iovcnt, op->file, op->offset + file_pos, chunk, &result); - if (err != TH_ERR_OK) - return err; - - op->pos += result; - th_iov_consume(&op->iov, &op->iovcnt, result); - if (op->pos == op->header_len + op->len) - return TH_ERR_OK; - return TH_ERR_SYSTEM(TH_EAGAIN); + th_tcp_conn* conn = self; + if (file) { + th_sendfile_op_init(&conn->send_op.sendfile, &conn->socket, iov, iovcnt, file, offset, len, callback, user_data); + th_op_perform(&conn->send_op.sendfile.base); + } else { + th_sendvec_op_init(&conn->send_op.sendvec, &conn->socket, iov, iovcnt, callback, user_data); + th_op_perform(&conn->send_op.sendvec.base); + } } TH_LOCAL(void) -th_sendfile_op_fn(void* self) +th_tcp_conn_cancel(void* self) { - th_sendfile_op* op = self; - if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { - th_sendfile_op_finalize(op); - return; - } - th_err err = th_sendfile_op_perform(op); - if (th_sendfile_op_is_retryable(err)) { - err = th_socket_submit(op->socket, &op->base); - if (err == TH_ERR_OK) - return; - } - th_sendfile_op_complete(op, err); + th_tcp_conn* conn = self; + th_socket_cancel(&conn->socket); } TH_LOCAL(void) -th_sendfile_op_abort(void* self, th_err err) +th_tcp_conn_free(void* self) { - th_sendfile_op_complete(self, err); + th_tcp_conn* conn = self; + TH_LOG_TRACE("%p: Destroying connection", conn); + th_socket_deinit(&conn->socket); + th_allocator_free(conn->allocator, conn); +} + +static const th_conn_methods th_tcp_conn_methods = { + .get_address = th_tcp_conn_get_address, + .get_socket = th_tcp_conn_get_socket, + .start = th_tcp_conn_start, + .recv = th_tcp_conn_recv, + .send = th_tcp_conn_send, + .cancel = th_tcp_conn_cancel, + .destroy = th_conn_observable_destroy, +}; + +TH_PRIVATE(th_err) +th_tcp_conn_create(th_conn** out, th_socket* socket, + th_conn_upgrader* upgrader, th_conn_observer* observer, + th_allocator* allocator) +{ + allocator = allocator ? allocator : th_default_allocator_get(); + th_tcp_conn* conn = th_allocator_alloc(allocator, sizeof(th_tcp_conn)); + if (!conn) + return TH_ERR_BAD_ALLOC; + th_conn_observable_init(&conn->base, &th_tcp_conn_methods, th_tcp_conn_free, observer); + conn->upgrader = upgrader; + conn->allocator = allocator; + conn->socket = *socket; + th_address_init(&conn->addr); + *out = (th_conn*)conn; + return TH_ERR_OK; } +/* End of src/th_tcp_conn.c */ +/* Start of src/th_request_parser.c */ + + +#undef TH_LOG_TAG +#define TH_LOG_TAG "request_parser" TH_PRIVATE(void) -th_sendfile_op_init(th_sendfile_op* op, th_socket* socket, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) +th_request_parser_init(th_request_parser* parser) { - th_op_init(&op->base, TH_OP_WRITE, th_sendfile_op_fn, th_sendfile_op_abort); - op->socket = socket; - op->iov = iov; - op->iovcnt = iovcnt; - op->file = file; - op->offset = offset; - op->len = len; - op->header_len = th_iov_bytes(iov, iovcnt); - op->pos = 0; - op->callback = callback; - op->user_data = user_data; - op->err = TH_ERR_OK; + parser->state = TH_REQUEST_PARSER_STATE_METHOD; + parser->content_len = 0; + parser->body_encoding = TH_REQUEST_BODY_ENCODING_NONE; } -/* End of src/th_sendfile.c */ -/* Start of src/th_acceptor.c */ +TH_PRIVATE(void) +th_request_parser_reset(th_request_parser* parser) +{ + parser->state = TH_REQUEST_PARSER_STATE_METHOD; + parser->content_len = 0; + parser->body_encoding = TH_REQUEST_BODY_ENCODING_NONE; +} -#if defined(TH_CONFIG_OS_POSIX) -#include -#include -#include -#include -#include -#include -#include -#include +TH_PRIVATE(size_t) +th_request_parser_content_len(th_request_parser* parser) +{ + return parser->content_len; +} TH_LOCAL(th_err) -th_acceptor_ops_os_set_nonblocking(int fd) +th_request_parser_do_cookie_list(th_request* request, th_str cookie_list) { - if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) < 0) - return TH_ERR_SYSTEM(errno); + th_cookie_parser parser; + th_cookie_parser_init(&parser, cookie_list); + while (!th_cookie_parser_done(&parser)) { + th_str key, value; + th_err err = th_cookie_parser_next(&parser, &key, &value); + if (err != TH_ERR_OK) { + return err; + } + if ((err = th_request_add_cookie(request, key, value)) != TH_ERR_OK) { + return err; + } + } return TH_ERR_OK; } TH_LOCAL(th_err) -th_acceptor_ops_os_open(void* self, const char* addr, const char* port, int* out_fd) +th_request_parser_do_next_queryvar(th_str string, size_t* pos, th_str* key, th_str* value) { - (void)self; - struct addrinfo hints = {0}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; - struct addrinfo* res = NULL; - if (getaddrinfo(addr, port, &hints, &res) != 0) - return TH_ERR_SYSTEM(errno); + size_t eq = th_str_find_first(string, *pos, '='); + if (eq == th_str_npos) { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + *key = th_str_trim(th_str_substr(string, *pos, eq - *pos)); + *pos = th_str_find_first(string, eq + 1, '&'); + if (*pos != th_str_npos) { + *value = th_str_trim(th_str_substr(string, eq + 1, *pos - eq - 1)); + (*pos)++; + return TH_ERR_OK; + } else { + *value = th_str_trim(th_str_substr(string, eq + 1, *pos)); + return TH_ERR_OK; + } + return TH_ERR_OK; +} +TH_LOCAL(th_err) +th_request_parser_do_bodyvars(th_request* request, th_str body) +{ th_err err = TH_ERR_OK; - int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (fd < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_addrinfo; - } -#if TH_CONFIG_REUSE_ADDR - { - int optval = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; + size_t pos = 0; + while (pos != th_str_npos) { + th_str key; + th_str value; + err = th_request_parser_do_next_queryvar(body, &pos, &key, &value); + if (err != TH_ERR_OK) { + return err; } - } -#endif -#if TH_CONFIG_REUSE_PORT - { -#if defined(SO_REUSEPORT) - int optval = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; + if ((err = th_request_add_formvar(request, key, value)) != TH_ERR_OK) { + return err; } -#else - TH_LOG_FATAL("SO_REUSEPORT is not supported on this platform"); - err = TH_ERR_NOSUPPORT; - goto cleanup_fd; -#endif - } -#endif - if ((err = th_acceptor_ops_os_set_nonblocking(fd)) != TH_ERR_OK) - goto cleanup_fd; - if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; } - if (listen(fd, 1024) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; + return err; +} + +/* Get the next HTTP token from the buffer, stopping at the given character */ +TH_LOCAL(th_err) +th_request_parser_next_token(th_str buffer, th_str* token, char until, size_t* parsed) +{ + static const int token_char[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32-47 (don't allow space, ") + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48-57 (0-9) + 1, 1, 0, 1, 0, 1, 1, // 58-64 (don't allow <,>) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 65-80 (A-P) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 81-90 (Q-Z) + 0, 0, 0, 1, 1, 1, // 91-96 (don't allow [, \, ]) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 97-112 (a-p) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 113-122 (q-z) + 0, 1, 0, 1, 0, // 123-127 (don't allow {, }, DEL) + // implicitely set to 0 for 128-255 + }; + size_t i = 0; + while (i < buffer.len && buffer.ptr[i] != until) { + if (token_char[(unsigned char)buffer.ptr[i]] == 0) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + i++; } - freeaddrinfo(res); - *out_fd = fd; + if (i == buffer.len) + return TH_ERR_OK; + if (i == 0) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *token = th_str_substr(buffer, 0, i); + *parsed = i + 1; return TH_ERR_OK; -cleanup_fd: - close(fd); -cleanup_addrinfo: - freeaddrinfo(res); - return err; +} + +TH_LOCAL(bool) +th_request_parser_is_printable_string(th_str input) +{ + for (size_t i = 0; i < input.len; i++) { + if (input.ptr[i] < 32 || input.ptr[i] > 126) { + return false; + } + } + return true; } TH_LOCAL(th_err) -th_acceptor_ops_os_accept(void* self, int fd, th_address* addr, int* out_fd) +th_request_parser_do_method(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed_out) { - (void)self; - int conn_fd = accept(fd, (struct sockaddr*)&addr->addr, &addr->addrlen); - if (conn_fd < 0) - return TH_ERR_SYSTEM(errno); - th_err err = th_acceptor_ops_os_set_nonblocking(conn_fd); - if (err != TH_ERR_OK) { - close(conn_fd); + th_str method; + size_t parsed = 0; + th_err err = th_request_parser_next_token(buffer, &method, ' ', &parsed); + if (err != TH_ERR_OK || parsed == 0) { return err; } - *out_fd = conn_fd; + struct th_method_mapping* mm = th_method_mapping_find(method.ptr, method.len); + if (!mm) { + return TH_ERR_HTTP(TH_CODE_NOT_IMPLEMENTED); + } + th_request_set_method(request, mm->method); + *parsed_out = parsed; + parser->state = TH_REQUEST_PARSER_STATE_PATH; return TH_ERR_OK; } -TH_PRIVATE(th_acceptor_ops*) -th_acceptor_ops_os(void) +TH_LOCAL(th_err) +th_request_parser_do_uri_query(th_request* request, th_str path) { - static th_acceptor_ops ops = { - .open = th_acceptor_ops_os_open, - .accept = th_acceptor_ops_os_accept, - }; - return &ops; + size_t pos = 0; + while (pos != th_str_npos) { + th_str key; + th_str value; + th_err err = th_request_parser_do_next_queryvar(path, &pos, &key, &value); + if (err != TH_ERR_OK) { + return err; + } + if (th_request_add_queryvar(request, key, value) != TH_ERR_OK) { + return TH_ERR_BAD_ALLOC; + } + } + return TH_ERR_OK; } -#endif /* TH_CONFIG_OS_POSIX */ - -TH_PRIVATE(void) -th_acceptor_init(th_acceptor* acceptor, th_loop* loop, th_acceptor_ops* ops) +TH_LOCAL(th_err) +th_request_parser_next_path_segment(th_str buffer, th_str* segment, size_t* parsed) { - acceptor->loop = loop; - acceptor->handle = NULL; - acceptor->ops = ops; + static const int uri_char[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32-47 (don't allow space, ") + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48-57 (0-9) + 1, 1, 0, 1, 0, 1, 1, // 58-64 (don't allow <,>) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 65-80 (A-P) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 81-90 (Q-Z) + 0, 0, 0, 0, 1, 0, // 91-96 (don't allow [, \, ], ^, `) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 97-112 (a-p) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 113-122 (q-z) + 0, 0, 0, 1, 0, // 123-127 (don't allow {, |, }, DEL) + // implicitely set to 0 for 128-255 + }; + size_t i = 0; + while (i < buffer.len && buffer.ptr[i] != ' ' && buffer.ptr[i] != '?') { + if (uri_char[(unsigned char)buffer.ptr[i]] == 0) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + i++; + } + if (i == buffer.len) + return TH_ERR_OK; + *segment = th_str_substr(buffer, 0, i); + *parsed = i + 1; + return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_acceptor_open(th_acceptor* acceptor, const char* addr, const char* port) +TH_LOCAL(th_err) +th_request_parser_do_path(th_request_parser* parser, th_request* request, th_str path, size_t* parsed) { - int fd = -1; - th_err err = acceptor->ops->open(acceptor->ops, addr, port, &fd); - if (err != TH_ERR_OK) + th_str segment; + size_t uri_parsed = 0; + th_err err = th_request_parser_next_path_segment(path, &segment, &uri_parsed); + if (err != TH_ERR_OK || uri_parsed == 0) return err; - th_acceptor_close(acceptor); - err = th_reactor_create_handle(acceptor->loop->reactor, &acceptor->handle, fd); - if (err != TH_ERR_OK) { -#if defined(TH_CONFIG_OS_POSIX) - close(fd); -#endif + if ((err = th_request_set_uri_path(request, segment)) != TH_ERR_OK) return err; + if (segment.ptr[segment.len] == '?') { // got a query + size_t query_parsed = 0; + err = th_request_parser_next_path_segment(th_str_substr(path, uri_parsed, th_str_npos), &segment, &query_parsed); + if (err != TH_ERR_OK || query_parsed == 0) + return err; + if ((err = th_request_set_uri_query(request, segment)) != TH_ERR_OK) + return err; + if ((err = th_request_parser_do_uri_query(request, segment)) != TH_ERR_OK) { + // If we can't parse the query, that's ok, we just ignore it + // restore the original state and continue + th_request_clear_queryvars(request); + } + uri_parsed += query_parsed; + } else { + if ((err = th_request_set_uri_query(request, TH_STR(""))) != TH_ERR_OK) + return err; } - th_handle_enable_timeout(acceptor->handle, false); + *parsed = uri_parsed; + parser->state = TH_REQUEST_PARSER_STATE_VERSION; return TH_ERR_OK; } -TH_PRIVATE(void) -th_acceptor_close(th_acceptor* acceptor) +TH_LOCAL(th_err) +th_request_parser_do_version(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) { - if (acceptor->handle) { - th_handle_destroy(acceptor->handle); - acceptor->handle = NULL; + size_t n = th_str_find_first(buffer, 0, '\r'); + if (n == th_str_npos || n + 1 == buffer.len) + return TH_ERR_OK; + if (buffer.ptr[n + 1] != '\n') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + th_str version = th_str_substr(buffer, 0, n); + if (version.len != 8) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[0] != 'H') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[1] != 'T') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[2] != 'T') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[3] != 'P') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[4] != '/') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[5] != '1') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[6] != '.') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (version.ptr[7] < '0' || version.ptr[7] > '9') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + th_request_set_version(request, version.ptr[7] - '0'); + *parsed = n + 2; + parser->state = TH_REQUEST_PARSER_STATE_HEADERS; + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_request_parse_handle_header(th_request_parser* parser, th_request* request, th_str name, th_str value) +{ + char arena[1024] = {0}; + th_arena_allocator arena_allocator; + th_arena_allocator_init(&arena_allocator, arena, sizeof(arena), NULL); + th_string normalized_name; + th_string_init(&normalized_name, &arena_allocator.base); + if (th_string_set(&normalized_name, name) != TH_ERR_OK) { + // This can only happen if the name is too long + return TH_ERR_HTTP(TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE); + } + th_string_to_lower(&normalized_name); + th_header_id id = th_header_id_from_string(th_string_data(&normalized_name), th_string_len(&normalized_name)); + switch (id) { + case TH_HEADER_ID_COOKIE: + return th_request_parser_do_cookie_list(request, value); + case TH_HEADER_ID_CONTENT_LENGTH: { + unsigned int content_len = 0; + th_err err = th_str_to_uint(value, &content_len); + parser->content_len = content_len; + return err; + } + case TH_HEADER_ID_CONNECTION: + if (th_str_eq(value, TH_STR("close"))) { + request->close = true; + } else if (th_str_eq(value, TH_STR("keep-alive"))) { + request->close = false; + } + break; + case TH_HEADER_ID_CONTENT_TYPE: + if (th_str_eq(value, TH_STR("application/x-www-form-urlencoded"))) { + parser->body_encoding = TH_REQUEST_BODY_ENCODING_FORM_URL_ENCODED; + } else if (th_str_eq(th_str_substr(value, 0, 19), TH_STR("multipart/form-data"))) { + parser->body_encoding = TH_REQUEST_BODY_ENCODING_MULTIPART_FORM_DATA; + } + break; + default: + break; } + return th_request_add_header(request, th_string_view(&normalized_name), value); } -TH_PRIVATE(void) -th_acceptor_deinit(th_acceptor* acceptor) +TH_LOCAL(th_err) +th_request_parser_do_header(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) { - th_acceptor_close(acceptor); + size_t n = th_str_find_first(buffer, 0, '\r'); + if (n == th_str_npos || n + 1 == buffer.len) + return TH_ERR_OK; + if (buffer.ptr[n + 1] != '\n') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (n == 0) { + *parsed = 2; + if (parser->content_len == 0) { + th_request_set_body(request, th_str_make(&buffer.ptr[2], 0)); + parser->state = TH_REQUEST_PARSER_STATE_DONE; + } else { + if (request->method == TH_METHOD_GET || request->method == TH_METHOD_HEAD) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + parser->state = TH_REQUEST_PARSER_STATE_BODY; + } + return TH_ERR_OK; + } + size_t key_parsed = 0; + th_str key; + th_err err = TH_ERR_OK; + if ((err = th_request_parser_next_token(buffer, &key, ':', &key_parsed)) != TH_ERR_OK + || key_parsed == 0) + return err; + th_str value = th_str_substr(buffer, key_parsed, n - key_parsed); + if (!th_request_parser_is_printable_string(value)) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if ((err = th_request_parse_handle_header(parser, request, th_str_trim(key), th_str_trim(value))) + != TH_ERR_OK) + return err; + *parsed = n + 2; + return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_acceptor_accept(th_acceptor* acceptor, th_address* addr, th_socket* out_socket) +TH_LOCAL(th_err) +th_request_parser_do_multipart_form_data(th_request* request, th_str body) { - int fd = -1; - th_err err = acceptor->ops->accept(acceptor->ops, th_acceptor_get_fd(acceptor), addr, &fd); - if (err != TH_ERR_OK) + th_str content_type = th_request_get_header(request, TH_STR("content-type")); + if (th_str_empty(content_type)) { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + th_err err = TH_ERR_OK; + th_str boundary = th_str_make_empty(); + if ((err = th_multipart_parser_boundary(content_type, &boundary)) != TH_ERR_OK) return err; - return th_socket_set_fd(out_socket, fd); -} -/* End of src/th_acceptor.c */ -/* Start of src/th_accept.c */ -TH_LOCAL(bool) -th_accept_op_is_retryable(th_err err) -{ - return err == TH_ERR_SYSTEM(TH_EAGAIN) - || err == TH_ERR_SYSTEM(TH_EWOULDBLOCK); + th_multipart_parser parser; + if ((err = th_multipart_parser_init(&parser, body, boundary)) != TH_ERR_OK) + return err; + while (!th_multipart_parser_done(&parser)) { + th_multipart_part part; + if ((err = th_multipart_parser_next(&parser, &part)) != TH_ERR_OK) + return err; + if (th_request_add_part(request, part.content, part.name, part.filename, part.content_type) != TH_ERR_OK) + return TH_ERR_BAD_ALLOC; + } + return TH_ERR_OK; } -TH_LOCAL(void) -th_accept_op_finalize(th_accept_op* op) +TH_LOCAL(th_err) +th_request_parser_do_body(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) { - op->callback(op->user_data, op->err); + if (buffer.len < parser->content_len) { + *parsed = 0; + return TH_ERR_OK; + } + // Got the whole body + th_str body = th_str_substr(buffer, 0, parser->content_len); + if (parser->body_encoding == TH_REQUEST_BODY_ENCODING_FORM_URL_ENCODED) { + th_err err = TH_ERR_OK; + if ((err = th_request_parser_do_bodyvars(request, body)) != TH_ERR_OK) + return err; + } else if (parser->body_encoding == TH_REQUEST_BODY_ENCODING_MULTIPART_FORM_DATA) { + th_err err = TH_ERR_OK; + if ((err = th_request_parser_do_multipart_form_data(request, body)) != TH_ERR_OK) + return err; + } + th_request_set_body(request, body); + *parsed = parser->content_len; + parser->state = TH_REQUEST_PARSER_STATE_DONE; + return TH_ERR_OK; } -TH_LOCAL(void) -th_accept_op_complete(th_accept_op* op, th_err err) +TH_LOCAL(th_err) +th_request_parser_parse_next(th_request_parser* parser, th_request* request, th_str data, size_t* parsed) { - op->err = err; - th_op_set_flags(&op->base, TH_OP_COMPLETED); - th_acceptor_post(op->acceptor, &op->base.base); + switch (parser->state) { + case TH_REQUEST_PARSER_STATE_METHOD: + return th_request_parser_do_method(parser, request, data, parsed); + case TH_REQUEST_PARSER_STATE_PATH: + return th_request_parser_do_path(parser, request, data, parsed); + case TH_REQUEST_PARSER_STATE_VERSION: + return th_request_parser_do_version(parser, request, data, parsed); + case TH_REQUEST_PARSER_STATE_HEADERS: + return th_request_parser_do_header(parser, request, data, parsed); + case TH_REQUEST_PARSER_STATE_BODY: + return th_request_parser_do_body(parser, request, data, parsed); + default: + *parsed = 0; + break; + } + return TH_ERR_OK; } -TH_LOCAL(th_err) -th_accept_op_perform(th_accept_op* op) +TH_PRIVATE(th_err) +th_request_parser_parse(th_request_parser* parser, th_request* request, th_str data, size_t* parsed) { - th_op_clear_flags(&op->base, TH_OP_IMMEDIATE); - th_address_init(op->addr); - return th_acceptor_accept(op->acceptor, op->addr, op->socket); + th_err err = TH_ERR_OK; + while (data.len > 0) { + size_t p = 0; + if ((err = th_request_parser_parse_next(parser, request, th_str_substr(data, p, data.len), &p)) != TH_ERR_OK) { + *parsed = p; + return err; + } + data.ptr += p; + data.len -= p; + *parsed += p; + if (p == 0 || parser->state == TH_REQUEST_PARSER_STATE_DONE) { + return TH_ERR_OK; + } + } + return TH_ERR_OK; } -TH_LOCAL(void) -th_accept_op_fn(void* self) +TH_PRIVATE(bool) +th_request_parser_header_done(th_request_parser* parser) { - th_accept_op* op = self; - if (th_op_get_flags(&op->base) & TH_OP_COMPLETED) { - th_accept_op_finalize(op); - return; - } - th_err err = th_accept_op_perform(op); - if (th_accept_op_is_retryable(err)) { - err = th_acceptor_submit(op->acceptor, &op->base); - if (err == TH_ERR_OK) - return; - } - th_accept_op_complete(op, err); + return parser->state > TH_REQUEST_PARSER_STATE_HEADERS; } -TH_LOCAL(void) -th_accept_op_abort(void* self, th_err err) +TH_PRIVATE(bool) +th_request_parser_done(th_request_parser* parser) { - th_accept_op_complete(self, err); + return parser->state == TH_REQUEST_PARSER_STATE_DONE; } +/* End of src/th_request_parser.c */ +/* Start of src/th_cookie_parser.c */ TH_PRIVATE(void) -th_accept_op_init(th_accept_op* op, th_acceptor* acceptor, th_address* addr, - th_socket* socket, th_accept_cb callback, void* user_data) +th_cookie_parser_init(th_cookie_parser* parser, th_str cookie_header) { - th_op_init(&op->base, TH_OP_READ, th_accept_op_fn, th_accept_op_abort); - op->acceptor = acceptor; - op->addr = addr; - op->socket = socket; - op->callback = callback; - op->user_data = user_data; - op->err = TH_ERR_OK; + parser->str = cookie_header; + parser->pos = cookie_header.len == 0 ? th_str_npos : 0; } -/* End of src/th_accept.c */ -/* Start of src/th_tcp_conn.c */ +TH_PRIVATE(bool) +th_cookie_parser_done(const th_cookie_parser* parser) +{ + return parser->pos == th_str_npos; +} -#undef TH_LOG_TAG -#define TH_LOG_TAG "tcp_conn" - -/** th_tcp_conn_op - * @brief At most one recv and one send are ever in flight at a time on - * an HTTP connection (request read, then response write), so a single - * union covers every th_conn_methods.recv/send call without allocating. - */ -typedef union th_tcp_conn_op { - th_recv_op recv; - th_sendvec_op sendvec; - th_sendfile_op sendfile; -} th_tcp_conn_op; +/* RFC 2616 section 2.2 token: no CTLs, no separators + * "()<>@,;:\"/[]?={} \t". Used for cookie-name. */ +static const int th_cookie_parser_name_char[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 !"#$%&'()*+,-./ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 0123456789:;<=>? + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL + // implicitly 0 for 128-255 +}; -typedef struct th_tcp_conn { - th_conn_observable base; - th_socket socket; - th_address addr; - th_tcp_conn_op recv_op; - th_tcp_conn_op send_op; - th_conn_upgrader* upgrader; - th_allocator* allocator; -} th_tcp_conn; +/* RFC 6265 section 4.1.1 cookie-octet: %x21 / %x23-2B / %x2D-3A / %x3C-5B / + * %x5D-7E - printable ASCII minus space, DQUOTE, comma, semicolon, + * backslash. Used for a bare (unquoted) cookie-value. */ +static const int th_cookie_parser_value_char[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 32-47 !"#$%&'()*+,-./ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, // 48-63 0123456789:;<=>? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL + // implicitly 0 for 128-255 +}; -TH_LOCAL(th_address*) -th_tcp_conn_get_address(void* self) -{ - th_tcp_conn* conn = self; - return &conn->addr; -} +/* Same as th_cookie_parser_value_char, plus space - the quoted form exists + * so servers can embed characters a bare cookie-value can't (project + * decision, not literal RFC 6265). */ +static const int th_cookie_parser_quoted_value_char[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 32-47 !"#$%&'()*+,-./ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, // 48-63 0123456789:;<=>? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL + // implicitly 0 for 128-255 +}; -TH_LOCAL(th_socket*) -th_tcp_conn_get_socket(void* self) +TH_LOCAL(bool) +th_cookie_parser_is_space(char c) { - th_tcp_conn* conn = self; - return &conn->socket; + return c == ' ' || c == '\t'; } -TH_LOCAL(void) -th_tcp_conn_start(void* self) +TH_LOCAL(size_t) +th_cookie_parser_skip_space(th_str str, size_t pos) { - th_tcp_conn* conn = self; - TH_LOG_TRACE("%p: Starting", conn); - th_conn_upgrader_upgrade(conn->upgrader, (th_conn*)conn); + while (pos < str.len && th_cookie_parser_is_space(str.ptr[pos])) { + pos++; + } + return pos; } -TH_LOCAL(void) -th_tcp_conn_recv(void* self, void* addr, size_t len, bool exact, th_recv_cb callback, void* user_data) +/* Scans a cookie-name: one or more token chars, followed by optional space. + * Leaves *pos on '=' (the caller checks it's actually there). */ +TH_LOCAL(th_err) +th_cookie_parser_scan_name(th_str str, size_t* pos, th_str* name) { - th_tcp_conn* conn = self; - th_recv_op_init(&conn->recv_op.recv, &conn->socket, addr, len, exact, callback, user_data); - th_op_perform(&conn->recv_op.recv.base); + size_t start = *pos; + while (*pos < str.len && th_cookie_parser_name_char[(unsigned char)str.ptr[*pos]]) { + (*pos)++; + } + if (*pos == start) { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + *name = th_str_substr(str, start, *pos - start); + *pos = th_cookie_parser_skip_space(str, *pos); + if (*pos >= str.len || str.ptr[*pos] != '=') { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + return TH_ERR_OK; } -TH_LOCAL(void) -th_tcp_conn_send(void* self, th_iov* iov, size_t iovcnt, th_file* file, size_t offset, size_t len, th_send_cb callback, void* user_data) +/* Scans a quoted cookie-value, starting at the opening DQUOTE. */ +TH_LOCAL(th_err) +th_cookie_parser_scan_quoted_value(th_str str, size_t* pos, th_str* value) { - th_tcp_conn* conn = self; - if (file) { - th_sendfile_op_init(&conn->send_op.sendfile, &conn->socket, iov, iovcnt, file, offset, len, callback, user_data); - th_op_perform(&conn->send_op.sendfile.base); - } else { - th_sendvec_op_init(&conn->send_op.sendvec, &conn->socket, iov, iovcnt, callback, user_data); - th_op_perform(&conn->send_op.sendvec.base); + size_t start = *pos + 1; + size_t i = start; + while (i < str.len && th_cookie_parser_quoted_value_char[(unsigned char)str.ptr[i]]) { + i++; } + if (i >= str.len || str.ptr[i] != '"') { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + *value = th_str_substr(str, start, i - start); + *pos = i + 1; + return TH_ERR_OK; } -TH_LOCAL(void) -th_tcp_conn_cancel(void* self) +/* Scans a bare (unquoted) cookie-value: zero or more cookie-octets. */ +TH_LOCAL(th_err) +th_cookie_parser_scan_bare_value(th_str str, size_t* pos, th_str* value) { - th_tcp_conn* conn = self; - th_socket_cancel(&conn->socket); + size_t start = *pos; + while (*pos < str.len && th_cookie_parser_value_char[(unsigned char)str.ptr[*pos]]) { + (*pos)++; + } + *value = th_str_substr(str, start, *pos - start); + return TH_ERR_OK; } -TH_LOCAL(void) -th_tcp_conn_free(void* self) +TH_LOCAL(th_err) +th_cookie_parser_scan_value(th_str str, size_t* pos, th_str* value) { - th_tcp_conn* conn = self; - TH_LOG_TRACE("%p: Destroying connection", conn); - th_socket_deinit(&conn->socket); - th_allocator_free(conn->allocator, conn); + if (*pos < str.len && str.ptr[*pos] == '"') { + return th_cookie_parser_scan_quoted_value(str, pos, value); + } + return th_cookie_parser_scan_bare_value(str, pos, value); } -static const th_conn_methods th_tcp_conn_methods = { - .get_address = th_tcp_conn_get_address, - .get_socket = th_tcp_conn_get_socket, - .start = th_tcp_conn_start, - .recv = th_tcp_conn_recv, - .send = th_tcp_conn_send, - .cancel = th_tcp_conn_cancel, - .destroy = th_conn_observable_destroy, -}; - -TH_PRIVATE(th_err) -th_tcp_conn_create(th_conn** out, th_socket* socket, - th_conn_upgrader* upgrader, th_conn_observer* observer, - th_allocator* allocator) +/* After a pair, only space may remain before ';' or the end of input - any + * other byte (e.g. a stray octet the value scan stopped on) is malformed. */ +TH_LOCAL(th_err) +th_cookie_parser_scan_pair_end(th_str str, size_t* pos) { - allocator = allocator ? allocator : th_default_allocator_get(); - th_tcp_conn* conn = th_allocator_alloc(allocator, sizeof(th_tcp_conn)); - if (!conn) - return TH_ERR_BAD_ALLOC; - th_conn_observable_init(&conn->base, &th_tcp_conn_methods, th_tcp_conn_free, observer); - conn->upgrader = upgrader; - conn->allocator = allocator; - conn->socket = *socket; - th_address_init(&conn->addr); - *out = (th_conn*)conn; + *pos = th_cookie_parser_skip_space(str, *pos); + if (*pos == str.len) { + return TH_ERR_OK; + } + if (str.ptr[*pos] != ';') { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + (*pos)++; return TH_ERR_OK; } -/* End of src/th_tcp_conn.c */ -/* Start of src/th_request_parser.c */ - - -#undef TH_LOG_TAG -#define TH_LOG_TAG "request_parser" -TH_PRIVATE(void) -th_request_parser_init(th_request_parser* parser) +TH_PRIVATE(th_err) +th_cookie_parser_next(th_cookie_parser* parser, th_str* key, th_str* value) { - parser->state = TH_REQUEST_PARSER_STATE_METHOD; - parser->content_len = 0; - parser->body_encoding = TH_REQUEST_BODY_ENCODING_NONE; -} + size_t pos = th_cookie_parser_skip_space(parser->str, parser->pos); -TH_PRIVATE(void) -th_request_parser_reset(th_request_parser* parser) -{ - parser->state = TH_REQUEST_PARSER_STATE_METHOD; - parser->content_len = 0; - parser->body_encoding = TH_REQUEST_BODY_ENCODING_NONE; -} + th_str name; + th_err err = th_cookie_parser_scan_name(parser->str, &pos, &name); + if (err != TH_ERR_OK) { + parser->pos = th_str_npos; + return err; + } + pos = th_cookie_parser_skip_space(parser->str, pos + 1); // skip '=' and space -TH_PRIVATE(size_t) -th_request_parser_content_len(th_request_parser* parser) -{ - return parser->content_len; -} + th_str raw_value; + if ((err = th_cookie_parser_scan_value(parser->str, &pos, &raw_value)) != TH_ERR_OK) { + parser->pos = th_str_npos; + return err; + } -TH_LOCAL(th_err) -th_request_parser_do_cookie_list(th_request* request, th_str cookie_list) -{ - th_cookie_parser parser; - th_cookie_parser_init(&parser, cookie_list); - while (!th_cookie_parser_done(&parser)) { - th_str key, value; - th_err err = th_cookie_parser_next(&parser, &key, &value); - if (err != TH_ERR_OK) { - return err; - } - if ((err = th_request_add_cookie(request, key, value)) != TH_ERR_OK) { - return err; - } + if ((err = th_cookie_parser_scan_pair_end(parser->str, &pos)) != TH_ERR_OK) { + parser->pos = th_str_npos; + return err; } + + parser->pos = pos == parser->str.len ? th_str_npos : pos; + *key = name; + *value = raw_value; return TH_ERR_OK; } +/* End of src/th_cookie_parser.c */ +/* Start of src/th_multipart_parser.c */ + TH_LOCAL(th_err) -th_request_parser_do_next_queryvar(th_str string, size_t* pos, th_str* key, th_str* value) +th_multipart_parser_next_header_param(th_str buffer, th_str* out_name, th_str* out_value, size_t* out_parsed) { - size_t eq = th_str_find_first(string, *pos, '='); - if (eq == th_str_npos) { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - *key = th_str_trim(th_str_substr(string, *pos, eq - *pos)); - *pos = th_str_find_first(string, eq + 1, '&'); - if (*pos != th_str_npos) { - *value = th_str_trim(th_str_substr(string, eq + 1, *pos - eq - 1)); - (*pos)++; + buffer = th_str_substr(buffer, th_str_find_first_not(buffer, 0, ' '), th_str_npos); + size_t eq = th_str_find_first_of(buffer, 0, "=; "); + if (eq == th_str_npos || buffer.ptr[eq] == ';') { + *out_name = th_str_substr(buffer, 0, eq); + *out_value = th_str_make_empty(); + *out_parsed = eq == th_str_npos ? buffer.len : eq + 1; return TH_ERR_OK; + } + if (buffer.ptr[eq] == ' ') // spaces are not allowed + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *out_name = th_str_substr(buffer, 0, eq); + size_t parsed = eq + 1; + buffer = th_str_substr(buffer, eq + 1, th_str_npos); + if (th_str_empty(buffer)) // equals sign must be followed by a value + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + if (buffer.ptr[0] == '"') { + size_t end = th_str_find_first(buffer, 1, '"'); + if (end == th_str_npos) // no closing quote + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *out_value = th_str_substr(buffer, 1, end - 1); + parsed += (end == th_str_npos ? buffer.len : end + 1); } else { - *value = th_str_trim(th_str_substr(string, eq + 1, *pos)); - return TH_ERR_OK; + size_t end = th_str_find_first_of(buffer, 0, "; "); + if (end != th_str_npos && buffer.ptr[end] == ' ') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *out_value = th_str_substr(buffer, 0, end); + parsed += (end == th_str_npos ? buffer.len : end + 1); } + *out_parsed = parsed; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_request_parser_do_bodyvars(th_request* request, th_str body) +TH_PRIVATE(th_err) +th_multipart_parser_boundary(th_str content_type, th_str* boundary) { - th_err err = TH_ERR_OK; - size_t pos = 0; - while (pos != th_str_npos) { - th_str key; - th_str value; - err = th_request_parser_do_next_queryvar(body, &pos, &key, &value); - if (err != TH_ERR_OK) { - return err; - } - if ((err = th_request_add_formvar(request, key, value)) != TH_ERR_OK) { + content_type = th_str_substr(content_type, th_str_find_first(content_type, 0, ';') + 1, th_str_npos); + while (!th_str_empty(content_type)) { + th_str name, value = th_str_make_empty(); + size_t parsed = 0; + th_err err = TH_ERR_OK; + if ((err = th_multipart_parser_next_header_param(content_type, &name, &value, &parsed)) != TH_ERR_OK) return err; + content_type = th_str_substr(content_type, parsed, th_str_npos); + if (th_str_eq(name, TH_STR("boundary"))) { + if (th_str_empty(value)) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *boundary = value; + return TH_ERR_OK; } } - return err; + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); } -/* Get the next HTTP token from the buffer, stopping at the given character */ -TH_LOCAL(th_err) -th_request_parser_next_token(th_str buffer, th_str* token, char until, size_t* parsed) +TH_LOCAL(size_t) +th_multipart_parser_find_eol(th_str buffer, size_t start) { - static const int token_char[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32-47 (don't allow space, ") - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48-57 (0-9) - 1, 1, 0, 1, 0, 1, 1, // 58-64 (don't allow <,>) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 65-80 (A-P) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 81-90 (Q-Z) - 0, 0, 0, 1, 1, 1, // 91-96 (don't allow [, \, ]) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 97-112 (a-p) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 113-122 (q-z) - 0, 1, 0, 1, 0, // 123-127 (don't allow {, }, DEL) - // implicitely set to 0 for 128-255 - }; - size_t i = 0; - while (i < buffer.len && buffer.ptr[i] != until) { - if (token_char[(unsigned char)buffer.ptr[i]] == 0) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - i++; + if (start + 1 >= buffer.len) + return th_str_npos; + th_str searchable = th_str_substr(buffer, 0, buffer.len - 1); + size_t pos = start; + while (pos != th_str_npos) { + pos = th_str_find_first(searchable, pos, '\r'); + if (pos == th_str_npos) + return th_str_npos; + if (buffer.ptr[pos + 1] == '\n') + return pos; + pos++; } - if (i == buffer.len) - return TH_ERR_OK; - if (i == 0) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *token = th_str_substr(buffer, 0, i); - *parsed = i + 1; - return TH_ERR_OK; + return th_str_npos; } TH_LOCAL(bool) -th_request_parser_is_printable_string(th_str input) +th_multipart_parser_is_boundary_line(th_str line, th_str boundary, bool* last) { - for (size_t i = 0; i < input.len; i++) { - if (input.ptr[i] < 32 || input.ptr[i] > 126) { - return false; + *last = false; + if (line.len < boundary.len + 2) + return false; + if (line.ptr[0] != '-' || line.ptr[1] != '-') + return false; + if (th_str_eq(th_str_substr(line, 2, boundary.len), boundary)) { + if (line.len == boundary.len + 2) + return true; + if (line.ptr[boundary.len + 2] == '-' && line.ptr[boundary.len + 3] == '-') { + *last = true; + return true; } } - return true; -} - -TH_LOCAL(th_err) -th_request_parser_do_method(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed_out) -{ - th_str method; - size_t parsed = 0; - th_err err = th_request_parser_next_token(buffer, &method, ' ', &parsed); - if (err != TH_ERR_OK || parsed == 0) { - return err; - } - struct th_method_mapping* mm = th_method_mapping_find(method.ptr, method.len); - if (!mm) { - return TH_ERR_HTTP(TH_CODE_NOT_IMPLEMENTED); - } - th_request_set_method(request, mm->method); - *parsed_out = parsed; - parser->state = TH_REQUEST_PARSER_STATE_PATH; - return TH_ERR_OK; + return false; } -TH_LOCAL(th_err) -th_request_parser_do_uri_query(th_request* request, th_str path) +TH_PRIVATE(th_err) +th_multipart_parser_init(th_multipart_parser* parser, th_str body, th_str boundary) { - size_t pos = 0; - while (pos != th_str_npos) { - th_str key; - th_str value; - th_err err = th_request_parser_do_next_queryvar(path, &pos, &key, &value); - if (err != TH_ERR_OK) { - return err; - } - if (th_request_add_queryvar(request, key, value) != TH_ERR_OK) { - return TH_ERR_BAD_ALLOC; - } + parser->body = body; + parser->boundary = boundary; + bool last = false; + size_t eol = th_multipart_parser_find_eol(body, 0); + if (!th_multipart_parser_is_boundary_line(th_str_substr(body, 0, eol), boundary, &last) || last) { + parser->pos = th_str_npos; + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); } + parser->pos = eol + 2; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_request_parser_next_path_segment(th_str buffer, th_str* segment, size_t* parsed) +TH_PRIVATE(bool) +th_multipart_parser_done(const th_multipart_parser* parser) { - static const int uri_char[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32-47 (don't allow space, ") - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48-57 (0-9) - 1, 1, 0, 1, 0, 1, 1, // 58-64 (don't allow <,>) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 65-80 (A-P) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 81-90 (Q-Z) - 0, 0, 0, 0, 1, 0, // 91-96 (don't allow [, \, ], ^, `) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 97-112 (a-p) - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 113-122 (q-z) - 0, 0, 0, 1, 0, // 123-127 (don't allow {, |, }, DEL) - // implicitely set to 0 for 128-255 - }; - size_t i = 0; - while (i < buffer.len && buffer.ptr[i] != ' ' && buffer.ptr[i] != '?') { - if (uri_char[(unsigned char)buffer.ptr[i]] == 0) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - i++; - } - if (i == buffer.len) - return TH_ERR_OK; - *segment = th_str_substr(buffer, 0, i); - *parsed = i + 1; - return TH_ERR_OK; + return parser->pos == th_str_npos; } TH_LOCAL(th_err) -th_request_parser_do_path(th_request_parser* parser, th_request* request, th_str path, size_t* parsed) +th_multipart_parser_content_disposition(th_str header_value, th_str* out_name, th_str* out_filename) { - th_str segment; - size_t uri_parsed = 0; - th_err err = th_request_parser_next_path_segment(path, &segment, &uri_parsed); - if (err != TH_ERR_OK || uri_parsed == 0) - return err; - if ((err = th_request_set_uri_path(request, segment)) != TH_ERR_OK) - return err; - if (segment.ptr[segment.len] == '?') { // got a query - size_t query_parsed = 0; - err = th_request_parser_next_path_segment(th_str_substr(path, uri_parsed, th_str_npos), &segment, &query_parsed); - if (err != TH_ERR_OK || query_parsed == 0) - return err; - if ((err = th_request_set_uri_query(request, segment)) != TH_ERR_OK) + header_value = th_str_substr(header_value, th_str_find_first(header_value, 0, ';') + 1, th_str_npos); + while (!th_str_empty(header_value)) { + th_err err = TH_ERR_OK; + th_str name, value = th_str_make_empty(); + size_t parsed = 0; + if ((err = th_multipart_parser_next_header_param(header_value, &name, &value, &parsed)) != TH_ERR_OK) return err; - if ((err = th_request_parser_do_uri_query(request, segment)) != TH_ERR_OK) { - // If we can't parse the query, that's ok, we just ignore it - // restore the original state and continue - th_request_clear_queryvars(request); + header_value = th_str_substr(header_value, parsed, th_str_npos); + if (th_str_eq(name, TH_STR("name"))) { + *out_name = value; + } else if (th_str_eq(name, TH_STR("filename"))) { + *out_filename = value; } - uri_parsed += query_parsed; - } else { - if ((err = th_request_set_uri_query(request, TH_STR(""))) != TH_ERR_OK) - return err; } - *parsed = uri_parsed; - parser->state = TH_REQUEST_PARSER_STATE_VERSION; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_request_parser_do_version(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) +TH_LOCAL(size_t) +th_multipart_parser_find_boundary(th_str buffer, th_str boundary, bool* last, size_t* length) { - size_t n = th_str_find_first(buffer, 0, '\r'); - if (n == th_str_npos || n + 1 == buffer.len) - return TH_ERR_OK; - if (buffer.ptr[n + 1] != '\n') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - th_str version = th_str_substr(buffer, 0, n); - if (version.len != 8) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[0] != 'H') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[1] != 'T') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[2] != 'T') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[3] != 'P') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[4] != '/') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[5] != '1') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[6] != '.') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (version.ptr[7] < '0' || version.ptr[7] > '9') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - th_request_set_version(request, version.ptr[7] - '0'); - *parsed = n + 2; - parser->state = TH_REQUEST_PARSER_STATE_HEADERS; - return TH_ERR_OK; + TH_ASSERT(length && "length pointer must not be NULL"); + size_t pos = 0; + while (1) { + size_t eol = th_multipart_parser_find_eol(buffer, pos); + if (eol == th_str_npos) + return th_str_npos; + th_str line = th_str_substr(buffer, pos, eol - pos); + if (th_multipart_parser_is_boundary_line(line, boundary, last)) { + *length = line.len; + break; + } + pos = eol + 2; + } + return pos; } TH_LOCAL(th_err) -th_request_parse_handle_header(th_request_parser* parser, th_request* request, th_str name, th_str value) +th_multipart_parser_headers(th_str* buffer, th_str* content_disposition, th_str* content_type, size_t* content_len) { - char arena[1024] = {0}; - th_arena_allocator arena_allocator; - th_arena_allocator_init(&arena_allocator, arena, sizeof(arena), NULL); - th_string normalized_name; - th_string_init(&normalized_name, &arena_allocator.base); - if (th_string_set(&normalized_name, name) != TH_ERR_OK) { - // This can only happen if the name is too long - return TH_ERR_HTTP(TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE); - } - th_string_to_lower(&normalized_name); - th_header_id id = th_header_id_from_string(th_string_data(&normalized_name), th_string_len(&normalized_name)); - switch (id) { - case TH_HEADER_ID_COOKIE: - return th_request_parser_do_cookie_list(request, value); - case TH_HEADER_ID_CONTENT_LENGTH: { - unsigned int content_len = 0; - th_err err = th_str_to_uint(value, &content_len); - parser->content_len = content_len; - return err; - } - case TH_HEADER_ID_CONNECTION: - if (th_str_eq(value, TH_STR("close"))) { - request->close = true; - } else if (th_str_eq(value, TH_STR("keep-alive"))) { - request->close = false; + while (1) { + if (th_str_empty(*buffer)) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + size_t line_length = th_multipart_parser_find_eol(*buffer, 0); + if (line_length == th_str_npos) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + th_str line = th_str_substr(*buffer, 0, line_length); + if (th_str_empty(line)) { + *buffer = th_str_substr(*buffer, line_length + 2, th_str_npos); + return TH_ERR_OK; // end of headers } - return TH_ERR_OK; - case TH_HEADER_ID_CONTENT_TYPE: - if (th_str_eq(value, TH_STR("application/x-www-form-urlencoded"))) { - parser->body_encoding = TH_REQUEST_BODY_ENCODING_FORM_URL_ENCODED; - } else if (th_str_eq(th_str_substr(value, 0, 19), TH_STR("multipart/form-data"))) { - parser->body_encoding = TH_REQUEST_BODY_ENCODING_MULTIPART_FORM_DATA; + th_str header_name, header_value; + th_err err = TH_ERR_OK; + size_t colon = th_str_find_first(line, 0, ':'); + if (colon == th_str_npos) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + header_name = th_str_trim(th_str_substr(line, 0, colon)); + header_value = th_str_trim(th_str_substr(line, colon + 1, th_str_npos)); + if (th_str_eq(header_name, TH_STR("Content-Disposition"))) { + *content_disposition = header_value; + } else if (th_str_eq(header_name, TH_STR("Content-Length"))) { + unsigned int part_content_len = 0; + if ((err = th_str_to_uint(header_value, &part_content_len)) != TH_ERR_OK) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *content_len = part_content_len; + } else if (th_str_eq(header_name, TH_STR("Content-Type"))) { + *content_type = header_value; } - break; - default: - break; + *buffer = th_str_substr(*buffer, line_length + 2, th_str_npos); } - return th_request_add_header(request, th_string_view(&normalized_name), value); } TH_LOCAL(th_err) -th_request_parser_do_header(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) +th_multipart_parser_content( + th_multipart_parser* parser, th_str* buffer, size_t content_len, th_str* content, bool* last) { - size_t n = th_str_find_first(buffer, 0, '\r'); - if (n == th_str_npos || n + 1 == buffer.len) - return TH_ERR_OK; - if (buffer.ptr[n + 1] != '\n') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (n == 0) { - *parsed = 2; - if (parser->content_len == 0) { - th_request_set_body(request, th_str_make(&buffer.ptr[2], 0)); - parser->state = TH_REQUEST_PARSER_STATE_DONE; - } else { - if (request->method == TH_METHOD_GET || request->method == TH_METHOD_HEAD) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - parser->state = TH_REQUEST_PARSER_STATE_BODY; - } - return TH_ERR_OK; + if (content_len != th_str_npos) { + *content = th_str_substr(*buffer, 0, content_len); + *buffer = th_str_substr(*buffer, content_len, th_str_npos); + if (buffer->len < 2 || buffer->ptr[0] != '\r' || buffer->ptr[1] != '\n') + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + size_t line_end = th_multipart_parser_find_eol(*buffer, 2); + th_str line = th_str_substr(*buffer, 2, line_end == th_str_npos ? th_str_npos : line_end - 2); + if (!th_multipart_parser_is_boundary_line(line, parser->boundary, last)) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *buffer = th_str_substr(*buffer, content_len + parser->boundary.len + 2, th_str_npos); + } else { + // we don't have the content length, so we need to find the boundary + size_t boundary_length = 0; + size_t pos = th_multipart_parser_find_boundary(*buffer, parser->boundary, last, &boundary_length); + if (pos == th_str_npos) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + *content = th_str_substr(*buffer, 0, pos - 2); // -2 to remove the \r\n + *buffer = th_str_substr(*buffer, pos + boundary_length + 2, th_str_npos); } - size_t key_parsed = 0; - th_str key; - th_err err = TH_ERR_OK; - if ((err = th_request_parser_next_token(buffer, &key, ':', &key_parsed)) != TH_ERR_OK - || key_parsed == 0) - return err; - th_str value = th_str_substr(buffer, key_parsed, n - key_parsed); - if (!th_request_parser_is_printable_string(value)) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if ((err = th_request_parse_handle_header(parser, request, th_str_trim(key), th_str_trim(value))) - != TH_ERR_OK) - return err; - *parsed = n + 2; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_request_parser_do_multipart_form_data(th_request* request, th_str body) +TH_PRIVATE(th_err) +th_multipart_parser_next(th_multipart_parser* parser, th_multipart_part* part) { - th_str content_type = th_request_get_header(request, TH_STR("content-type")); - if (th_str_empty(content_type)) { + th_str buffer = th_str_substr(parser->body, parser->pos, th_str_npos); + size_t original_len = buffer.len; + + th_str content_disposition = th_str_make_empty(); + th_str content_type = th_str_make_empty(); + size_t content_len = th_str_npos; + th_err err = th_multipart_parser_headers(&buffer, &content_disposition, &content_type, &content_len); + if (err != TH_ERR_OK) { + parser->pos = th_str_npos; + return err; + } + if (th_str_empty(content_disposition)) { + parser->pos = th_str_npos; return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); } - th_err err = TH_ERR_OK; - th_str boundary = th_str_make_empty(); - if ((err = th_multipart_parser_boundary(content_type, &boundary)) != TH_ERR_OK) + + th_str name = th_str_make_empty(); + th_str filename = th_str_make_empty(); + if ((err = th_multipart_parser_content_disposition(content_disposition, &name, &filename)) != TH_ERR_OK) { + parser->pos = th_str_npos; return err; + } + if (th_str_empty(name)) { + parser->pos = th_str_npos; + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } - th_multipart_parser parser; - if ((err = th_multipart_parser_init(&parser, body, boundary)) != TH_ERR_OK) + bool last = false; + th_str content = th_str_make_empty(); + if ((err = th_multipart_parser_content(parser, &buffer, content_len, &content, &last)) != TH_ERR_OK) { + parser->pos = th_str_npos; return err; - while (!th_multipart_parser_done(&parser)) { - th_multipart_part part; - if ((err = th_multipart_parser_next(&parser, &part)) != TH_ERR_OK) - return err; - if (th_request_add_part(request, part.content, part.name, part.filename, part.content_type) != TH_ERR_OK) - return TH_ERR_BAD_ALLOC; } + if (last && !th_str_empty(buffer)) { + parser->pos = th_str_npos; + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + + part->name = name; + part->filename = filename; + part->content_type = content_type; + part->content = content; + + parser->pos = last ? th_str_npos : parser->pos + (original_len - buffer.len); return TH_ERR_OK; } +/* End of src/th_multipart_parser.c */ +/* Start of src/th_part.c */ -TH_LOCAL(th_err) -th_request_parser_do_body(th_request_parser* parser, th_request* request, th_str buffer, size_t* parsed) +TH_PRIVATE(void) +th_part_init(th_part* part, th_str content, th_allocator* allocator) { - if (buffer.len < parser->content_len) { - *parsed = 0; - return TH_ERR_OK; - } - // Got the whole body - th_str body = th_str_substr(buffer, 0, parser->content_len); - if (parser->body_encoding == TH_REQUEST_BODY_ENCODING_FORM_URL_ENCODED) { - th_err err = TH_ERR_OK; - if ((err = th_request_parser_do_bodyvars(request, body)) != TH_ERR_OK) - return err; - } else if (parser->body_encoding == TH_REQUEST_BODY_ENCODING_MULTIPART_FORM_DATA) { - th_err err = TH_ERR_OK; - if ((err = th_request_parser_do_multipart_form_data(request, body)) != TH_ERR_OK) - return err; - } - th_request_set_body(request, body); - *parsed = parser->content_len; - parser->state = TH_REQUEST_PARSER_STATE_DONE; - return TH_ERR_OK; + th_string_init(&part->name, allocator); + th_string_init(&part->filename, allocator); + th_string_init(&part->content_type, allocator); + part->content = content; } -TH_LOCAL(th_err) -th_request_parser_parse_next(th_request_parser* parser, th_request* request, th_str data, size_t* parsed) +TH_PRIVATE(void) +th_part_deinit(th_part* part) { - switch (parser->state) { - case TH_REQUEST_PARSER_STATE_METHOD: - return th_request_parser_do_method(parser, request, data, parsed); - case TH_REQUEST_PARSER_STATE_PATH: - return th_request_parser_do_path(parser, request, data, parsed); - case TH_REQUEST_PARSER_STATE_VERSION: - return th_request_parser_do_version(parser, request, data, parsed); - case TH_REQUEST_PARSER_STATE_HEADERS: - return th_request_parser_do_header(parser, request, data, parsed); - case TH_REQUEST_PARSER_STATE_BODY: - return th_request_parser_do_body(parser, request, data, parsed); - default: - *parsed = 0; - break; - } - return TH_ERR_OK; + th_string_deinit(&part->name); + th_string_deinit(&part->filename); + th_string_deinit(&part->content_type); } TH_PRIVATE(th_err) -th_request_parser_parse(th_request_parser* parser, th_request* request, th_str data, size_t* parsed) +th_part_set_name(th_part* part, th_str name) { - th_err err = TH_ERR_OK; - while (data.len > 0) { - size_t p = 0; - if ((err = th_request_parser_parse_next(parser, request, th_str_substr(data, p, data.len), &p)) != TH_ERR_OK) { - *parsed = p; - return err; - } - data.ptr += p; - data.len -= p; - *parsed += p; - if (p == 0 || parser->state == TH_REQUEST_PARSER_STATE_DONE) { - return TH_ERR_OK; - } - } - return TH_ERR_OK; + return th_string_set(&part->name, name); } -TH_PRIVATE(bool) -th_request_parser_header_done(th_request_parser* parser) +TH_PRIVATE(th_err) +th_part_set_filename(th_part* part, th_str filename) { - return parser->state > TH_REQUEST_PARSER_STATE_HEADERS; + return th_string_set(&part->filename, filename); } -TH_PRIVATE(bool) -th_request_parser_done(th_request_parser* parser) +TH_PRIVATE(th_err) +th_part_set_content_type(th_part* part, th_str content_type) { - return parser->state == TH_REQUEST_PARSER_STATE_DONE; + return th_string_set(&part->content_type, content_type); } -/* End of src/th_request_parser.c */ -/* Start of src/th_cookie_parser.c */ -TH_PRIVATE(void) -th_cookie_parser_init(th_cookie_parser* parser, th_str cookie_header) +// Public API + +TH_PUBLIC(const char*) +th_part_name(const th_part* part) { - parser->str = cookie_header; - parser->pos = cookie_header.len == 0 ? th_str_npos : 0; + return th_string_data(&part->name); } -TH_PRIVATE(bool) -th_cookie_parser_done(const th_cookie_parser* parser) +TH_PUBLIC(const char*) +th_part_filename(const th_part* part) { - return parser->pos == th_str_npos; + return th_string_data(&part->filename); } -/* RFC 2616 section 2.2 token: no CTLs, no separators - * "()<>@,;:\"/[]?={} \t". Used for cookie-name. */ -static const int th_cookie_parser_name_char[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 !"#$%&'()*+,-./ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 0123456789:;<=>? - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL - // implicitly 0 for 128-255 -}; - -/* RFC 6265 section 4.1.1 cookie-octet: %x21 / %x23-2B / %x2D-3A / %x3C-5B / - * %x5D-7E - printable ASCII minus space, DQUOTE, comma, semicolon, - * backslash. Used for a bare (unquoted) cookie-value. */ -static const int th_cookie_parser_value_char[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 32-47 !"#$%&'()*+,-./ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, // 48-63 0123456789:;<=>? - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL - // implicitly 0 for 128-255 -}; - -/* Same as th_cookie_parser_value_char, plus space - the quoted form exists - * so servers can embed characters a bare cookie-value can't (project - * decision, not literal RFC 6265). */ -static const int th_cookie_parser_quoted_value_char[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 32-47 !"#$%&'()*+,-./ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, // 48-63 0123456789:;<=>? - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 @ABCDEFGHIJKLMNO - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 80-95 PQRSTUVWXYZ[\]^_ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 `abcdefghijklmno - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112-127 pqrstuvwxyz{|}~ DEL - // implicitly 0 for 128-255 -}; - -TH_LOCAL(bool) -th_cookie_parser_is_space(char c) +TH_PUBLIC(const char*) +th_part_content_type(const th_part* part) { - return c == ' ' || c == '\t'; + return th_string_data(&part->content_type); } -TH_LOCAL(size_t) -th_cookie_parser_skip_space(th_str str, size_t pos) +TH_PUBLIC(th_buffer) +th_part_content(const th_part* part) { - while (pos < str.len && th_cookie_parser_is_space(str.ptr[pos])) { - pos++; - } - return pos; + return (th_buffer){part->content.ptr, part->content.len}; } +/* End of src/th_part.c */ +/* Start of src/th_request.c */ + + +#include +#include +#include +#include + +#undef TH_LOG_TAG +#define TH_LOG_TAG "request" + +/* hstr iterator begin */ -/* Scans a cookie-name: one or more token chars, followed by optional space. - * Leaves *pos on '=' (the caller checks it's actually there). */ -TH_LOCAL(th_err) -th_cookie_parser_scan_name(th_str str, size_t* pos, th_str* name) +TH_INLINE(bool) +th_hstr_iter_next(th_iter* it) { - size_t start = *pos; - while (*pos < str.len && th_cookie_parser_name_char[(unsigned char)str.ptr[*pos]]) { - (*pos)++; - } - if (*pos == start) { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - *name = th_str_substr(str, start, *pos - start); - *pos = th_cookie_parser_skip_space(str, *pos); - if (*pos >= str.len || str.ptr[*pos] != '=') { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - return TH_ERR_OK; + it->ptr = ((const th_hstr_pair*)it->ptr) + 1; + return it->ptr < it->end; } -/* Scans a quoted cookie-value, starting at the opening DQUOTE. */ -TH_LOCAL(th_err) -th_cookie_parser_scan_quoted_value(th_str str, size_t* pos, th_str* value) +TH_INLINE(const char*) +th_hstr_iter_key(const th_iter* it) { - size_t start = *pos + 1; - size_t i = start; - while (i < str.len && th_cookie_parser_quoted_value_char[(unsigned char)str.ptr[i]]) { - i++; - } - if (i >= str.len || str.ptr[i] != '"') { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - *value = th_str_substr(str, start, i - start); - *pos = i + 1; - return TH_ERR_OK; + return th_string_data(&((const th_hstr_pair*)it->ptr)->key); } -/* Scans a bare (unquoted) cookie-value: zero or more cookie-octets. */ -TH_LOCAL(th_err) -th_cookie_parser_scan_bare_value(th_str str, size_t* pos, th_str* value) +TH_INLINE(const void*) +th_hstr_iter_val(const th_iter* it) { - size_t start = *pos; - while (*pos < str.len && th_cookie_parser_value_char[(unsigned char)str.ptr[*pos]]) { - (*pos)++; - } - *value = th_str_substr(str, start, *pos - start); - return TH_ERR_OK; + return th_string_data(&((const th_hstr_pair*)it->ptr)->value); } -TH_LOCAL(th_err) -th_cookie_parser_scan_value(th_str str, size_t* pos, th_str* value) +static th_iter_methods th_hstr_iter_methods = { + .next = th_hstr_iter_next, + .key = th_hstr_iter_key, + .val = th_hstr_iter_val, +}; + +// hstr iterator end +// part iterator begin + +TH_INLINE(bool) +th_part_iter_next(th_iter* it) { - if (*pos < str.len && str.ptr[*pos] == '"') { - return th_cookie_parser_scan_quoted_value(str, pos, value); - } - return th_cookie_parser_scan_bare_value(str, pos, value); + it->ptr = ((const th_part*)it->ptr) + 1; + return it->ptr < it->end; } -/* After a pair, only space may remain before ';' or the end of input - any - * other byte (e.g. a stray octet the value scan stopped on) is malformed. */ -TH_LOCAL(th_err) -th_cookie_parser_scan_pair_end(th_str str, size_t* pos) +TH_INLINE(const char*) +th_part_iter_key(const th_iter* it) { - *pos = th_cookie_parser_skip_space(str, *pos); - if (*pos == str.len) { - return TH_ERR_OK; - } - if (str.ptr[*pos] != ';') { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - (*pos)++; - return TH_ERR_OK; + return th_string_data(&((const th_part*)it->ptr)->name); } -TH_PRIVATE(th_err) -th_cookie_parser_next(th_cookie_parser* parser, th_str* key, th_str* value) +TH_INLINE(const void*) +th_part_iter_val(const th_iter* it) { - size_t pos = th_cookie_parser_skip_space(parser->str, parser->pos); + return it->ptr; +} - th_str name; - th_err err = th_cookie_parser_scan_name(parser->str, &pos, &name); - if (err != TH_ERR_OK) { - parser->pos = th_str_npos; - return err; - } - pos = th_cookie_parser_skip_space(parser->str, pos + 1); // skip '=' and space +static th_iter_methods th_part_iter_methods = { + .next = th_part_iter_next, + .key = th_part_iter_key, + .val = th_part_iter_val, +}; - th_str raw_value; - if ((err = th_cookie_parser_scan_value(parser->str, &pos, &raw_value)) != TH_ERR_OK) { - parser->pos = th_str_npos; - return err; - } +// part iterator end - if ((err = th_cookie_parser_scan_pair_end(parser->str, &pos)) != TH_ERR_OK) { - parser->pos = th_str_npos; +TH_LOCAL(th_err) +th_request_map_store(th_request* request, th_hstr_vec* vec, th_str key, th_str value) +{ + th_err err = TH_ERR_OK; + th_string k; + th_string v; + if ((err = th_string_init_with(&k, key, request->allocator)) != TH_ERR_OK) return err; - } - - parser->pos = pos == parser->str.len ? th_str_npos : pos; - *key = name; - *value = raw_value; + if ((err = th_string_init_with(&v, value, request->allocator)) != TH_ERR_OK) + goto cleanup_key; + if ((err = th_hstr_vec_push_back(vec, (th_hstr_pair){k, v})) != TH_ERR_OK) + goto cleanup_value; return TH_ERR_OK; +cleanup_value: + th_string_deinit(&v); +cleanup_key: + th_string_deinit(&k); + return err; } -/* End of src/th_cookie_parser.c */ -/* Start of src/th_multipart_parser.c */ - TH_LOCAL(th_err) -th_multipart_parser_next_header_param(th_str buffer, th_str* out_name, th_str* out_value, size_t* out_parsed) +th_request_map_store_url_decoded(th_request* request, th_hstr_vec* vec, th_str key, th_str value, th_url_decode_type type) { - buffer = th_str_substr(buffer, th_str_find_first_not(buffer, 0, ' '), th_str_npos); - size_t eq = th_str_find_first_of(buffer, 0, "=; "); - if (eq == th_str_npos || buffer.ptr[eq] == ';') { - *out_name = th_str_substr(buffer, 0, eq); - *out_value = th_str_make_empty(); - *out_parsed = eq == th_str_npos ? buffer.len : eq + 1; - return TH_ERR_OK; - } - if (buffer.ptr[eq] == ' ') // spaces are not allowed - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *out_name = th_str_substr(buffer, 0, eq); - size_t parsed = eq + 1; - buffer = th_str_substr(buffer, eq + 1, th_str_npos); - if (th_str_empty(buffer)) // equals sign must be followed by a value - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - if (buffer.ptr[0] == '"') { - size_t end = th_str_find_first(buffer, 1, '"'); - if (end == th_str_npos) // no closing quote - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *out_value = th_str_substr(buffer, 1, end - 1); - parsed += (end == th_str_npos ? buffer.len : end + 1); - } else { - size_t end = th_str_find_first_of(buffer, 0, "; "); - if (end != th_str_npos && buffer.ptr[end] == ' ') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *out_value = th_str_substr(buffer, 0, end); - parsed += (end == th_str_npos ? buffer.len : end + 1); - } - *out_parsed = parsed; + th_err err = TH_ERR_OK; + th_string k; + th_string v; + th_string_init(&k, request->allocator); + th_string_init(&v, request->allocator); + if ((err = th_url_decode_string(key, &k, type)) != TH_ERR_OK) + goto cleanup; + if ((err = th_url_decode_string(value, &v, type)) != TH_ERR_OK) + goto cleanup; + if ((err = th_hstr_vec_push_back(vec, (th_hstr_pair){k, v})) != TH_ERR_OK) + goto cleanup; return TH_ERR_OK; +cleanup: + th_string_deinit(&v); + th_string_deinit(&k); + return err; } TH_PRIVATE(th_err) -th_multipart_parser_boundary(th_str content_type, th_str* boundary) +th_request_add_cookie(th_request* request, th_str key, th_str value) { - content_type = th_str_substr(content_type, th_str_find_first(content_type, 0, ';') + 1, th_str_npos); - while (!th_str_empty(content_type)) { - th_str name, value = th_str_make_empty(); - size_t parsed = 0; - th_err err = TH_ERR_OK; - if ((err = th_multipart_parser_next_header_param(content_type, &name, &value, &parsed)) != TH_ERR_OK) - return err; - content_type = th_str_substr(content_type, parsed, th_str_npos); - if (th_str_eq(name, TH_STR("boundary"))) { - if (th_str_empty(value)) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *boundary = value; - return TH_ERR_OK; - } - } - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + return th_request_map_store(request, &request->cookies, key, value); } -TH_LOCAL(size_t) -th_multipart_parser_find_eol(th_str buffer, size_t start) +TH_PRIVATE(th_err) +th_request_add_header(th_request* request, th_str key, th_str value) { - if (start + 1 >= buffer.len) - return th_str_npos; - th_str searchable = th_str_substr(buffer, 0, buffer.len - 1); - size_t pos = start; - while (pos != th_str_npos) { - pos = th_str_find_first(searchable, pos, '\r'); - if (pos == th_str_npos) - return th_str_npos; - if (buffer.ptr[pos + 1] == '\n') - return pos; - pos++; - } - return th_str_npos; + return th_request_map_store(request, &request->headers, key, value); } -TH_LOCAL(bool) -th_multipart_parser_is_boundary_line(th_str line, th_str boundary, bool* last) +TH_PRIVATE(th_err) +th_request_add_part(th_request* request, th_str content, th_str name, th_str filename, th_str content_type) { - *last = false; - if (line.len < boundary.len + 2) - return false; - if (line.ptr[0] != '-' || line.ptr[1] != '-') - return false; - if (th_str_eq(th_str_substr(line, 2, boundary.len), boundary)) { - if (line.len == boundary.len + 2) - return true; - if (line.ptr[boundary.len + 2] == '-' && line.ptr[boundary.len + 3] == '-') { - *last = true; - return true; - } - } - return false; + th_part part; + th_part_init(&part, content, request->allocator); + th_err err = TH_ERR_OK; + if ((err = th_part_set_name(&part, name)) != TH_ERR_OK) + goto cleanup_part; + if ((err = th_part_set_filename(&part, filename)) != TH_ERR_OK) + goto cleanup_part; + if ((err = th_part_set_content_type(&part, content_type)) != TH_ERR_OK) + goto cleanup_part; + if ((err = th_part_vec_push_back(&request->parts, part)) != TH_ERR_OK) + goto cleanup_part; + return TH_ERR_OK; +cleanup_part: + th_part_deinit(&part); + return err; +} + +TH_PRIVATE(th_err) +th_request_add_queryvar(th_request* request, th_str key, th_str value) +{ + return th_request_map_store_url_decoded(request, &request->queryvars, key, value, TH_URL_DECODE_TYPE_QUERY); } TH_PRIVATE(th_err) -th_multipart_parser_init(th_multipart_parser* parser, th_str body, th_str boundary) +th_request_add_formvar(th_request* request, th_str key, th_str value) { - parser->body = body; - parser->boundary = boundary; - bool last = false; - size_t eol = th_multipart_parser_find_eol(body, 0); - if (!th_multipart_parser_is_boundary_line(th_str_substr(body, 0, eol), boundary, &last) || last) { - parser->pos = th_str_npos; - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - parser->pos = eol + 2; - return TH_ERR_OK; + return th_request_map_store_url_decoded(request, &request->formvars, key, value, TH_URL_DECODE_TYPE_QUERY); } -TH_PRIVATE(bool) -th_multipart_parser_done(const th_multipart_parser* parser) +TH_PRIVATE(th_err) +th_request_add_pathvar(th_request* request, th_str key, th_str value) { - return parser->pos == th_str_npos; + return th_request_map_store(request, &request->pathvars, key, value); } -TH_LOCAL(th_err) -th_multipart_parser_content_disposition(th_str header_value, th_str* out_name, th_str* out_filename) +TH_PRIVATE(th_err) +th_request_set_uri_path(th_request* request, th_str path) { - header_value = th_str_substr(header_value, th_str_find_first(header_value, 0, ';') + 1, th_str_npos); - while (!th_str_empty(header_value)) { - th_err err = TH_ERR_OK; - th_str name, value = th_str_make_empty(); - size_t parsed = 0; - if ((err = th_multipart_parser_next_header_param(header_value, &name, &value, &parsed)) != TH_ERR_OK) - return err; - header_value = th_str_substr(header_value, parsed, th_str_npos); - if (th_str_eq(name, TH_STR("name"))) { - *out_name = value; - } else if (th_str_eq(name, TH_STR("filename"))) { - *out_filename = value; - } - } - return TH_ERR_OK; + return th_string_set(&request->uri_path, path); } -TH_LOCAL(size_t) -th_multipart_parser_find_boundary(th_str buffer, th_str boundary, bool* last, size_t* length) +TH_PRIVATE(th_err) +th_request_set_uri_query(th_request* request, th_str query) { - TH_ASSERT(length && "length pointer must not be NULL"); - size_t pos = 0; - while (1) { - size_t eol = th_multipart_parser_find_eol(buffer, pos); - if (eol == th_str_npos) - return th_str_npos; - th_str line = th_str_substr(buffer, pos, eol - pos); - if (th_multipart_parser_is_boundary_line(line, boundary, last)) { - *length = line.len; - break; - } - pos = eol + 2; - } - return pos; + return th_string_set(&request->uri_query, query); } -TH_LOCAL(th_err) -th_multipart_parser_headers(th_str* buffer, th_str* content_disposition, th_str* content_type, size_t* content_len) +TH_PRIVATE(void) +th_request_set_version(th_request* request, int version) { - while (1) { - if (th_str_empty(*buffer)) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - size_t line_length = th_multipart_parser_find_eol(*buffer, 0); - if (line_length == th_str_npos) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - th_str line = th_str_substr(*buffer, 0, line_length); - if (th_str_empty(line)) { - *buffer = th_str_substr(*buffer, line_length + 2, th_str_npos); - return TH_ERR_OK; // end of headers - } - th_str header_name, header_value; - th_err err = TH_ERR_OK; - size_t colon = th_str_find_first(line, 0, ':'); - if (colon == th_str_npos) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - header_name = th_str_trim(th_str_substr(line, 0, colon)); - header_value = th_str_trim(th_str_substr(line, colon + 1, th_str_npos)); - if (th_str_eq(header_name, TH_STR("Content-Disposition"))) { - *content_disposition = header_value; - } else if (th_str_eq(header_name, TH_STR("Content-Length"))) { - unsigned int part_content_len = 0; - if ((err = th_str_to_uint(header_value, &part_content_len)) != TH_ERR_OK) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *content_len = part_content_len; - } else if (th_str_eq(header_name, TH_STR("Content-Type"))) { - *content_type = header_value; - } - *buffer = th_str_substr(*buffer, line_length + 2, th_str_npos); - } + request->version = version; } -TH_LOCAL(th_err) -th_multipart_parser_content( - th_multipart_parser* parser, th_str* buffer, size_t content_len, th_str* content, bool* last) +TH_PRIVATE(void) +th_request_set_method(th_request* request, th_method method) { - if (content_len != th_str_npos) { - *content = th_str_substr(*buffer, 0, content_len); - *buffer = th_str_substr(*buffer, content_len, th_str_npos); - if (buffer->len < 2 || buffer->ptr[0] != '\r' || buffer->ptr[1] != '\n') - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - size_t line_end = th_multipart_parser_find_eol(*buffer, 2); - th_str line = th_str_substr(*buffer, 2, line_end == th_str_npos ? th_str_npos : line_end - 2); - if (!th_multipart_parser_is_boundary_line(line, parser->boundary, last)) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *buffer = th_str_substr(*buffer, content_len + parser->boundary.len + 2, th_str_npos); - } else { - // we don't have the content length, so we need to find the boundary - size_t boundary_length = 0; - size_t pos = th_multipart_parser_find_boundary(*buffer, parser->boundary, last, &boundary_length); - if (pos == th_str_npos) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - *content = th_str_substr(*buffer, 0, pos - 2); // -2 to remove the \r\n - *buffer = th_str_substr(*buffer, pos + boundary_length + 2, th_str_npos); - } - return TH_ERR_OK; + request->method = method; } -TH_PRIVATE(th_err) -th_multipart_parser_next(th_multipart_parser* parser, th_multipart_part* part) +TH_PRIVATE(void) +th_request_clear_queryvars(th_request* request) { - th_str buffer = th_str_substr(parser->body, parser->pos, th_str_npos); - size_t original_len = buffer.len; + th_hstr_vec_clear(&request->queryvars); + // TODO: clear heap strings +} - th_str content_disposition = th_str_make_empty(); - th_str content_type = th_str_make_empty(); - size_t content_len = th_str_npos; - th_err err = th_multipart_parser_headers(&buffer, &content_disposition, &content_type, &content_len); - if (err != TH_ERR_OK) { - parser->pos = th_str_npos; - return err; - } - if (th_str_empty(content_disposition)) { - parser->pos = th_str_npos; - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } +TH_PRIVATE(void) +th_request_set_body(th_request* request, th_str body) +{ + request->body = body; +} - th_str name = th_str_make_empty(); - th_str filename = th_str_make_empty(); - if ((err = th_multipart_parser_content_disposition(content_disposition, &name, &filename)) != TH_ERR_OK) { - parser->pos = th_str_npos; - return err; - } - if (th_str_empty(name)) { - parser->pos = th_str_npos; - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } +TH_PRIVATE(void) +th_request_init(th_request* request, th_allocator* allocator) +{ + request->allocator = allocator ? allocator : th_default_allocator_get(); + th_string_init(&request->uri_path, request->allocator); + th_string_init(&request->uri_query, request->allocator); + th_part_vec_init(&request->parts, request->allocator); + th_hstr_vec_init(&request->cookies, request->allocator); + th_hstr_vec_init(&request->headers, request->allocator); + th_hstr_vec_init(&request->queryvars, request->allocator); + th_hstr_vec_init(&request->formvars, request->allocator); + th_hstr_vec_init(&request->pathvars, request->allocator); + request->body = (th_str){0}; + request->version = 0; + request->close = false; +} - bool last = false; - th_str content = th_str_make_empty(); - if ((err = th_multipart_parser_content(parser, &buffer, content_len, &content, &last)) != TH_ERR_OK) { - parser->pos = th_str_npos; - return err; - } - if (last && !th_str_empty(buffer)) { - parser->pos = th_str_npos; - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } +TH_PRIVATE(void) +th_request_deinit(th_request* request) +{ + th_string_deinit(&request->uri_path); + th_string_deinit(&request->uri_query); + th_part_vec_deinit(&request->parts); + th_hstr_vec_deinit(&request->cookies); + th_hstr_vec_deinit(&request->headers); + th_hstr_vec_deinit(&request->queryvars); + th_hstr_vec_deinit(&request->formvars); + th_hstr_vec_deinit(&request->pathvars); +} - part->name = name; - part->filename = filename; - part->content_type = content_type; - part->content = content; +TH_PRIVATE(void) +th_request_reset(th_request* request) +{ + th_string_clear(&request->uri_path); + th_string_clear(&request->uri_query); + th_part_vec_clear(&request->parts); + th_hstr_vec_clear(&request->cookies); + th_hstr_vec_clear(&request->headers); + th_hstr_vec_clear(&request->queryvars); + th_hstr_vec_clear(&request->formvars); + th_hstr_vec_clear(&request->pathvars); + request->body = (th_str){0}; + request->version = 0; + request->close = false; +} - parser->pos = last ? th_str_npos : parser->pos + (original_len - buffer.len); - return TH_ERR_OK; +TH_LOCAL(th_str) +th_request_vec_get(th_hstr_vec* vec, th_str key) +{ + size_t num = th_hstr_vec_size(vec); + for (size_t i = 0; i < num; i++) { + if (th_string_eq(&vec->data[i].key, key)) + return th_string_view(&vec->data[i].value); + } + return TH_STR(""); } -/* End of src/th_multipart_parser.c */ -/* Start of src/th_part.c */ -TH_PRIVATE(void) -th_part_init(th_part* part, th_str content, th_allocator* allocator) +TH_PRIVATE(th_str) +th_request_get_header(th_request* request, th_str key) { - th_string_init(&part->name, allocator); - th_string_init(&part->filename, allocator); - th_string_init(&part->content_type, allocator); - part->content = content; + return th_request_vec_get(&request->headers, key); } -TH_PRIVATE(void) -th_part_deinit(th_part* part) +TH_PRIVATE(th_str) +th_request_get_pathvar(th_request* request, th_str key) { - th_string_deinit(&part->name); - th_string_deinit(&part->filename); - th_string_deinit(&part->content_type); + return th_request_vec_get(&request->pathvars, key); } -TH_PRIVATE(th_err) -th_part_set_name(th_part* part, th_str name) +TH_PRIVATE(th_str) +th_request_get_queryvar(th_request* request, th_str key) { - return th_string_set(&part->name, name); + return th_request_vec_get(&request->queryvars, key); } -TH_PRIVATE(th_err) -th_part_set_filename(th_part* part, th_str filename) +TH_PRIVATE(th_str) +th_request_get_formvar(th_request* request, th_str key) { - return th_string_set(&part->filename, filename); + return th_request_vec_get(&request->formvars, key); } -TH_PRIVATE(th_err) -th_part_set_content_type(th_part* part, th_str content_type) +TH_PRIVATE(th_part*) +th_request_get_part(th_request* request, th_str key) { - return th_string_set(&part->content_type, content_type); + size_t num = th_part_vec_size(&request->parts); + for (size_t i = 0; i < num; i++) { + if (th_string_eq(&request->parts.data[i].name, key)) + return th_part_vec_at(&request->parts, i); + } + return NULL; } -// Public API +/* Public iterator API begin */ -TH_PUBLIC(const char*) -th_part_name(const th_part* part) +TH_PUBLIC(bool) +th_next(th_iter* it) { - return th_string_data(&part->name); + return it->methods->next(it); } TH_PUBLIC(const char*) -th_part_filename(const th_part* part) +th_key(const th_iter* it) { - return th_string_data(&part->filename); + return it->methods->key(it); } -TH_PUBLIC(const char*) -th_part_content_type(const th_part* part) +TH_PUBLIC(const void*) +th_val(const th_iter* it) { - return th_string_data(&part->content_type); + return it->methods->val(it); } -TH_PUBLIC(th_buffer) -th_part_content(const th_part* part) +TH_PUBLIC(const char*) +th_cval(const th_iter* it) { - return (th_buffer){part->content.ptr, part->content.len}; + return (const char*)it->methods->val(it); } -/* End of src/th_part.c */ -/* Start of src/th_request.c */ - - -#include -#include -#include -#include - -#undef TH_LOG_TAG -#define TH_LOG_TAG "request" -/* hstr iterator begin */ +/* Public iterator API end */ +/* Public request API begin */ -TH_INLINE(bool) -th_hstr_iter_next(th_iter* it) +TH_PUBLIC(const char*) +th_get_path(const th_request* req) { - it->ptr = ((const th_hstr_pair*)it->ptr) + 1; - return it->ptr < it->end; + return th_string_data(&req->uri_path); } -TH_INLINE(const char*) -th_hstr_iter_key(const th_iter* it) +TH_PUBLIC(const char*) +th_get_query(const th_request* req) { - return th_string_data(&((const th_hstr_pair*)it->ptr)->key); + return th_string_data(&req->uri_query); } -TH_INLINE(const void*) -th_hstr_iter_val(const th_iter* it) +TH_PUBLIC(th_buffer) +th_get_body(const th_request* req) { - return th_string_data(&((const th_hstr_pair*)it->ptr)->value); + return (th_buffer){req->body.ptr, req->body.len}; } -static th_iter_methods th_hstr_iter_methods = { - .next = th_hstr_iter_next, - .key = th_hstr_iter_key, - .val = th_hstr_iter_val, -}; - -// hstr iterator end -// part iterator begin - -TH_INLINE(bool) -th_part_iter_next(th_iter* it) +TH_PUBLIC(th_method) +th_get_method(const th_request* req) { - it->ptr = ((const th_part*)it->ptr) + 1; - return it->ptr < it->end; + return req->method; } -TH_INLINE(const char*) -th_part_iter_key(const th_iter* it) +TH_PUBLIC(th_prot_version) +th_get_version(const th_request* req) { - return th_string_data(&((const th_part*)it->ptr)->name); + return (th_prot_version)req->version; } -TH_INLINE(const void*) -th_part_iter_val(const th_iter* it) +TH_PUBLIC(const char*) +th_find_header(const th_request* req, const char* key) { - return it->ptr; + size_t num = th_hstr_vec_size(&req->headers); + for (size_t i = 0; i < num; i++) { + if (strcmp(key, th_string_data(&req->headers.data[i].key)) == 0) { + return th_string_data(&req->headers.data[i].value); + } + } + return NULL; } -static th_iter_methods th_part_iter_methods = { - .next = th_part_iter_next, - .key = th_part_iter_key, - .val = th_part_iter_val, -}; - -// part iterator end - -TH_LOCAL(th_err) -th_request_map_store(th_request* request, th_hstr_vec* vec, th_str key, th_str value) +TH_PUBLIC(th_iter) +th_header_iter(const th_request* req) { - th_err err = TH_ERR_OK; - th_string k; - th_string v; - if ((err = th_string_init_with(&k, key, request->allocator)) != TH_ERR_OK) - return err; - if ((err = th_string_init_with(&v, value, request->allocator)) != TH_ERR_OK) - goto cleanup_key; - if ((err = th_hstr_vec_push_back(vec, (th_hstr_pair){k, v})) != TH_ERR_OK) - goto cleanup_value; - return TH_ERR_OK; -cleanup_value: - th_string_deinit(&v); -cleanup_key: - th_string_deinit(&k); - return err; + return (th_iter){ + .methods = &th_hstr_iter_methods, + .ptr = req->headers.data, + .end = req->headers.data + req->headers.size, + }; } -TH_LOCAL(th_err) -th_request_map_store_url_decoded(th_request* request, th_hstr_vec* vec, th_str key, th_str value, th_url_decode_type type) +TH_PUBLIC(const char*) +th_find_cookie(const th_request* req, const char* key) { - th_err err = TH_ERR_OK; - th_string k; - th_string v; - th_string_init(&k, request->allocator); - th_string_init(&v, request->allocator); - if ((err = th_url_decode_string(key, &k, type)) != TH_ERR_OK) - goto cleanup; - if ((err = th_url_decode_string(value, &v, type)) != TH_ERR_OK) - goto cleanup; - if ((err = th_hstr_vec_push_back(vec, (th_hstr_pair){k, v})) != TH_ERR_OK) - goto cleanup; - return TH_ERR_OK; -cleanup: - th_string_deinit(&v); - th_string_deinit(&k); - return err; + size_t num = th_hstr_vec_size(&req->cookies); + for (size_t i = 0; i < num; i++) { + if (strcmp(key, th_string_data(&req->cookies.data[i].key)) == 0) { + return th_string_data(&req->cookies.data[i].value); + } + } + return NULL; } -TH_PRIVATE(th_err) -th_request_add_cookie(th_request* request, th_str key, th_str value) +TH_PUBLIC(th_iter) +th_cookie_iter(const th_request* req) { - return th_request_map_store(request, &request->cookies, key, value); + return (th_iter){ + .methods = &th_hstr_iter_methods, + .ptr = req->cookies.data, + .end = req->cookies.data + req->cookies.size, + }; } -TH_PRIVATE(th_err) -th_request_add_header(th_request* request, th_str key, th_str value) +TH_PUBLIC(const char*) +th_find_queryvar(const th_request* req, const char* key) { - return th_request_map_store(request, &request->headers, key, value); + size_t num = th_hstr_vec_size(&req->queryvars); + for (size_t i = 0; i < num; i++) { + if (strcmp(key, th_string_data(&req->queryvars.data[i].key)) == 0) { + return th_string_data(&req->queryvars.data[i].value); + } + } + return NULL; } -TH_PRIVATE(th_err) -th_request_add_part(th_request* request, th_str content, th_str name, th_str filename, th_str content_type) +TH_PUBLIC(th_iter) +th_queryvar_iter(const th_request* req) { - th_part part; - th_part_init(&part, content, request->allocator); - th_err err = TH_ERR_OK; - if ((err = th_part_set_name(&part, name)) != TH_ERR_OK) - goto cleanup_part; - if ((err = th_part_set_filename(&part, filename)) != TH_ERR_OK) - goto cleanup_part; - if ((err = th_part_set_content_type(&part, content_type)) != TH_ERR_OK) - goto cleanup_part; - if ((err = th_part_vec_push_back(&request->parts, part)) != TH_ERR_OK) - goto cleanup_part; - return TH_ERR_OK; -cleanup_part: - th_part_deinit(&part); - return err; + return (th_iter){ + .methods = &th_hstr_iter_methods, + .ptr = req->queryvars.data, + .end = req->queryvars.data + req->queryvars.size, + }; } -TH_PRIVATE(th_err) -th_request_add_queryvar(th_request* request, th_str key, th_str value) +TH_PUBLIC(const char*) +th_find_formvar(const th_request* req, const char* key) { - return th_request_map_store_url_decoded(request, &request->queryvars, key, value, TH_URL_DECODE_TYPE_QUERY); + size_t num = th_hstr_vec_size(&req->formvars); + for (size_t i = 0; i < num; i++) { + if (strcmp(key, th_string_data(&req->formvars.data[i].key)) == 0) { + return th_string_data(&req->formvars.data[i].value); + } + } + return NULL; } -TH_PRIVATE(th_err) -th_request_add_formvar(th_request* request, th_str key, th_str value) +TH_PUBLIC(th_iter) +th_formvar_iter(const th_request* req) { - return th_request_map_store_url_decoded(request, &request->formvars, key, value, TH_URL_DECODE_TYPE_QUERY); + return (th_iter){ + .methods = &th_hstr_iter_methods, + .ptr = req->formvars.data, + .end = req->formvars.data + req->formvars.size, + }; } -TH_PRIVATE(th_err) -th_request_add_pathvar(th_request* request, th_str key, th_str value) +TH_PUBLIC(const char*) +th_find_pathvar(const th_request* req, const char* key) { - return th_request_map_store(request, &request->pathvars, key, value); + size_t num = th_hstr_vec_size(&req->pathvars); + for (size_t i = 0; i < num; i++) { + if (strcmp(key, th_string_data(&req->pathvars.data[i].key)) == 0) { + return th_string_data(&req->pathvars.data[i].value); + } + } + return NULL; } -TH_PRIVATE(th_err) -th_request_set_uri_path(th_request* request, th_str path) +TH_PUBLIC(th_iter) +th_pathvar_iter(const th_request* req) { - return th_string_set(&request->uri_path, path); + return (th_iter){ + .methods = &th_hstr_iter_methods, + .ptr = req->pathvars.data, + .end = req->pathvars.data + req->pathvars.size, + }; } -TH_PRIVATE(th_err) -th_request_set_uri_query(th_request* request, th_str query) +TH_PUBLIC(const th_part*) +th_find_part(const th_request* req, const char* name) { - return th_string_set(&request->uri_query, query); + size_t num = th_part_vec_size(&req->parts); + for (size_t i = 0; i < num; i++) { + if (strcmp(name, th_string_data(&req->parts.data[i].name)) == 0) { + return th_part_vec_cat(&req->parts, i); + } + } + return NULL; } -TH_PRIVATE(void) -th_request_set_version(th_request* request, int version) +TH_PUBLIC(th_iter) +th_part_iter(const th_request* req) { - request->version = version; + return (th_iter){ + .methods = &th_part_iter_methods, + .ptr = req->parts.data, + .end = req->parts.data + req->parts.size, + }; } -TH_PRIVATE(void) -th_request_set_method(th_request* request, th_method method) -{ - request->method = method; -} +/* Public request API end */ +/* End of src/th_request.c */ +/* Start of src/th_response.c */ -TH_PRIVATE(void) -th_request_clear_queryvars(th_request* request) -{ - th_hstr_vec_clear(&request->queryvars); - // TODO: clear heap strings -} + +#include +#include +#include +#include +#include +#include + +#undef TH_LOG_TAG +#define TH_LOG_TAG "response" + +/* th_response implementation begin */ TH_PRIVATE(void) -th_request_set_body(th_request* request, th_str body) +th_response_init(th_response* response, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) { - request->body = body; + allocator = allocator ? allocator : th_default_allocator_get(); + th_string_init(&response->headers, allocator); + th_string_init(&response->body, allocator); + response->iov[0] = (th_iov){0}; + response->iov[1] = (th_iov){0}; + response->iov[2] = (th_iov){0}; + response->allocator = allocator; + response->dir_mgr = dir_mgr; + response->fcache = fcache; + response->fcache_entry = NULL; + response->file_len = 0; + response->code = TH_CODE_OK; + memset(response->header_is_set, 0, sizeof(response->header_is_set)); + response->is_file = false; + response->only_headers = false; } TH_PRIVATE(void) -th_request_init(th_request* request, th_allocator* allocator) +th_response_deinit(th_response* response) { - request->allocator = allocator ? allocator : th_default_allocator_get(); - th_string_init(&request->uri_path, request->allocator); - th_string_init(&request->uri_query, request->allocator); - th_part_vec_init(&request->parts, request->allocator); - th_hstr_vec_init(&request->cookies, request->allocator); - th_hstr_vec_init(&request->headers, request->allocator); - th_hstr_vec_init(&request->queryvars, request->allocator); - th_hstr_vec_init(&request->formvars, request->allocator); - th_hstr_vec_init(&request->pathvars, request->allocator); - request->body = (th_str){0}; - request->version = 0; - request->close = false; + th_string_deinit(&response->headers); + th_string_deinit(&response->body); + if (response->fcache_entry) { + th_fcache_entry_unref(response->fcache_entry); + response->fcache_entry = NULL; + } } TH_PRIVATE(void) -th_request_deinit(th_request* request) +th_response_reset(th_response* response) { - th_string_deinit(&request->uri_path); - th_string_deinit(&request->uri_query); - th_part_vec_deinit(&request->parts); - th_hstr_vec_deinit(&request->cookies); - th_hstr_vec_deinit(&request->headers); - th_hstr_vec_deinit(&request->queryvars); - th_hstr_vec_deinit(&request->formvars); - th_hstr_vec_deinit(&request->pathvars); + th_string_clear(&response->headers); + th_string_clear(&response->body); + response->iov[0] = (th_iov){0}; + response->iov[1] = (th_iov){0}; + response->iov[2] = (th_iov){0}; + if (response->fcache_entry) { + th_fcache_entry_unref(response->fcache_entry); + response->fcache_entry = NULL; + } + response->file_len = 0; + response->code = TH_CODE_OK; + memset(response->header_is_set, 0, sizeof(response->header_is_set)); + response->is_file = false; + response->only_headers = false; } TH_PRIVATE(void) -th_request_reset(th_request* request) +th_response_set_code(th_response* response, th_code code) { - th_string_clear(&request->uri_path); - th_string_clear(&request->uri_query); - th_part_vec_clear(&request->parts); - th_hstr_vec_clear(&request->cookies); - th_hstr_vec_clear(&request->headers); - th_hstr_vec_clear(&request->queryvars); - th_hstr_vec_clear(&request->formvars); - th_hstr_vec_clear(&request->pathvars); - request->body = (th_str){0}; - request->version = 0; - request->close = false; + response->code = code; } -TH_LOCAL(th_str) -th_request_vec_get(th_hstr_vec* vec, th_str key) +TH_PUBLIC(th_err) +th_response_add_header(th_response* response, th_str key, th_str value) { - size_t num = th_hstr_vec_size(vec); - for (size_t i = 0; i < num; i++) { - if (th_string_eq(&vec->data[i].key, key)) - return th_string_view(&vec->data[i].value); + th_header_id header_id = th_header_id_from_string(key.ptr, key.len); + if (header_id != TH_HEADER_ID_UNKNOWN && response->header_is_set[header_id]) { + return TH_ERR_INVALID_ARG; } - return TH_STR(""); -} - -TH_PRIVATE(th_str) -th_request_get_header(th_request* request, th_str key) -{ - return th_request_vec_get(&request->headers, key); + th_err err = TH_ERR_OK; + size_t old_len = th_string_len(&response->headers); + if ((err = th_string_append(&response->headers, key)) != TH_ERR_OK) + goto cleanup; + if ((err = th_string_append(&response->headers, TH_STR(": "))) != TH_ERR_OK) + goto cleanup; + if ((err = th_string_append(&response->headers, value)) != TH_ERR_OK) + goto cleanup; + if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) + goto cleanup; + if (header_id != TH_HEADER_ID_UNKNOWN) { + response->header_is_set[header_id] = 1; + } + return TH_ERR_OK; +cleanup: + th_string_resize(&response->headers, old_len, '\0'); + return err; } -TH_PRIVATE(th_str) -th_request_get_pathvar(th_request* request, th_str key) +TH_LOCAL(th_str) +th_response_get_mime_type(th_str filename) { - return th_request_vec_get(&request->pathvars, key); + char ext[256]; + size_t ei = 0; + size_t max = filename.len < sizeof(ext) ? filename.len : sizeof(ext); + for (size_t i = 0; i < max; ++i) { + size_t ri = filename.len - i - 1; + ei = max - i - 1; + ext[ei] = filename.ptr[ri]; + if (filename.ptr[ri] == '.' || filename.ptr[ri] == '/') { + break; + } + } + struct th_mime_mapping* mm = NULL; + if (ext[ei] == '.') { + mm = th_mime_mapping_find(&ext[ei + 1], max - ei - 1); + return mm ? mm->mime : TH_STR("application/octet-stream"); + } else { + return TH_STR("application/octet-stream"); + } } -TH_PRIVATE(th_str) -th_request_get_queryvar(th_request* request, th_str key) +TH_LOCAL(th_err) +th_response_set_body_from_file(th_response* response, th_str root, th_str path) { - return th_request_vec_get(&request->queryvars, key); + th_dir* dir = th_dir_mgr_get(response->dir_mgr, root); + if (!dir) + return TH_ERR_INVALID_ARG; + th_err err = TH_ERR_OK; + if ((err = th_fcache_get(response->fcache, dir, path, &response->fcache_entry)) != TH_ERR_OK) { + return err; + } + // Set the content type, if not already set + if (response->header_is_set[TH_HEADER_ID_CONTENT_TYPE] == 0) { + th_str mime_type = th_response_get_mime_type(path); + if ((err = th_response_add_header(response, TH_STR("Content-Type"), mime_type)) != TH_ERR_OK) + goto cleanup_fcache_entry; + } + response->is_file = 1; + return TH_ERR_OK; +cleanup_fcache_entry: + th_fcache_entry_unref(response->fcache_entry); + response->fcache_entry = NULL; + return err; } -TH_PRIVATE(th_str) -th_request_get_formvar(th_request* request, th_str key) +TH_PRIVATE(th_err) +th_response_set_body(th_response* response, th_str body) { - return th_request_vec_get(&request->formvars, key); + th_err err = TH_ERR_OK; + if ((err = th_string_set(&response->body, body)) != TH_ERR_OK) + return err; + response->is_file = 0; + return TH_ERR_OK; } -TH_PRIVATE(th_part*) -th_request_get_part(th_request* request, th_str key) +TH_LOCAL(th_err) +TH_PRINTF_FMT(2, 0) +th_response_set_body_va(th_response* response, const char* fmt, va_list args) { - size_t num = th_part_vec_size(&request->parts); - for (size_t i = 0; i < num; i++) { - if (th_string_eq(&request->parts.data[i].name, key)) - return th_part_vec_at(&request->parts, i); + char buffer[512]; + th_err err = TH_ERR_OK; + va_list va; + va_copy(va, args); + int len = vsnprintf(buffer, sizeof(buffer), fmt, va); + va_end(va); + if (len < 0) { + return TH_ERR_INVALID_ARG; + } else if ((size_t)len < sizeof(buffer)) { + if ((err = th_string_set(&response->body, th_str_make(buffer, (size_t)len))) != TH_ERR_OK) { + return err; + } + } else { + th_string_resize(&response->body, (size_t)len, ' '); + vsnprintf(th_string_at(&response->body, 0), (size_t)len + 1, fmt, args); } - return NULL; + response->is_file = 0; + return TH_ERR_OK; } -/* Public iterator API begin */ - -TH_PUBLIC(bool) -th_next(th_iter* it) +TH_LOCAL(th_err) +th_response_finalize_headers(th_response* response) { - return it->methods->next(it); -} + th_err err = TH_ERR_OK; + if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) + return err; + size_t headers_len = th_string_len(&response->headers); -TH_PUBLIC(const char*) -th_key(const th_iter* it) -{ - return it->methods->key(it); + // Set the start line + char int_buffer[128]; // Buffer for the integer to string conversion + if ((err = th_string_append(&response->headers, TH_STR("HTTP/1.1 "))) != TH_ERR_OK) + return err; + if ((err = th_string_append_cstr(&response->headers, th_fmt_uint_to_str(int_buffer, sizeof(int_buffer), response->code))) != TH_ERR_OK) + return err; + if ((err = th_string_append(&response->headers, TH_STR(" "))) != TH_ERR_OK) + return err; + if ((err = th_string_append_cstr(&response->headers, th_http_strerror((int)response->code))) != TH_ERR_OK) + return err; + if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) + return err; + response->iov[0].base = th_string_at(&response->headers, headers_len); + response->iov[0].len = th_string_len(&response->headers) - headers_len; + response->iov[1].base = th_string_at(&response->headers, 0); + response->iov[1].len = headers_len; + return TH_ERR_OK; } -TH_PUBLIC(const void*) -th_val(const th_iter* it) +TH_LOCAL(th_err) +th_response_set_default_headers(th_response* response) { - return it->methods->val(it); + th_err err = TH_ERR_OK; + char buffer[256]; + if (response->code == TH_CODE_SWITCHING_PROTOCOLS) { + // No body, and RFC 7230 forbids Content-Length framing here. + } else if (response->is_file) { + size_t len = 0; + const char* content_len = th_fmt_uint_to_str_ex(buffer, sizeof(buffer), (unsigned int)response->file_len, &len); + if ((err = th_response_add_header(response, TH_STR("Content-Length"), th_str_make(content_len, len))) != TH_ERR_OK) + return err; + } else { + size_t len = 0; + const char* body_len = th_fmt_uint_to_str_ex(buffer, sizeof(buffer), (unsigned int)th_string_len(&response->body), &len); + if ((err = th_response_add_header(response, TH_STR("Content-Length"), th_str_make(body_len, len))) != TH_ERR_OK) + return err; + } + if (!response->header_is_set[TH_HEADER_ID_SERVER]) { + if ((err = th_response_add_header(response, TH_STR("Server"), TH_STR("TinyHTTP"))) != TH_ERR_OK) + return err; + } + if (!response->header_is_set[TH_HEADER_ID_DATE]) { + th_date now = th_date_now(); + char date[64]; + size_t len = th_fmt_strtime(date, sizeof(date), now); + if ((err = th_response_add_header(response, TH_STR("Date"), th_str_make(date, len))) != TH_ERR_OK) + return err; + } + return TH_ERR_OK; } -TH_PUBLIC(const char*) -th_cval(const th_iter* it) +TH_PRIVATE(th_err) +th_response_prepare_write(th_response* response, th_response_write_plan* plan) { - return (const char*)it->methods->val(it); + th_err err = TH_ERR_OK; + size_t iovcnt = 2; // start line + headers + if (response->is_file) { + response->file_len = response->fcache_entry->stream.size; + } + if ((err = th_response_set_default_headers(response)) != TH_ERR_OK) + return err; + if ((err = th_response_finalize_headers(response)) != TH_ERR_OK) + return err; + if (!response->only_headers && response->is_file == 0 && th_string_len(&response->body) > 0) { + response->iov[iovcnt].base = (void*)th_string_data(&response->body); + response->iov[iovcnt].len = th_string_len(&response->body); + iovcnt++; + } + plan->iov = response->iov; + plan->iovcnt = iovcnt; + if (!response->only_headers && response->is_file != 0) { + plan->file = &response->fcache_entry->stream; + plan->offset = 0; + plan->len = (size_t)response->file_len; + } else { + plan->file = NULL; + plan->offset = 0; + plan->len = 0; + } + return TH_ERR_OK; } -/* Public iterator API end */ -/* Public request API begin */ - -TH_PUBLIC(const char*) -th_get_path(const th_request* req) -{ - return th_string_data(&req->uri_path); -} +/* Public response API begin */ -TH_PUBLIC(const char*) -th_get_query(const th_request* req) +TH_PUBLIC(th_err) +th_set_body(th_response* response, const char* body) { - return th_string_data(&req->uri_query); + return th_response_set_body(response, th_str_from_cstr(body)); } -TH_PUBLIC(th_buffer) -th_get_body(const th_request* req) +TH_PUBLIC(th_err) +TH_PRINTF_FMT(2, 3) +th_printf_body(th_response* resp, const char* fmt, ...) { - return (th_buffer){req->body.ptr, req->body.len}; + va_list args; + va_start(args, fmt); + th_err err = th_response_set_body_va(resp, fmt, args); + va_end(args); + return err; } -TH_PUBLIC(th_method) -th_get_method(const th_request* req) +TH_PUBLIC(th_err) +th_set_body_from_file(th_response* response, const char* root, const char* filepath) { - return req->method; + (void)root; + return th_response_set_body_from_file(response, th_str_from_cstr(root), th_str_from_cstr(filepath)); } -TH_PUBLIC(th_prot_version) -th_get_version(const th_request* req) +TH_PUBLIC(th_err) +th_add_header(th_response* response, const char* key, const char* value) { - return (th_prot_version)req->version; + return th_response_add_header(response, th_str_from_cstr(key), th_str_from_cstr(value)); } -TH_PUBLIC(const char*) -th_find_header(const th_request* req, const char* key) +TH_PUBLIC(th_err) +th_add_cookie(th_response* response, const char* key, const char* value, th_cookie_attr* attr) { - size_t num = th_hstr_vec_size(&req->headers); - for (size_t i = 0; i < num; i++) { - if (strncmp(key, th_string_data(&req->headers.data[i].key), th_string_len(&req->headers.data[i].key)) == 0) { - return th_string_data(&req->headers.data[i].value); + char buffer[512]; + size_t len = 0; + len += th_fmt_str_append(buffer, len, sizeof(buffer), key); + len += th_fmt_str_append(buffer, len, sizeof(buffer), "="); + len += th_fmt_str_append(buffer, len, sizeof(buffer), value); + if (attr) { + th_date empty_date = {0}; + if (memcmp(&attr->expires, &empty_date, sizeof(th_date)) != 0) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Expires="); + len += th_fmt_strtime(buffer + len, sizeof(buffer) - len, attr->expires); + } + if (attr->max_age.seconds) { + char max_age[32]; + const char* max_age_str = th_fmt_uint_to_str(max_age, sizeof(max_age), (unsigned int)attr->max_age.seconds); + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Max-Age="); + len += th_fmt_str_append(buffer, len, sizeof(buffer), max_age_str); + } + if (attr->domain) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Domain="); + len += th_fmt_str_append(buffer, len, sizeof(buffer), attr->domain); + } + if (attr->path) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Path="); + len += th_fmt_str_append(buffer, len, sizeof(buffer), attr->path); + } + if (attr->secure) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Secure"); + } + if (attr->http_only) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; HttpOnly"); + } + if (attr->same_site) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "; SameSite="); + switch (attr->same_site) { + case TH_COOKIE_SAME_SITE_NONE: + if (attr->secure) { + len += th_fmt_str_append(buffer, len, sizeof(buffer), "None"); + } else { + return TH_ERR_INVALID_ARG; + } + break; + case TH_COOKIE_SAME_SITE_LAX: + len += th_fmt_str_append(buffer, len, sizeof(buffer), "Lax"); + break; + case TH_COOKIE_SAME_SITE_STRICT: + len += th_fmt_str_append(buffer, len, sizeof(buffer), "Strict"); + break; + default: + return TH_ERR_INVALID_ARG; + break; + } } } - return NULL; + return th_response_add_header(response, TH_STR("Set-Cookie"), th_str_make(buffer, len)); } +/* End of src/th_response.c */ +/* Start of src/th_conn.c */ -TH_PUBLIC(th_iter) -th_header_iter(const th_request* req) -{ - return (th_iter){ - .methods = &th_hstr_iter_methods, - .ptr = req->headers.data, - .end = req->headers.data + req->headers.size, - }; -} +/* th_conn_observable begin */ -TH_PUBLIC(const char*) -th_find_cookie(const th_request* req, const char* key) +TH_PRIVATE(void) +th_conn_observable_destroy(void* self) { - size_t num = th_hstr_vec_size(&req->cookies); - for (size_t i = 0; i < num; i++) { - if (strncmp(key, th_string_data(&req->cookies.data[i].key), th_string_len(&req->cookies.data[i].key)) == 0) { - return th_string_data(&req->cookies.data[i].value); - } - } - return NULL; + th_conn_observable* observable = self; + th_conn_observer_on_deinit(observable->observer, observable); + observable->destroy(observable); } -TH_PUBLIC(th_iter) -th_cookie_iter(const th_request* req) +TH_PRIVATE(void) +th_conn_observable_init(th_conn_observable* observable, const th_conn_methods* methods, + void (*destroy)(void* self), th_conn_observer* observer) { - return (th_iter){ - .methods = &th_hstr_iter_methods, - .ptr = req->cookies.data, - .end = req->cookies.data + req->cookies.size, - }; + /* methods->destroy must already be th_conn_observable_destroy: the + * concrete conn type's static methods table points destroy there + * so th_conn_destroy always notifies the observer first, then this + * calls the type's real destructor (the destroy param below). */ + observable->base.methods = methods; + th_conn_observer_on_init(observer, observable); + observable->destroy = destroy; + observable->observer = observer; } -TH_PUBLIC(const char*) -th_find_queryvar(const th_request* req, const char* key) +/* th_conn_observable end */ +/* End of src/th_conn.c */ +/* Start of src/th_header_id.c */ +/* ANSI-C code produced by gperf version 3.2.1 */ +/* Computed positions: -k'' */ + + +#include +#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wconversion" +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +struct th_header_id_mapping; + +#define TH_HEADER_ID_TOTAL_KEYWORDS 6 +#define TH_HEADER_ID_MIN_WORD_LENGTH 5 +#define TH_HEADER_ID_MAX_WORD_LENGTH 17 +#define TH_HEADER_ID_MIN_HASH_VALUE 5 +#define TH_HEADER_ID_MAX_HASH_VALUE 17 +/* maximum key range = 13, duplicates = 0 */ + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +/*ARGSUSED*/ +static unsigned int +th_header_id_hash (register const char *str, register size_t len) { - size_t num = th_hstr_vec_size(&req->queryvars); - for (size_t i = 0; i < num; i++) { - if (strncmp(key, th_string_data(&req->queryvars.data[i].key), th_string_len(&req->queryvars.data[i].key)) == 0) { - return th_string_data(&req->queryvars.data[i].value); - } - } - return NULL; + (void) str; + return len; } -TH_PUBLIC(th_iter) -th_queryvar_iter(const th_request* req) +struct th_header_id_mapping * +th_header_id_mapping_find (register const char *str, register size_t len) { - return (th_iter){ - .methods = &th_hstr_iter_methods, - .ptr = req->queryvars.data, - .end = req->queryvars.data + req->queryvars.size, +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + static struct th_header_id_mapping wordlist[] = + { + {""}, {""}, {""}, {""}, {""}, + {"range", TH_HEADER_ID_RANGE}, + {"cookie", TH_HEADER_ID_COOKIE}, + {""}, {""}, {""}, + {"connection", TH_HEADER_ID_CONNECTION}, + {""}, + {"content-type", TH_HEADER_ID_CONTENT_TYPE}, + {""}, + {"content-length", TH_HEADER_ID_CONTENT_LENGTH}, + {""}, {""}, + {"transfer-encoding", TH_HEADER_ID_TRANSFER_ENCODING} }; -} +#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) +#pragma GCC diagnostic pop +#endif -TH_PUBLIC(const char*) -th_find_formvar(const th_request* req, const char* key) -{ - size_t num = th_hstr_vec_size(&req->formvars); - for (size_t i = 0; i < num; i++) { - if (strncmp(key, th_string_data(&req->formvars.data[i].key), th_string_len(&req->formvars.data[i].key)) == 0) { - return th_string_data(&req->formvars.data[i].value); - } - } - return NULL; -} + if (len <= TH_HEADER_ID_MAX_WORD_LENGTH && len >= TH_HEADER_ID_MIN_WORD_LENGTH) + { + register unsigned int key = th_header_id_hash (str, len); -TH_PUBLIC(th_iter) -th_formvar_iter(const th_request* req) -{ - return (th_iter){ - .methods = &th_hstr_iter_methods, - .ptr = req->formvars.data, - .end = req->formvars.data + req->formvars.size, - }; -} + if (key <= TH_HEADER_ID_MAX_HASH_VALUE) + { + register const char *s = wordlist[key].name; -TH_PUBLIC(const char*) -th_find_pathvar(const th_request* req, const char* key) -{ - size_t num = th_hstr_vec_size(&req->pathvars); - for (size_t i = 0; i < num; i++) { - if (strncmp(key, th_string_data(&req->pathvars.data[i].key), th_string_len(&req->pathvars.data[i].key)) == 0) { - return th_string_data(&req->pathvars.data[i].value); + if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') + return &wordlist[key]; } } - return NULL; + return (struct th_header_id_mapping *) 0; } -TH_PUBLIC(th_iter) -th_pathvar_iter(const th_request* req) -{ - return (th_iter){ - .methods = &th_hstr_iter_methods, - .ptr = req->pathvars.data, - .end = req->pathvars.data + req->pathvars.size, - }; -} +#pragma GCC diagnostic pop +/* End of src/th_header_id.c */ +/* Start of src/th_filepath.c */ -TH_PUBLIC(const th_part*) -th_find_part(const th_request* req, const char* name) +#include + +TH_PRIVATE(th_err) +th_filepath_init(th_filepath* path, th_str str) { - size_t num = th_part_vec_size(&req->parts); - for (size_t i = 0; i < num; i++) { - if (strncmp(name, th_string_data(&req->parts.data[i].name), th_string_len(&req->parts.data[i].name)) - == 0) { - return th_part_vec_cat(&req->parts, i); + if (str.len == 0 || str.len > TH_CONFIG_MAX_PATH_LEN) + return TH_ERR_INVALID_ARG; + if (str.ptr[0] == '/' || str.ptr[str.len - 1] == '/') + return TH_ERR_INVALID_ARG; + if (th_str_find_first(str, 0, '\0') != th_str_npos) + return TH_ERR_INVALID_ARG; + size_t out = 0; + size_t start = 0; + while (start < str.len) { + size_t sep = th_str_find_first(str, start, '/'); + size_t end = sep == th_str_npos ? str.len : sep; + size_t len = end - start; + if (len == 2 && str.ptr[start] == '.' && str.ptr[start + 1] == '.') + return TH_ERR_INVALID_ARG; + bool is_dot = len == 1 && str.ptr[start] == '.'; + if (len > 0 && !is_dot) { + if (out > 0) + path->buf[out++] = '/'; + memcpy(path->buf + out, str.ptr + start, len); + out += len; } + start = end + 1; } - return NULL; -} - -TH_PUBLIC(th_iter) -th_part_iter(const th_request* req) -{ - return (th_iter){ - .methods = &th_part_iter_methods, - .ptr = req->parts.data, - .end = req->parts.data + req->parts.size, - }; + if (out == 0) + return TH_ERR_INVALID_ARG; + path->buf[out] = '\0'; + return TH_ERR_OK; } +/* End of src/th_filepath.c */ +/* Start of src/th_file.c */ -/* Public request API end */ -/* End of src/th_request.c */ -/* Start of src/th_response.c */ - +#include -#include +#if defined(TH_CONFIG_OS_POSIX) +#include #include -#include -#include -#include +#include +#include #include +#endif #undef TH_LOG_TAG -#define TH_LOG_TAG "response" - -/* th_response implementation begin */ - -TH_PRIVATE(void) -th_response_init(th_response* response, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) -{ - allocator = allocator ? allocator : th_default_allocator_get(); - th_string_init(&response->headers, allocator); - th_string_init(&response->body, allocator); - response->iov[0] = (th_iov){0}; - response->iov[1] = (th_iov){0}; - response->iov[2] = (th_iov){0}; - response->allocator = allocator; - response->dir_mgr = dir_mgr; - response->fcache = fcache; - response->fcache_entry = NULL; - response->file_len = 0; - response->code = TH_CODE_OK; - memset(response->header_is_set, 0, sizeof(response->header_is_set)); - response->is_file = false; - response->only_headers = false; -} - -TH_PRIVATE(void) -th_response_deinit(th_response* response) -{ - th_string_deinit(&response->headers); - th_string_deinit(&response->body); - if (response->fcache_entry) { - th_fcache_entry_unref(response->fcache_entry); - response->fcache_entry = NULL; - } -} +#define TH_LOG_TAG "file" -TH_PRIVATE(void) -th_response_reset(th_response* response) -{ - th_string_clear(&response->headers); - th_string_clear(&response->body); - response->iov[0] = (th_iov){0}; - response->iov[1] = (th_iov){0}; - response->iov[2] = (th_iov){0}; - if (response->fcache_entry) { - th_fcache_entry_unref(response->fcache_entry); - response->fcache_entry = NULL; - } - response->file_len = 0; - response->code = TH_CODE_OK; - memset(response->header_is_set, 0, sizeof(response->header_is_set)); - response->is_file = false; - response->only_headers = false; -} +/* th_file_ops implementation begin */ -TH_PRIVATE(void) -th_response_set_code(th_response* response, th_code code) +#if defined(TH_CONFIG_OS_POSIX) +TH_LOCAL(th_err) +th_file_ops_os_openat(void* self, int dirfd, const char* path, int flags, int* fd) { - response->code = code; + (void)self; + int ret = openat(dirfd, path, flags, 0644); + if (ret == -1) + return TH_ERR_SYSTEM(errno); + *fd = ret; + return TH_ERR_OK; } -TH_PUBLIC(th_err) -th_response_add_header(th_response* response, th_str key, th_str value) +TH_LOCAL(th_err) +th_file_ops_os_seek(void* self, int fd, int whence, size_t* pos) { - th_header_id header_id = th_header_id_from_string(key.ptr, key.len); - if (header_id != TH_HEADER_ID_UNKNOWN && response->header_is_set[header_id]) { - return TH_ERR_INVALID_ARG; - } - th_err err = TH_ERR_OK; - size_t old_len = th_string_len(&response->headers); - if ((err = th_string_append(&response->headers, key)) != TH_ERR_OK) - goto cleanup; - if ((err = th_string_append(&response->headers, TH_STR(": "))) != TH_ERR_OK) - goto cleanup; - if ((err = th_string_append(&response->headers, value)) != TH_ERR_OK) - goto cleanup; - if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) - goto cleanup; - if (header_id != TH_HEADER_ID_UNKNOWN) { - response->header_is_set[header_id] = 1; - } + (void)self; + off_t ret = lseek(fd, 0, whence); + if (ret == -1) + return TH_ERR_SYSTEM(errno); + *pos = (size_t)ret; return TH_ERR_OK; -cleanup: - th_string_resize(&response->headers, old_len, '\0'); - return err; } -TH_LOCAL(th_str) -th_response_get_mime_type(th_str filename) +TH_LOCAL(th_err) +th_file_ops_os_read(void* self, int fd, void* addr, size_t len, size_t offset, size_t* read) { - char ext[256]; - size_t ei = 0; - size_t max = filename.len < sizeof(ext) ? filename.len : sizeof(ext); - for (size_t i = 0; i < max; ++i) { - size_t ri = filename.len - i - 1; - ei = max - i - 1; - ext[ei] = filename.ptr[ri]; - if (filename.ptr[ri] == '.' || filename.ptr[ri] == '/') { - break; - } - } - struct th_mime_mapping* mm = NULL; - if (ext[ei] == '.') { - mm = th_mime_mapping_find(&ext[ei + 1], max - ei - 1); - return mm ? mm->mime : TH_STR("application/octet-stream"); - } else { - return TH_STR("application/octet-stream"); + (void)self; + off_t ret = pread(fd, addr, len, (off_t)offset); + if (ret == -1) { + *read = 0; + return TH_ERR_SYSTEM(errno); } + *read = (size_t)ret; + return TH_ERR_OK; } TH_LOCAL(th_err) -th_response_set_body_from_file(th_response* response, th_str root, th_str path) +th_file_ops_os_write(void* self, int fd, const void* addr, size_t len, size_t offset, size_t* written) { - th_dir* dir = th_dir_mgr_get(response->dir_mgr, root); - if (!dir) - return TH_ERR_INVALID_ARG; - th_err err = TH_ERR_OK; - if ((err = th_fcache_get(response->fcache, dir, path, &response->fcache_entry)) != TH_ERR_OK) { - return err; - } - // Set the content type, if not already set - if (response->header_is_set[TH_HEADER_ID_CONTENT_TYPE] == 0) { - th_str mime_type = th_response_get_mime_type(path); - if ((err = th_response_add_header(response, TH_STR("Content-Type"), mime_type)) != TH_ERR_OK) - goto cleanup_fcache_entry; + (void)self; + off_t ret = pwrite(fd, addr, len, (off_t)offset); + if (ret == -1) { + *written = 0; + return TH_ERR_SYSTEM(errno); } - response->is_file = 1; + *written = (size_t)ret; return TH_ERR_OK; -cleanup_fcache_entry: - th_fcache_entry_unref(response->fcache_entry); - response->fcache_entry = NULL; - return err; } -TH_PRIVATE(th_err) -th_response_set_body(th_response* response, th_str body) +TH_LOCAL(th_err) +th_file_ops_os_stat(void* self, int fd, struct stat* out) { - th_err err = TH_ERR_OK; - if ((err = th_string_set(&response->body, body)) != TH_ERR_OK) - return err; - response->is_file = 0; + (void)self; + if (fstat(fd, out) == -1) + return TH_ERR_SYSTEM(errno); return TH_ERR_OK; } -TH_LOCAL(th_err) -TH_PRINTF_FMT(2, 0) -th_response_set_body_va(th_response* response, const char* fmt, va_list args) +TH_LOCAL(void) +th_file_ops_os_close(void* self, int fd) { - char buffer[512]; - th_err err = TH_ERR_OK; - va_list va; - va_copy(va, args); - int len = vsnprintf(buffer, sizeof(buffer), fmt, va); - va_end(va); - if (len < 0) { - return TH_ERR_INVALID_ARG; - } else if ((size_t)len < sizeof(buffer)) { - if ((err = th_string_set(&response->body, th_str_make(buffer, (size_t)len))) != TH_ERR_OK) { - return err; - } - } else { - th_string_resize(&response->body, (size_t)len, ' '); - vsnprintf(th_string_at(&response->body, 0), (size_t)len + 1, fmt, args); - } - response->is_file = 0; - return TH_ERR_OK; + (void)self; + close(fd); } -TH_LOCAL(th_err) -th_response_finalize_headers(th_response* response) +TH_PRIVATE(th_file_ops*) +th_file_ops_os(void) { - th_err err = TH_ERR_OK; - if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) - return err; - size_t headers_len = th_string_len(&response->headers); + static th_file_ops ops = { + .openat = th_file_ops_os_openat, + .seek = th_file_ops_os_seek, + .read = th_file_ops_os_read, + .write = th_file_ops_os_write, + .stat = th_file_ops_os_stat, + .close = th_file_ops_os_close, + }; + return &ops; +} +#endif - // Set the start line - char int_buffer[128]; // Buffer for the integer to string conversion - if ((err = th_string_append(&response->headers, TH_STR("HTTP/1.1 "))) != TH_ERR_OK) - return err; - if ((err = th_string_append_cstr(&response->headers, th_fmt_uint_to_str(int_buffer, sizeof(int_buffer), response->code))) != TH_ERR_OK) - return err; - if ((err = th_string_append(&response->headers, TH_STR(" "))) != TH_ERR_OK) - return err; - if ((err = th_string_append_cstr(&response->headers, th_http_strerror((int)response->code))) != TH_ERR_OK) - return err; - if ((err = th_string_append(&response->headers, TH_STR("\r\n"))) != TH_ERR_OK) +/* th_file_ops implementation end */ +/* th_file implementation begin */ + +TH_PRIVATE(void) +th_file_init(th_file* stream, th_file_ops* ops) +{ + stream->ops = ops; + stream->fd = -1; +} + +TH_LOCAL(int) +th_open_opt_to_flags(th_open_opt opt) +{ + int flags = O_NOFOLLOW; + if (opt.read && opt.write) + flags |= O_RDWR; + else if (opt.read) + flags |= O_RDONLY; + else if (opt.write) + flags |= O_WRONLY; + if (opt.create) + flags |= O_CREAT; + if (opt.truncate) + flags |= O_TRUNC; + return flags; +} + +TH_PRIVATE(th_err) +th_file_openat(th_file* stream, th_dir* dir, const th_filepath* path, th_open_opt opt) +{ + int fd = -1; + th_err err = stream->ops->openat(stream->ops, dir->fd, th_filepath_cstr(path), th_open_opt_to_flags(opt), &fd); + if (err != TH_ERR_OK) return err; - response->iov[0].base = th_string_at(&response->headers, headers_len); - response->iov[0].len = th_string_len(&response->headers) - headers_len; - response->iov[1].base = th_string_at(&response->headers, 0); - response->iov[1].len = headers_len; + size_t size = 0; + size_t unused = 0; + if ((err = stream->ops->seek(stream->ops, fd, SEEK_END, &size)) != TH_ERR_OK) + goto cleanup; + if ((err = stream->ops->seek(stream->ops, fd, SEEK_SET, &unused)) != TH_ERR_OK) + goto cleanup; + stream->fd = fd; + stream->size = size; return TH_ERR_OK; +cleanup: + stream->ops->close(stream->ops, fd); + return err; } -TH_LOCAL(th_err) -th_response_set_default_headers(th_response* response) +TH_PRIVATE(th_err) +th_file_read(th_file* stream, void* addr, size_t len, size_t offset, size_t* read) { - th_err err = TH_ERR_OK; - char buffer[256]; - if (response->is_file) { - size_t len = 0; - const char* content_len = th_fmt_uint_to_str_ex(buffer, sizeof(buffer), (unsigned int)response->file_len, &len); - if ((err = th_response_add_header(response, TH_STR("Content-Length"), th_str_make(content_len, len))) != TH_ERR_OK) - return err; - } else { - size_t len = 0; - const char* body_len = th_fmt_uint_to_str_ex(buffer, sizeof(buffer), (unsigned int)th_string_len(&response->body), &len); - if ((err = th_response_add_header(response, TH_STR("Content-Length"), th_str_make(body_len, len))) != TH_ERR_OK) - return err; - } - if (!response->header_is_set[TH_HEADER_ID_SERVER]) { - if ((err = th_response_add_header(response, TH_STR("Server"), TH_STR("TinyHTTP"))) != TH_ERR_OK) - return err; - } - if (!response->header_is_set[TH_HEADER_ID_DATE]) { - th_date now = th_date_now(); - char date[64]; - size_t len = th_fmt_strtime(date, sizeof(date), now); - if ((err = th_response_add_header(response, TH_STR("Date"), th_str_make(date, len))) != TH_ERR_OK) - return err; - } - return TH_ERR_OK; + return stream->ops->read(stream->ops, stream->fd, addr, len, offset, read); } TH_PRIVATE(th_err) -th_response_prepare_write(th_response* response, th_response_write_plan* plan) +th_file_write(th_file* stream, const void* addr, size_t len, size_t offset, size_t* written) { - th_err err = TH_ERR_OK; - size_t iovcnt = 2; // start line + headers - if (response->is_file) { - response->file_len = response->fcache_entry->stream.size; - } - if ((err = th_response_set_default_headers(response)) != TH_ERR_OK) - return err; - if ((err = th_response_finalize_headers(response)) != TH_ERR_OK) - return err; - if (!response->only_headers && response->is_file == 0 && th_string_len(&response->body) > 0) { - response->iov[iovcnt].base = (void*)th_string_data(&response->body); - response->iov[iovcnt].len = th_string_len(&response->body); - iovcnt++; - } - plan->iov = response->iov; - plan->iovcnt = iovcnt; - if (!response->only_headers && response->is_file != 0) { - plan->file = &response->fcache_entry->stream; - plan->offset = 0; - plan->len = (size_t)response->file_len; - } else { - plan->file = NULL; - plan->offset = 0; - plan->len = 0; + return stream->ops->write(stream->ops, stream->fd, addr, len, offset, written); +} + +/** + * We use DJB2 hash function, without multiplication, + * as it's faster and good enough for our purposes. + */ +#define FSTAT_HASH_INIT 5381 +#define FSTAT_HASH_NEXT(hash, val) ((hash << 5) + hash + val) + +TH_PRIVATE(uint32_t) +th_file_stat_hash(th_file* stream) +{ + struct stat st = {0}; + th_err err = stream->ops->stat(stream->ops, stream->fd, &st); + if (err != TH_ERR_OK) { + TH_LOG_ERROR("stat failed: %s, can't calculate hash", th_strerror(err)); + TH_ASSERT(0 && "stat failed"); + return 0; } - return TH_ERR_OK; +#if defined(TH_CONFIG_OS_OSX) + int64_t mtime_sec = st.st_mtimespec.tv_sec; + int64_t mtime_nsec = st.st_mtimespec.tv_nsec; +#else + int64_t mtime_sec = st.st_mtime; + int64_t mtime_nsec = 0; +#endif + uint32_t hash = FSTAT_HASH_INIT; + hash = FSTAT_HASH_NEXT(hash, (uint32_t)mtime_sec); + hash = FSTAT_HASH_NEXT(hash, (uint32_t)mtime_nsec); + hash = FSTAT_HASH_NEXT(hash, (uint32_t)st.st_size); + hash = FSTAT_HASH_NEXT(hash, st.st_mode); + hash = FSTAT_HASH_NEXT(hash, (uint32_t)st.st_ino); + hash = FSTAT_HASH_NEXT(hash, st.st_uid); + hash = FSTAT_HASH_NEXT(hash, st.st_gid); + hash = FSTAT_HASH_NEXT(hash, (uint32_t)(st.st_nlink != 0)); + return hash; } +#undef FSTAT_HASH_INIT +#undef FSTAT_HASH_NEXT -/* Public response API begin */ +TH_PRIVATE(void) +th_file_close(th_file* stream) +{ + if (stream->fd != -1) + stream->ops->close(stream->ops, stream->fd); + stream->fd = -1; +} -TH_PUBLIC(th_err) -th_set_body(th_response* response, const char* body) +TH_PRIVATE(void) +th_file_deinit(th_file* stream) { - return th_response_set_body(response, th_str_from_cstr(body)); + th_file_close(stream); } +/* End of src/th_file.c */ +/* Start of src/th_fcache.c */ + +#undef TH_LOG_TAG +#define TH_LOG_TAG "fcache" -TH_PUBLIC(th_err) -TH_PRINTF_FMT(2, 3) -th_printf_body(th_response* resp, const char* fmt, ...) +TH_LOCAL(th_fcache_id) +th_fcache_entry_id(th_fcache_entry* entry) { - va_list args; - va_start(args, fmt); - th_err err = th_response_set_body_va(resp, fmt, args); - va_end(args); - return err; + return (th_fcache_id){th_string_view(&entry->path), entry->dir}; } -TH_PUBLIC(th_err) -th_set_body_from_file(th_response* response, const char* root, const char* filepath) +TH_LOCAL(void) +th_fcache_entry_actual_destroy(void* self) { - (void)root; - return th_response_set_body_from_file(response, th_str_from_cstr(root), th_str_from_cstr(filepath)); + th_fcache_entry* entry = self; + // Remove entry from cache + th_fcache_map_iter it = th_fcache_map_find(&entry->cache->map, th_fcache_entry_id(entry)); + if (it != NULL) { + th_fcache_map_erase(&entry->cache->map, it); + } + th_file_deinit(&entry->stream); + th_string_deinit(&entry->path); + th_allocator_free(entry->allocator, entry); } -TH_PUBLIC(th_err) -th_add_header(th_response* response, const char* key, const char* value) +TH_LOCAL(void) +th_fcache_entry_init(th_fcache_entry* entry, th_fcache* cache, th_allocator* allocator) { - return th_response_add_header(response, th_str_from_cstr(key), th_str_from_cstr(value)); + entry->allocator = allocator ? allocator : th_default_allocator_get(); + th_refcounted_init(&entry->base, th_fcache_entry_actual_destroy); + th_file_init(&entry->stream, cache->file_ops); + th_string_init(&entry->path, entry->allocator); + entry->cache = cache; + entry->next = NULL; + entry->prev = NULL; } -TH_PUBLIC(th_err) -th_add_cookie(th_response* response, const char* key, const char* value, th_cookie_attr* attr) +TH_LOCAL(th_err) +th_fcache_entry_open(th_fcache_entry* entry, th_dir* dir, th_str path) { - char buffer[512]; - size_t len = 0; - len += th_fmt_str_append(buffer, len, sizeof(buffer), key); - len += th_fmt_str_append(buffer, len, sizeof(buffer), "="); - len += th_fmt_str_append(buffer, len, sizeof(buffer), value); - if (attr) { - th_date empty_date = {0}; - if (memcmp(&attr->expires, &empty_date, sizeof(th_date)) != 0) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Expires="); - len += th_fmt_strtime(buffer + len, sizeof(buffer) - len, attr->expires); - } - if (attr->max_age.seconds) { - char max_age[32]; - const char* max_age_str = th_fmt_uint_to_str(max_age, sizeof(max_age), (unsigned int)attr->max_age.seconds); - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Max-Age="); - len += th_fmt_str_append(buffer, len, sizeof(buffer), max_age_str); - } - if (attr->domain) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Domain="); - len += th_fmt_str_append(buffer, len, sizeof(buffer), attr->domain); - } - if (attr->path) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Path="); - len += th_fmt_str_append(buffer, len, sizeof(buffer), attr->path); - } - if (attr->secure) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; Secure"); - } - if (attr->http_only) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; HttpOnly"); - } - if (attr->same_site) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "; SameSite="); - switch (attr->same_site) { - case TH_COOKIE_SAME_SITE_NONE: - if (attr->secure) { - len += th_fmt_str_append(buffer, len, sizeof(buffer), "None"); - } else { - return TH_ERR_INVALID_ARG; - } - break; - case TH_COOKIE_SAME_SITE_LAX: - len += th_fmt_str_append(buffer, len, sizeof(buffer), "Lax"); - break; - case TH_COOKIE_SAME_SITE_STRICT: - len += th_fmt_str_append(buffer, len, sizeof(buffer), "Strict"); - break; - default: - return TH_ERR_INVALID_ARG; - break; - } - } + th_err err = TH_ERR_OK; + th_filepath filepath; + th_open_opt opt = {.read = true}; + if ((err = th_filepath_init(&filepath, path)) != TH_ERR_OK) { + TH_LOG_INFO("Invalid file path %.*s: %s", (int)path.len, path.ptr, th_strerror(err)); + goto cleanup; } - return th_response_add_header(response, TH_STR("Set-Cookie"), th_str_make(buffer, len)); + if ((err = th_file_openat(&entry->stream, dir, &filepath, opt)) != TH_ERR_OK) { + TH_LOG_INFO("Failed to open file at %.*s: %s", (int)path.len, path.ptr, th_strerror(err)); + goto cleanup; + } + if ((err = th_string_set(&entry->path, path)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to set path: %s", th_strerror(err)); + goto cleanup_fstream; + } + entry->stat_hash = th_file_stat_hash(&entry->stream); + entry->dir = dir; + return TH_ERR_OK; +cleanup_fstream: + th_file_deinit(&entry->stream); +cleanup: + return err; } -/* End of src/th_response.c */ -/* Start of src/th_conn.c */ -/* th_conn_observable begin */ +TH_LOCAL(th_fcache_entry*) +th_fcache_entry_ref(th_fcache_entry* entry) +{ + th_refcounted_ref(&entry->base); + return entry; +} TH_PRIVATE(void) -th_conn_observable_destroy(void* self) +th_fcache_entry_unref(th_fcache_entry* entry) { - th_conn_observable* observable = self; - th_conn_observer_on_deinit(observable->observer, observable); - observable->destroy(observable); + th_refcounted_unref(&entry->base); } TH_PRIVATE(void) -th_conn_observable_init(th_conn_observable* observable, const th_conn_methods* methods, - void (*destroy)(void* self), th_conn_observer* observer) +th_fcache_init(th_fcache* cache, th_file_ops* file_ops, th_allocator* allocator) { - /* methods->destroy must already be th_conn_observable_destroy: the - * concrete conn type's static methods table points destroy there - * so th_conn_destroy always notifies the observer first, then this - * calls the type's real destructor (the destroy param below). */ - observable->base.methods = methods; - th_conn_observer_on_init(observer, observable); - observable->destroy = destroy; - observable->observer = observer; + cache->allocator = allocator ? allocator : th_default_allocator_get(); + cache->file_ops = file_ops; + th_fcache_map_init(&cache->map, cache->allocator); + cache->list = (th_fcache_list){NULL, NULL}; + cache->num_cached = 0; + cache->max_cached = TH_CONFIG_MAX_CACHED_FDS; } -/* th_conn_observable end */ -/* End of src/th_conn.c */ -/* Start of src/th_header_id.c */ -/* ANSI-C code produced by gperf version 3.2.1 */ -/* Computed positions: -k'' */ - - -#include -#include -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wconversion" -#if defined(__clang__) -#pragma clang diagnostic ignored "-Wshorten-64-to-32" -#endif -struct th_header_id_mapping; - -#define TH_HEADER_ID_TOTAL_KEYWORDS 6 -#define TH_HEADER_ID_MIN_WORD_LENGTH 5 -#define TH_HEADER_ID_MAX_WORD_LENGTH 17 -#define TH_HEADER_ID_MIN_HASH_VALUE 5 -#define TH_HEADER_ID_MAX_HASH_VALUE 17 -/* maximum key range = 13, duplicates = 0 */ - -#ifdef __GNUC__ -__inline -#else -#ifdef __cplusplus -inline -#endif -#endif -/*ARGSUSED*/ -static unsigned int -th_header_id_hash (register const char *str, register size_t len) +TH_LOCAL(void) +th_fcache_erase(th_fcache* cache, th_fcache_entry* entry) { - (void) str; - return len; + th_fcache_list_erase(&cache->list, entry); + th_fcache_entry_unref(entry); + --cache->num_cached; } -struct th_header_id_mapping * -th_header_id_mapping_find (register const char *str, register size_t len) +TH_LOCAL(th_fcache_entry*) +th_fcache_try_get(th_fcache* cache, th_dir* dir, th_str path) { -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif - static struct th_header_id_mapping wordlist[] = - { - {""}, {""}, {""}, {""}, {""}, - {"range", TH_HEADER_ID_RANGE}, - {"cookie", TH_HEADER_ID_COOKIE}, - {""}, {""}, {""}, - {"connection", TH_HEADER_ID_CONNECTION}, - {""}, - {"content-type", TH_HEADER_ID_CONTENT_TYPE}, - {""}, - {"content-length", TH_HEADER_ID_CONTENT_LENGTH}, - {""}, {""}, - {"transfer-encoding", TH_HEADER_ID_TRANSFER_ENCODING} - }; -#if (defined __GNUC__ && __GNUC__ + (__GNUC_MINOR__ >= 6) > 4) || (defined __clang__ && __clang_major__ >= 3) -#pragma GCC diagnostic pop -#endif - - if (len <= TH_HEADER_ID_MAX_WORD_LENGTH && len >= TH_HEADER_ID_MIN_WORD_LENGTH) - { - register unsigned int key = th_header_id_hash (str, len); - - if (key <= TH_HEADER_ID_MAX_HASH_VALUE) - { - register const char *s = wordlist[key].name; + th_fcache_entry** v = th_fcache_map_try_get(&cache->map, (th_fcache_id){path, dir}); + if (!v) + return NULL; + th_fcache_entry* entry = *v; + // Check if the file has been modified + uint32_t hash = th_file_stat_hash(&entry->stream); + if (hash != entry->stat_hash) { + TH_LOG_TRACE("File has been modified, don't use cached entry"); + th_fcache_erase(cache, entry); + return NULL; + } + // Move entry to the back of the list + th_fcache_list_erase(&cache->list, entry); + th_fcache_list_push_back(&cache->list, entry); + return th_fcache_entry_ref(entry); +} - if (*str == *s && !strncmp (str + 1, s + 1, len - 1) && s[len] == '\0') - return &wordlist[key]; - } +TH_LOCAL(th_err) +th_fcache_insert(th_fcache* cache, th_fcache_entry* entry) +{ + if (cache->num_cached == cache->max_cached) { + // Evict the first entry + th_fcache_entry* first = th_fcache_list_front(&cache->list); + th_fcache_erase(cache, first); } - return (struct th_header_id_mapping *) 0; + th_err err = TH_ERR_OK; + if ((err = th_fcache_map_set(&cache->map, th_fcache_entry_id(entry), entry)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to insert entry into map: %s", th_strerror(err)); + return err; + } + th_fcache_list_push_back(&cache->list, th_fcache_entry_ref(entry)); + cache->num_cached++; + return TH_ERR_OK; } -#pragma GCC diagnostic pop -/* End of src/th_header_id.c */ -/* Start of src/th_filepath.c */ - -#include - TH_PRIVATE(th_err) -th_filepath_init(th_filepath* path, th_str str) +th_fcache_get(th_fcache* cache, th_dir* dir, th_str path, th_fcache_entry** out) { - if (str.len == 0 || str.len > TH_CONFIG_MAX_PATH_LEN) - return TH_ERR_INVALID_ARG; - if (str.ptr[0] == '/' || str.ptr[str.len - 1] == '/') - return TH_ERR_INVALID_ARG; - if (th_str_find_first(str, 0, '\0') != th_str_npos) - return TH_ERR_INVALID_ARG; - size_t out = 0; - size_t start = 0; - while (start < str.len) { - size_t sep = th_str_find_first(str, start, '/'); - size_t end = sep == th_str_npos ? str.len : sep; - size_t len = end - start; - if (len == 2 && str.ptr[start] == '.' && str.ptr[start + 1] == '.') - return TH_ERR_INVALID_ARG; - bool is_dot = len == 1 && str.ptr[start] == '.'; - if (len > 0 && !is_dot) { - if (out > 0) - path->buf[out++] = '/'; - memcpy(path->buf + out, str.ptr + start, len); - out += len; - } - start = end + 1; + th_fcache_entry* entry = th_fcache_try_get(cache, dir, path); + if (entry) { + *out = entry; + return TH_ERR_OK; } - if (out == 0) - return TH_ERR_INVALID_ARG; - path->buf[out] = '\0'; + entry = th_allocator_alloc(cache->allocator, sizeof(th_fcache_entry)); + if (!entry) + return TH_ERR_BAD_ALLOC; + th_fcache_entry_init(entry, cache, cache->allocator); + th_err err = TH_ERR_OK; + if ((err = th_fcache_entry_open(entry, dir, path)) != TH_ERR_OK) { + th_allocator_free(cache->allocator, entry); + return err; + } + if ((err = th_fcache_insert(cache, entry)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to insert fcache entry"); + th_fcache_entry_unref(entry); + return err; + } + *out = entry; return TH_ERR_OK; } -/* End of src/th_filepath.c */ -/* Start of src/th_file.c */ -#include +TH_PRIVATE(void) +th_fcache_deinit(th_fcache* cache) +{ + th_fcache_entry* entry = NULL; + while ((entry = th_fcache_list_pop_front(&cache->list))) { + th_fcache_entry_unref(entry); + } + th_fcache_map_deinit(&cache->map); +} +/* End of src/th_fcache.c */ +/* Start of src/th_dir.c */ #if defined(TH_CONFIG_OS_POSIX) +#include #include #include -#include #include #include -#endif -#undef TH_LOG_TAG -#define TH_LOG_TAG "file" - -/* th_file_ops implementation begin */ - -#if defined(TH_CONFIG_OS_POSIX) TH_LOCAL(th_err) -th_file_ops_os_openat(void* self, int dirfd, const char* path, int flags, int* fd) +th_dir_ops_os_open(void* self, const char* path, int* fd) { (void)self; - int ret = openat(dirfd, path, flags, 0644); - if (ret == -1) + int ret = open(path, O_RDONLY | O_DIRECTORY); + if (ret < 0) return TH_ERR_SYSTEM(errno); *fd = ret; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_file_ops_os_seek(void* self, int fd, int whence, size_t* pos) +TH_LOCAL(void) +th_dir_ops_os_close(void* self, int fd) { (void)self; - off_t ret = lseek(fd, 0, whence); - if (ret == -1) - return TH_ERR_SYSTEM(errno); - *pos = (size_t)ret; - return TH_ERR_OK; + int ret = close(fd); + (void)ret; + TH_ASSERT(ret == 0 && "This should not happen"); } -TH_LOCAL(th_err) -th_file_ops_os_read(void* self, int fd, void* addr, size_t len, size_t offset, size_t* read) +TH_PRIVATE(th_dir_ops*) +th_dir_ops_os(void) { - (void)self; - off_t ret = pread(fd, addr, len, (off_t)offset); - if (ret == -1) { - *read = 0; - return TH_ERR_SYSTEM(errno); - } - *read = (size_t)ret; - return TH_ERR_OK; + static th_dir_ops ops = { + .open = th_dir_ops_os_open, + .close = th_dir_ops_os_close, + }; + return &ops; } +#endif -TH_LOCAL(th_err) -th_file_ops_os_write(void* self, int fd, const void* addr, size_t len, size_t offset, size_t* written) +TH_PRIVATE(void) +th_dir_init(th_dir* dir, th_dir_ops* ops) { - (void)self; - off_t ret = pwrite(fd, addr, len, (off_t)offset); - if (ret == -1) { - *written = 0; - return TH_ERR_SYSTEM(errno); - } - *written = (size_t)ret; - return TH_ERR_OK; + dir->ops = ops; + dir->fd = -1; } -TH_LOCAL(th_err) -th_file_ops_os_stat(void* self, int fd, struct stat* out) +TH_PRIVATE(th_err) +th_dir_open(th_dir* dir, th_str path) { - (void)self; - if (fstat(fd, out) == -1) - return TH_ERR_SYSTEM(errno); + if (path.len > TH_CONFIG_MAX_PATH_LEN) + return TH_ERR_INVALID_ARG; + char path_buf[TH_CONFIG_MAX_PATH_LEN + 1] = {0}; + memcpy(path_buf, path.ptr, path.len); + path_buf[path.len] = '\0'; + int fd = -1; + th_err err = TH_ERR_OK; + if ((err = dir->ops->open(dir->ops, path_buf, &fd)) != TH_ERR_OK) + return err; + dir->fd = fd; return TH_ERR_OK; } -TH_LOCAL(void) -th_file_ops_os_close(void* self, int fd) +TH_PRIVATE(void) +th_dir_deinit(th_dir* dir) { - (void)self; - close(fd); + if (dir->fd >= 0) + dir->ops->close(dir->ops, dir->fd); } +/* End of src/th_dir.c */ +/* Start of src/th_dir_mgr.c */ -TH_PRIVATE(th_file_ops*) -th_file_ops_os(void) +TH_PRIVATE(void) +th_dir_mgr_init(th_dir_mgr* mgr, th_allocator* allocator) { - static th_file_ops ops = { - .openat = th_file_ops_os_openat, - .seek = th_file_ops_os_seek, - .read = th_file_ops_os_read, - .write = th_file_ops_os_write, - .stat = th_file_ops_os_stat, - .close = th_file_ops_os_close, - }; - return &ops; + mgr->allocator = allocator ? allocator : th_default_allocator_get(); + th_dir_map_init(&mgr->map, allocator); + th_string_vec_init(&mgr->strings, allocator); } -#endif -/* th_file_ops implementation end */ -/* th_file implementation begin */ +TH_LOCAL(bool) +th_dir_mgr_label_exists(th_dir_mgr* mgr, th_str label) +{ + return th_dir_map_find(&mgr->map, label) != NULL; +} -TH_PRIVATE(void) -th_file_init(th_file* stream, th_file_ops* ops) +TH_LOCAL(th_err) +th_dir_mgr_store_string(th_dir_mgr* mgr, th_str str) { - stream->ops = ops; - stream->fd = -1; + th_string owned = {0}; + th_string_init(&owned, mgr->allocator); + if (th_string_set(&owned, str) != TH_ERR_OK) { + return TH_ERR_BAD_ALLOC; + } + if (th_string_vec_push_back(&mgr->strings, owned) != TH_ERR_OK) { + th_string_deinit(&owned); + return TH_ERR_BAD_ALLOC; + } + return TH_ERR_OK; } -TH_LOCAL(int) -th_open_opt_to_flags(th_open_opt opt) +TH_LOCAL(th_str) +th_dir_mgr_get_last_string(th_dir_mgr* mgr) { - int flags = O_NOFOLLOW; - if (opt.read && opt.write) - flags |= O_RDWR; - else if (opt.read) - flags |= O_RDONLY; - else if (opt.write) - flags |= O_WRONLY; - if (opt.create) - flags |= O_CREAT; - if (opt.truncate) - flags |= O_TRUNC; - return flags; + return th_string_view(th_string_vec_end(&mgr->strings) - 1); +} + +TH_LOCAL(void) +th_dir_mgr_remove_last_string(th_dir_mgr* mgr) +{ + th_string_deinit(th_string_vec_end(&mgr->strings) - 1); + th_string_vec_resize(&mgr->strings, th_string_vec_size(&mgr->strings) - 1); } TH_PRIVATE(th_err) -th_file_openat(th_file* stream, th_dir* dir, const th_filepath* path, th_open_opt opt) +th_dir_mgr_add(th_dir_mgr* mgr, th_str label, th_dir dir) { - int fd = -1; - th_err err = stream->ops->openat(stream->ops, dir->fd, th_filepath_cstr(path), th_open_opt_to_flags(opt), &fd); - if (err != TH_ERR_OK) + th_err err = TH_ERR_OK; + if (th_dir_mgr_label_exists(mgr, label)) { + th_dir_deinit(&dir); + return TH_ERR_INVALID_ARG; + } + if ((err = th_dir_mgr_store_string(mgr, label)) != TH_ERR_OK) { + th_dir_deinit(&dir); return err; - size_t size = 0; - size_t unused = 0; - if ((err = stream->ops->seek(stream->ops, fd, SEEK_END, &size)) != TH_ERR_OK) - goto cleanup; - if ((err = stream->ops->seek(stream->ops, fd, SEEK_SET, &unused)) != TH_ERR_OK) - goto cleanup; - stream->fd = fd; - stream->size = size; + } + if ((err = th_dir_map_set(&mgr->map, th_dir_mgr_get_last_string(mgr), dir)) != TH_ERR_OK) { + th_dir_mgr_remove_last_string(mgr); + th_dir_deinit(&dir); + return err; + } return TH_ERR_OK; -cleanup: - stream->ops->close(stream->ops, fd); - return err; } -TH_PRIVATE(th_err) -th_file_read(th_file* stream, void* addr, size_t len, size_t offset, size_t* read) +TH_PRIVATE(th_dir*) +th_dir_mgr_get(th_dir_mgr* mgr, th_str label) +{ + th_dir_map_iter it = th_dir_map_find(&mgr->map, label); + if (it == NULL) + return NULL; + return &it->value; +} + +TH_PRIVATE(void) +th_dir_mgr_deinit(th_dir_mgr* mgr) { - return stream->ops->read(stream->ops, stream->fd, addr, len, offset, read); + th_dir_map_iter it = th_dir_map_begin(&mgr->map); + while (it != NULL) { + th_dir_deinit(&it->value); + it = th_dir_map_next(&mgr->map, it); + } + th_dir_map_deinit(&mgr->map); + th_string_vec_deinit(&mgr->strings); } +/* End of src/th_dir_mgr.c */ +/* Start of src/th_str.c */ -TH_PRIVATE(th_err) -th_file_write(th_file* stream, const void* addr, size_t len, size_t offset, size_t* written) +#include +#include +#include + + +size_t th_str_npos = (size_t)-1; + +TH_PRIVATE(bool) +th_str_is_uint(th_str str) { - return stream->ops->write(stream->ops, stream->fd, addr, len, offset, written); + for (size_t i = 0; i < str.len; i++) { + if (str.ptr[i] < '0' || str.ptr[i] > '9') { + return false; + } + } + return true; } -/** - * We use DJB2 hash function, without multiplication, - * as it's faster and good enough for our purposes. - */ -#define FSTAT_HASH_INIT 5381 -#define FSTAT_HASH_NEXT(hash, val) ((hash << 5) + hash + val) +TH_PRIVATE(th_err) +th_str_to_uint(th_str str, unsigned int* out) +{ + *out = 0; + for (size_t i = 0; i < str.len; i++) { + if (str.ptr[i] < '0' || str.ptr[i] > '9') + return TH_ERR_INVALID_ARG; + *out = *out * 10 + (unsigned int)(str.ptr[i] - '0'); + } + return TH_ERR_OK; +} -TH_PRIVATE(uint32_t) -th_file_stat_hash(th_file* stream) +TH_PRIVATE(bool) +th_str_eq(th_str a, th_str b) { - struct stat st = {0}; - th_err err = stream->ops->stat(stream->ops, stream->fd, &st); - if (err != TH_ERR_OK) { - TH_LOG_ERROR("stat failed: %s, can't calculate hash", th_strerror(err)); - TH_ASSERT(0 && "stat failed"); + if (a.len != b.len) { return 0; } -#if defined(TH_CONFIG_OS_OSX) - int64_t mtime_sec = st.st_mtimespec.tv_sec; - int64_t mtime_nsec = st.st_mtimespec.tv_nsec; -#else - int64_t mtime_sec = st.st_mtime; - int64_t mtime_nsec = 0; -#endif - uint32_t hash = FSTAT_HASH_INIT; - hash = FSTAT_HASH_NEXT(hash, (uint32_t)mtime_sec); - hash = FSTAT_HASH_NEXT(hash, (uint32_t)mtime_nsec); - hash = FSTAT_HASH_NEXT(hash, (uint32_t)st.st_size); - hash = FSTAT_HASH_NEXT(hash, st.st_mode); - hash = FSTAT_HASH_NEXT(hash, (uint32_t)st.st_ino); - hash = FSTAT_HASH_NEXT(hash, st.st_uid); - hash = FSTAT_HASH_NEXT(hash, st.st_gid); - hash = FSTAT_HASH_NEXT(hash, (uint32_t)(st.st_nlink != 0)); - return hash; + return memcmp(a.ptr, b.ptr, a.len) == 0; } -#undef FSTAT_HASH_INIT -#undef FSTAT_HASH_NEXT -TH_PRIVATE(void) -th_file_close(th_file* stream) +TH_PRIVATE(bool) +th_str_ieq(th_str a, th_str b) { - if (stream->fd != -1) - stream->ops->close(stream->ops, stream->fd); - stream->fd = -1; + if (a.len != b.len) { + return 0; + } + for (size_t i = 0; i < a.len; i++) { + if (tolower((unsigned char)a.ptr[i]) != tolower((unsigned char)b.ptr[i])) + return 0; + } + return 1; } -TH_PRIVATE(void) -th_file_deinit(th_file* stream) +TH_PRIVATE(size_t) +th_str_find_first(th_str str, size_t start, char c) { - th_file_close(stream); + if (start >= str.len) { + return th_str_npos; + } + const char* found = memchr(str.ptr + start, c, str.len - start); + return found ? (size_t)(found - str.ptr) : th_str_npos; } -/* End of src/th_file.c */ -/* Start of src/th_fcache.c */ -#undef TH_LOG_TAG -#define TH_LOG_TAG "fcache" - -TH_LOCAL(th_fcache_id) -th_fcache_entry_id(th_fcache_entry* entry) +TH_PRIVATE(size_t) +th_str_find_first_not(th_str str, size_t start, char c) { - return (th_fcache_id){th_string_view(&entry->path), entry->dir}; + for (size_t i = start; i < str.len; i++) { + if (str.ptr[i] != c) { + return i; + } + } + return th_str_npos; } -TH_LOCAL(void) -th_fcache_entry_actual_destroy(void* self) +TH_PRIVATE(size_t) +th_str_find_first_of(th_str str, size_t start, const char* chars) { - th_fcache_entry* entry = self; - // Remove entry from cache - th_fcache_map_iter it = th_fcache_map_find(&entry->cache->map, th_fcache_entry_id(entry)); - if (it != NULL) { - th_fcache_map_erase(&entry->cache->map, it); + size_t chars_len = strlen(chars); + for (size_t i = start; i < str.len; i++) { + for (size_t j = 0; j < chars_len; j++) { + if (str.ptr[i] == chars[j]) { + return i; + } + } } - th_file_deinit(&entry->stream); - th_string_deinit(&entry->path); - th_allocator_free(entry->allocator, entry); + return th_str_npos; } -TH_LOCAL(void) -th_fcache_entry_init(th_fcache_entry* entry, th_fcache* cache, th_allocator* allocator) +TH_PRIVATE(size_t) +th_str_find_last(th_str str, size_t start, char c) { - entry->allocator = allocator ? allocator : th_default_allocator_get(); - th_refcounted_init(&entry->base, th_fcache_entry_actual_destroy); - th_file_init(&entry->stream, cache->file_ops); - th_string_init(&entry->path, entry->allocator); - entry->cache = cache; - entry->next = NULL; - entry->prev = NULL; + for (size_t i = start; i < str.len; i++) { + if (str.ptr[str.len - i - 1] == c) { + return i; + } + } + return th_str_npos; } -TH_LOCAL(th_err) -th_fcache_entry_open(th_fcache_entry* entry, th_dir* dir, th_str path) +TH_PRIVATE(th_str) +th_str_substr(th_str str, size_t start, size_t len) { - th_err err = TH_ERR_OK; - th_filepath filepath; - th_open_opt opt = {.read = true}; - if ((err = th_filepath_init(&filepath, path)) != TH_ERR_OK) { - TH_LOG_INFO("Invalid file path %.*s: %s", (int)path.len, path.ptr, th_strerror(err)); - goto cleanup; - } - if ((err = th_file_openat(&entry->stream, dir, &filepath, opt)) != TH_ERR_OK) { - TH_LOG_INFO("Failed to open file at %.*s: %s", (int)path.len, path.ptr, th_strerror(err)); - goto cleanup; + if (start >= str.len) { + return th_str_make(str.ptr + len, 0); } - if ((err = th_string_set(&entry->path, path)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to set path: %s", th_strerror(err)); - goto cleanup_fstream; + if (len == th_str_npos || start + len > str.len) { + len = str.len - start; } - entry->stat_hash = th_file_stat_hash(&entry->stream); - entry->dir = dir; - return TH_ERR_OK; -cleanup_fstream: - th_file_deinit(&entry->stream); -cleanup: - return err; + return th_str_make(str.ptr + start, len); } -TH_LOCAL(th_fcache_entry*) -th_fcache_entry_ref(th_fcache_entry* entry) +TH_PRIVATE(th_str) +th_str_trim(th_str str) { - th_refcounted_ref(&entry->base); - return entry; + size_t start = 0; + while (start < str.len && (str.ptr[start] == ' ' || str.ptr[start] == '\t')) { + start++; + } + size_t end = str.len; + while (end > start && (str.ptr[end - 1] == ' ' || str.ptr[end - 1] == '\t')) { + end--; + } + return th_str_substr(str, start, end - start); } -TH_PRIVATE(void) -th_fcache_entry_unref(th_fcache_entry* entry) +TH_PRIVATE(size_t) +th_str_hash(th_str str) { - th_refcounted_unref(&entry->base); + return th_hash_bytes(str.ptr, str.len); } +/* End of src/th_str.c */ +/* Start of src/th_string.c */ -TH_PRIVATE(void) -th_fcache_init(th_fcache* cache, th_file_ops* file_ops, th_allocator* allocator) +#include + +#define TH_STRING_SMALL (sizeof(char*) + sizeof(size_t) + sizeof(size_t) - 2) +#define TH_STRING_ALIGNUP(size) TH_ALIGNUP(size, 16) +TH_LOCAL(void) +th_detail_small_string_init(th_detail_small_string* self, th_allocator* allocator) { - cache->allocator = allocator ? allocator : th_default_allocator_get(); - cache->file_ops = file_ops; - th_fcache_map_init(&cache->map, cache->allocator); - cache->list = (th_fcache_list){NULL, NULL}; - cache->num_cached = 0; - cache->max_cached = TH_CONFIG_MAX_CACHED_FDS; + self->small = 1; + self->len = 0; + self->buf[0] = '\0'; + self->allocator = allocator; + if (self->allocator == NULL) { + self->allocator = th_default_allocator_get(); + } } -TH_LOCAL(void) -th_fcache_erase(th_fcache* cache, th_fcache_entry* entry) +TH_PRIVATE(void) +th_string_init(th_string* self, th_allocator* allocator) { - th_fcache_list_erase(&cache->list, entry); - th_fcache_entry_unref(entry); - --cache->num_cached; + th_detail_small_string_init(&self->impl.small, allocator); } -TH_LOCAL(th_fcache_entry*) -th_fcache_try_get(th_fcache* cache, th_dir* dir, th_str path) +TH_PRIVATE(th_err) +th_string_init_with(th_string* self, th_str str, th_allocator* allocator) { - th_fcache_entry** v = th_fcache_map_try_get(&cache->map, (th_fcache_id){path, dir}); - if (!v) - return NULL; - th_fcache_entry* entry = *v; - // Check if the file has been modified - uint32_t hash = th_file_stat_hash(&entry->stream); - if (hash != entry->stat_hash) { - TH_LOG_TRACE("File has been modified, don't use cached entry"); - th_fcache_erase(cache, entry); - return NULL; - } - // Move entry to the back of the list - th_fcache_list_erase(&cache->list, entry); - th_fcache_list_push_back(&cache->list, entry); - return th_fcache_entry_ref(entry); + th_string_init(self, allocator); + return th_string_set(self, str); +} + +TH_LOCAL(void) +th_detail_small_string_set(th_detail_small_string* self, th_str str) +{ + TH_ASSERT(str.len <= TH_STRING_SMALL_MAX_LEN); + if (str.len > 0) + memcpy(self->buf, str.ptr, str.len); + self->buf[str.len] = '\0'; + self->len = str.len & 0x7F; } TH_LOCAL(th_err) -th_fcache_insert(th_fcache* cache, th_fcache_entry* entry) +th_detail_large_string_set(th_detail_large_string* self, th_str str) { - if (cache->num_cached == cache->max_cached) { - // Evict the first entry - th_fcache_entry* first = th_fcache_list_front(&cache->list); - th_fcache_erase(cache, first); - } - th_err err = TH_ERR_OK; - if ((err = th_fcache_map_set(&cache->map, th_fcache_entry_id(entry), entry)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to insert entry into map: %s", th_strerror(err)); - return err; + size_t required_capacity = str.len + 1; + if (self->capacity < required_capacity) { + size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); + char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); + if (new_ptr == NULL) { + return TH_ERR_BAD_ALLOC; + } + self->ptr = new_ptr; + self->capacity = new_capacity; } - th_fcache_list_push_back(&cache->list, th_fcache_entry_ref(entry)); - cache->num_cached++; + self->len = str.len; + if (str.len > 0) + memcpy(self->ptr, str.ptr, str.len); + self->ptr[str.len] = '\0'; return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_fcache_get(th_fcache* cache, th_dir* dir, th_str path, th_fcache_entry** out) +TH_LOCAL(th_err) +th_string_small_to_large(th_string* self, size_t capacity) { - th_fcache_entry* entry = th_fcache_try_get(cache, dir, path); - if (entry) { - *out = entry; - return TH_ERR_OK; - } - entry = th_allocator_alloc(cache->allocator, sizeof(th_fcache_entry)); - if (!entry) + TH_ASSERT(self->impl.small.small); + th_detail_large_string large = {0}; + capacity = TH_STRING_ALIGNUP(capacity); + large.capacity = capacity; + large.len = self->impl.small.len; + large.ptr = th_allocator_alloc(self->impl.small.allocator, capacity); + if (large.ptr == NULL) { return TH_ERR_BAD_ALLOC; - th_fcache_entry_init(entry, cache, cache->allocator); - th_err err = TH_ERR_OK; - if ((err = th_fcache_entry_open(entry, dir, path)) != TH_ERR_OK) { - th_allocator_free(cache->allocator, entry); - return err; - } - if ((err = th_fcache_insert(cache, entry)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to insert fcache entry"); - th_fcache_entry_unref(entry); - return err; } - *out = entry; + large.allocator = self->impl.small.allocator; + memcpy(large.ptr, self->impl.small.buf, self->impl.small.len); + large.ptr[self->impl.small.len] = '\0'; + self->impl.large = large; return TH_ERR_OK; } -TH_PRIVATE(void) -th_fcache_deinit(th_fcache* cache) +TH_PRIVATE(th_err) +th_string_set(th_string* self, th_str str) { - th_fcache_entry* entry = NULL; - while ((entry = th_fcache_list_pop_front(&cache->list))) { - th_fcache_entry_unref(entry); + TH_ASSERT(str.ptr != NULL && "Invalid string"); + if (self->impl.small.small) { + if (str.len <= TH_STRING_SMALL_MAX_LEN) { + th_detail_small_string_set(&self->impl.small, str); + return TH_ERR_OK; + } else { + th_err err = th_string_small_to_large(self, str.len + 1); + if (err != TH_ERR_OK) + return err; + } } - th_fcache_map_deinit(&cache->map); + return th_detail_large_string_set(&self->impl.large, str); } -/* End of src/th_fcache.c */ -/* Start of src/th_dir.c */ -#if defined(TH_CONFIG_OS_POSIX) -#include -#include -#include -#include -#include +TH_LOCAL(void) +th_detail_small_string_append(th_detail_small_string* self, th_str str) +{ + TH_ASSERT(self->len + str.len <= TH_STRING_SMALL_MAX_LEN); + memcpy(self->buf + self->len, str.ptr, str.len); + self->len += str.len & 0x7F; + self->buf[self->len] = '\0'; +} TH_LOCAL(th_err) -th_dir_ops_os_open(void* self, const char* path, int* fd) +th_detail_large_string_append(th_detail_large_string* self, th_str str) { - (void)self; - int ret = open(path, O_RDONLY | O_DIRECTORY); - if (ret < 0) - return TH_ERR_SYSTEM(errno); - *fd = ret; + size_t required_capacity = self->len + str.len + 1; + if (required_capacity > self->capacity) { + size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); + char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); + if (new_ptr == NULL) { + return TH_ERR_BAD_ALLOC; + } + self->ptr = new_ptr; + self->capacity = new_capacity; + } + memcpy(self->ptr + self->len, str.ptr, str.len); + self->len += str.len; + self->ptr[self->len] = '\0'; return TH_ERR_OK; } -TH_LOCAL(void) -th_dir_ops_os_close(void* self, int fd) +TH_PRIVATE(th_err) +th_string_append(th_string* self, th_str str) { - (void)self; - int ret = close(fd); - (void)ret; - TH_ASSERT(ret == 0 && "This should not happen"); + if (self->impl.small.small) { + if (self->impl.small.len + str.len <= TH_STRING_SMALL_MAX_LEN) { + th_detail_small_string_append(&self->impl.small, str); + return TH_ERR_OK; + } else { + th_err err = th_string_small_to_large(self, self->impl.small.len + str.len + 1); + if (err != TH_ERR_OK) + return err; + } + } + return th_detail_large_string_append(&self->impl.large, str); } -TH_PRIVATE(th_dir_ops*) -th_dir_ops_os(void) +TH_PRIVATE(th_err) +th_string_append_cstr(th_string* self, const char* str) { - static th_dir_ops ops = { - .open = th_dir_ops_os_open, - .close = th_dir_ops_os_close, - }; - return &ops; + return th_string_append(self, th_str_make(str, strlen(str))); } -#endif -TH_PRIVATE(void) -th_dir_init(th_dir* dir, th_dir_ops* ops) +TH_PRIVATE(th_err) +th_string_push_back(th_string* self, char c) { - dir->ops = ops; - dir->fd = -1; + return th_string_append(self, (th_str){&c, 1}); } -TH_PRIVATE(th_err) -th_dir_open(th_dir* dir, th_str path) +TH_LOCAL(void) +th_detail_small_string_resize(th_detail_small_string* self, size_t new_len, char fill) { - if (path.len > TH_CONFIG_MAX_PATH_LEN) - return TH_ERR_INVALID_ARG; - char path_buf[TH_CONFIG_MAX_PATH_LEN + 1] = {0}; - memcpy(path_buf, path.ptr, path.len); - path_buf[path.len] = '\0'; - int fd = -1; - th_err err = TH_ERR_OK; - if ((err = dir->ops->open(dir->ops, path_buf, &fd)) != TH_ERR_OK) - return err; - dir->fd = fd; - return TH_ERR_OK; + TH_ASSERT(new_len <= TH_STRING_SMALL_MAX_LEN && "Invalid length"); + if (new_len > self->len) + memset(self->buf + self->len, fill, new_len - self->len); + self->len = new_len & 0x7F; + self->buf[new_len] = '\0'; } -TH_PRIVATE(void) -th_dir_deinit(th_dir* dir) +TH_LOCAL(th_err) +th_detail_large_string_resize(th_detail_large_string* self, size_t new_len, char fill) { - if (dir->fd >= 0) - dir->ops->close(dir->ops, dir->fd); + size_t required_capacity = new_len + 1; + if (required_capacity > self->capacity) { + size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); + char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); + if (new_ptr == NULL) { + return TH_ERR_BAD_ALLOC; + } + self->ptr = new_ptr; + self->capacity = new_capacity; + } + if (new_len > self->len) + memset(self->ptr + self->len, fill, new_len - self->len); + self->len = new_len; + self->ptr[new_len] = '\0'; + return TH_ERR_OK; } -/* End of src/th_dir.c */ -/* Start of src/th_dir_mgr.c */ -TH_PRIVATE(void) -th_dir_mgr_init(th_dir_mgr* mgr, th_allocator* allocator) +TH_PRIVATE(th_err) +th_string_resize(th_string* self, size_t new_len, char fill) { - mgr->allocator = allocator ? allocator : th_default_allocator_get(); - th_dir_map_init(&mgr->map, allocator); - th_string_vec_init(&mgr->strings, allocator); + if (self->impl.small.small) { + if (new_len <= TH_STRING_SMALL_MAX_LEN) { + th_detail_small_string_resize(&self->impl.small, new_len, fill); + return TH_ERR_OK; + } else { + th_err err = th_string_small_to_large(self, new_len + 1); + if (err != TH_ERR_OK) + return err; + } + } + return th_detail_large_string_resize(&self->impl.large, new_len, fill); } -TH_LOCAL(bool) -th_dir_mgr_label_exists(th_dir_mgr* mgr, th_str label) +TH_PRIVATE(th_str) +th_string_view(const th_string* self) { - return th_dir_map_find(&mgr->map, label) != NULL; + if (self->impl.small.small) { + return (th_str){self->impl.small.buf, self->impl.small.len}; + } else { + return (th_str){self->impl.large.ptr, self->impl.large.len}; + } } -TH_LOCAL(th_err) -th_dir_mgr_store_string(th_dir_mgr* mgr, th_str str) +TH_PRIVATE(const char*) +th_string_data(const th_string* self) { - th_string owned = {0}; - th_string_init(&owned, mgr->allocator); - if (th_string_set(&owned, str) != TH_ERR_OK) { - return TH_ERR_BAD_ALLOC; + if (self->impl.small.small) { + return self->impl.small.buf; + } else { + return self->impl.large.ptr; } - if (th_string_vec_push_back(&mgr->strings, owned) != TH_ERR_OK) { - th_string_deinit(&owned); - return TH_ERR_BAD_ALLOC; +} + +TH_PRIVATE(char*) +th_string_at(th_string* self, size_t index) +{ + TH_ASSERT(index < th_string_len(self) && "Index out of bounds"); + if (self->impl.small.small) { + return &self->impl.small.buf[index]; + } else { + return &self->impl.large.ptr[index]; } - return TH_ERR_OK; } -TH_LOCAL(th_str) -th_dir_mgr_get_last_string(th_dir_mgr* mgr) +TH_PRIVATE(size_t) +th_string_len(const th_string* self) { - return th_string_view(th_string_vec_end(&mgr->strings) - 1); + if (self->impl.small.small) { + return self->impl.small.len; + } else { + return self->impl.large.len; + } } -TH_LOCAL(void) -th_dir_mgr_remove_last_string(th_dir_mgr* mgr) +TH_PRIVATE(void) +th_string_clear(th_string* self) { - th_string_deinit(th_string_vec_end(&mgr->strings) - 1); - th_string_vec_resize(&mgr->strings, th_string_vec_size(&mgr->strings) - 1); + if (self->impl.small.small) { + self->impl.small.len = 0; + self->impl.small.buf[0] = '\0'; + } else { + self->impl.large.len = 0; + self->impl.large.ptr[0] = '\0'; + } } -TH_PRIVATE(th_err) -th_dir_mgr_add(th_dir_mgr* mgr, th_str label, th_dir dir) +TH_PRIVATE(void) +th_string_to_lower(th_string* self) { - th_err err = TH_ERR_OK; - if (th_dir_mgr_label_exists(mgr, label)) { - th_dir_deinit(&dir); - return TH_ERR_INVALID_ARG; - } - if ((err = th_dir_mgr_store_string(mgr, label)) != TH_ERR_OK) { - th_dir_deinit(&dir); - return err; - } - if ((err = th_dir_map_set(&mgr->map, th_dir_mgr_get_last_string(mgr), dir)) != TH_ERR_OK) { - th_dir_mgr_remove_last_string(mgr); - th_dir_deinit(&dir); - return err; + char* ptr = th_string_at(self, 0); + size_t n = th_string_len(self); + for (size_t i = 0; i < n; i++) { + ptr[i] = (char)tolower((int)ptr[i]); } - return TH_ERR_OK; } -TH_PRIVATE(th_dir*) -th_dir_mgr_get(th_dir_mgr* mgr, th_str label) +TH_PRIVATE(bool) +th_string_eq(const th_string* self, th_str other) { - th_dir_map_iter it = th_dir_map_find(&mgr->map, label); - if (it == NULL) - return NULL; - return &it->value; + const char* ptr = NULL; + size_t n = 0; + if (self->impl.small.small) { + ptr = self->impl.small.buf; + n = self->impl.small.len; + } else { + ptr = self->impl.large.ptr; + n = self->impl.large.len; + } + return n == other.len && (n == 0 || memcmp(ptr, other.ptr, n) == 0); } +// TH_PRIVATE(uint32_t) +// th_string_hash(const th_string* self) +//{ +// const char* ptr = NULL; +// size_t n = 0; +// if (self->impl.small.small) { +// ptr = self->impl.small.buf; +// n = self->impl.small.len; +// } else { +// ptr = self->impl.large.ptr; +// n = self->impl.large.len; +// } +// return th_hash_bytes(ptr, n); +// } + TH_PRIVATE(void) -th_dir_mgr_deinit(th_dir_mgr* mgr) +th_string_deinit(th_string* self) { - th_dir_map_iter it = th_dir_map_begin(&mgr->map); - while (it != NULL) { - th_dir_deinit(&it->value); - it = th_dir_map_next(&mgr->map, it); + if (!self->impl.small.small) { + th_allocator_free(self->impl.large.allocator, self->impl.large.ptr); } - th_dir_map_deinit(&mgr->map); - th_string_vec_deinit(&mgr->strings); } -/* End of src/th_dir_mgr.c */ -/* Start of src/th_str.c */ +/* End of src/th_string.c */ +/* Start of src/th_log.c */ -#include -#include +#include +/* global log instance */ -size_t th_str_npos = (size_t)-1; +static th_log* th_user_log = NULL; -TH_PRIVATE(bool) -th_str_is_uint(th_str str) +TH_PUBLIC(void) +th_log_set(th_log* log) { - for (size_t i = 0; i < str.len; i++) { - if (str.ptr[i] < '0' || str.ptr[i] > '9') { - return false; - } - } - return true; + th_user_log = log; } -TH_PRIVATE(th_err) -th_str_to_uint(th_str str, unsigned int* out) +/** th_log_get + * @brief Get the current user log instance. + * @return The current user log instance, or the default log instance if no user log is set. + */ +TH_LOCAL(th_log*) +th_log_get(void) { - *out = 0; - for (size_t i = 0; i < str.len; i++) { - if (str.ptr[i] < '0' || str.ptr[i] > '9') - return TH_ERR_INVALID_ARG; - *out = *out * 10 + (unsigned int)(str.ptr[i] - '0'); - } - return TH_ERR_OK; + return th_user_log ? th_user_log : th_default_log_get(); } -TH_PRIVATE(bool) -th_str_eq(th_str a, th_str b) +/* th_default_log implementation begin */ + +/** th_default_log + * @brief Default log implementation, simply prints log messages to stderr. + */ +typedef struct th_default_log { + th_log base; +} th_default_log; + +TH_LOCAL(void) +th_default_log_print(void* self, int level, const char* msg) { - if (a.len != b.len) { - return 0; - } - return memcmp(a.ptr, b.ptr, a.len) == 0; + (void)self; + (void)level; + fprintf(stderr, "%s\n", msg); } -TH_PRIVATE(size_t) -th_str_find_first(th_str str, size_t start, char c) +TH_PRIVATE(th_log*) +th_default_log_get(void) { - if (start >= str.len) { - return th_str_npos; - } - const char* found = memchr(str.ptr + start, c, str.len - start); - return found ? (size_t)(found - str.ptr) : th_str_npos; + static th_default_log log = { + .base = { + .print = th_default_log_print, + }}; + return (th_log*)&log; } -TH_PRIVATE(size_t) -th_str_find_first_not(th_str str, size_t start, char c) +TH_PRIVATE(void) +TH_PRINTF_FMT(2, 3) +th_log_printf(int level, const char* fmt, ...) { - for (size_t i = start; i < str.len; i++) { - if (str.ptr[i] != c) { - return i; - } - } - return th_str_npos; + th_log* log = th_log_get(); + char buffer[1024]; + va_list args; + va_start(args, fmt); + int ret = vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + if (ret < 0 || (size_t)ret >= sizeof(buffer)) + goto on_error; + log->print(log, level, buffer); + return; +on_error: + log->print(log, TH_LOG_LEVEL_ERROR, "ERROR: [th_log] Failed to format log message"); } -TH_PRIVATE(size_t) -th_str_find_first_of(th_str str, size_t start, const char* chars) +/* th_default_log implementation end */ +/* End of src/th_log.c */ +/* Start of src/th_http.c */ + +#include + +#undef TH_LOG_TAG +#define TH_LOG_TAG "http" + +#define TH_HTTP_CLOSE true +#define TH_HTTP_KEEP_ALIVE false + +TH_LOCAL(void) +th_http_destroy(void* self) { - size_t chars_len = strlen(chars); - for (size_t i = start; i < str.len; i++) { - for (size_t j = 0; j < chars_len; j++) { - if (str.ptr[i] == chars[j]) { - return i; - } - } - } - return th_str_npos; + th_http* http = self; + TH_LOG_TRACE("%p: Destroying http protocol instance", http); + th_conn_destroy(http->conn); + th_request_deinit(&http->request); + th_response_deinit(&http->response); + th_buf_vec_deinit(&http->buf); + th_allocator_free(http->allocator, http); } -TH_PRIVATE(size_t) -th_str_find_last(th_str str, size_t start, char c) +// Moves conn out of http (leaving it destroy-safe with a NULL conn) and +// destroys everything else - used to hand conn off to upgrade it to +// another protocol without tearing it down. +TH_LOCAL(th_conn*) +th_http_detach_conn(th_http* http) { - for (size_t i = start; i < str.len; i++) { - if (str.ptr[str.len - i - 1] == c) { - return i; - } - } - return th_str_npos; + th_conn* conn = TH_MOVE_PTR(http->conn); + th_http_destroy(http); + return conn; } -TH_PRIVATE(th_str) -th_str_substr(th_str str, size_t start, size_t len) +TH_LOCAL(void) +th_http_handle_read_request(void* user_data, size_t len, th_err err); + +TH_LOCAL(void) +th_http_handle_write_response(void* user_data, size_t len, th_err err); + +TH_LOCAL(void) +th_http_handle_error(th_http* http, th_err err); + +TH_LOCAL(void) +th_http_restart(th_http* http) { - if (start >= str.len) { - return th_str_make(str.ptr + len, 0); - } - if (len == th_str_npos || start + len > str.len) { - len = str.len - start; + http->read_bytes = 0; + http->parsed_bytes = 0; + th_request_parser_reset(&http->parser); + th_request_reset(&http->request); + th_response_reset(&http->response); + th_conn_recv(http->conn, th_buf_vec_at(&http->buf, 0), th_buf_vec_size(&http->buf), false, th_http_handle_read_request, http); +} + +TH_LOCAL(void) +th_http_complete(th_http* http) +{ + if (http->close) { + th_http_destroy(http); + } else { + th_http_restart(http); } - return th_str_make(str.ptr + start, len); } -TH_PRIVATE(th_str) -th_str_trim(th_str str) +TH_LOCAL(void) +th_http_write_response_cb(th_http* http, th_send_cb callback) { - size_t start = 0; - while (start < str.len && (str.ptr[start] == ' ' || str.ptr[start] == '\t')) { - start++; - } - size_t end = str.len; - while (end > start && (str.ptr[end - 1] == ' ' || str.ptr[end - 1] == '\t')) { - end--; + th_response_write_plan plan; + th_err err = th_response_prepare_write(&http->response, &plan); + if (err != TH_ERR_OK) { + callback(http, 0, err); + return; } - return th_str_substr(str, start, end - start); + th_conn_send(http->conn, plan.iov, plan.iovcnt, plan.file, plan.offset, plan.len, callback, http); } -TH_PRIVATE(size_t) -th_str_hash(th_str str) +TH_LOCAL(void) +th_http_write_response(th_http* http) { - return th_hash_bytes(str.ptr, str.len); + th_http_write_response_cb(http, th_http_handle_write_response); } -/* End of src/th_str.c */ -/* Start of src/th_string.c */ -#include - -#define TH_STRING_SMALL (sizeof(char*) + sizeof(size_t) + sizeof(size_t) - 2) -#define TH_STRING_ALIGNUP(size) TH_ALIGNUP(size, 16) TH_LOCAL(void) -th_detail_small_string_init(th_detail_small_string* self, th_allocator* allocator) +th_http_handle_ws_upgrade_written(void* user_data, size_t len, th_err err) { - self->small = 1; - self->len = 0; - self->buf[0] = '\0'; - self->allocator = allocator; - if (self->allocator == NULL) { - self->allocator = th_default_allocator_get(); + th_http* http = user_data; + (void)len; + if (err != TH_ERR_OK) { + TH_LOG_ERROR("%p: Failed to write WS upgrade response: %s", (void*)http, th_strerror(err)); + th_http_destroy(http); + return; } + th_ws_handler handler = http->ws_handler; + void* ws_user_data = http->ws_user_data; + th_conn* conn = th_http_detach_conn(http); + th_ws* ws = NULL; + if ((err = th_ws_create(&ws, conn, handler, ws_user_data, NULL)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to create ws instance: %s", th_strerror(err)); + th_conn_destroy(conn); + return; + } + th_ws_start(ws); } -TH_PRIVATE(void) -th_string_init(th_string* self, th_allocator* allocator) +// Sends the 101 response and, on success, hands conn off to a new th_ws. +// If request wasn't actually a WS handshake, sends a 426 Upgrade Required +// instead. +TH_LOCAL(void) +th_http_try_upgrade_ws(th_http* http) +{ + th_ws_handler handler = NULL; + void* user_data = NULL; + bool is_ws_route = th_router_find_ws_route(http->router, th_string_view(&http->request.uri_path), &handler, &user_data); + if (!is_ws_route || !th_ws_is_handshake(&http->request)) { + th_response_add_header(&http->response, TH_STR("Upgrade"), TH_STR("websocket")); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_UPGRADE_REQUIRED)); + return; + } + + th_err err = TH_ERR_OK; + if ((err = th_response_add_header(&http->response, TH_STR("Upgrade"), TH_STR("websocket"))) != TH_ERR_OK) + goto fail; + if ((err = th_response_add_header(&http->response, TH_STR("Connection"), TH_STR("Upgrade"))) != TH_ERR_OK) + goto fail; + th_string accept_key; + th_string_init(&accept_key, http->allocator); + err = th_ws_handshake_accept_key(th_request_get_header(&http->request, TH_STR("sec-websocket-key")), &accept_key); + if (err == TH_ERR_OK) + err = th_response_add_header(&http->response, TH_STR("Sec-WebSocket-Accept"), th_string_view(&accept_key)); + th_string_deinit(&accept_key); + if (err != TH_ERR_OK) + goto fail; + + http->ws_handler = handler; + http->ws_user_data = user_data; + th_http_write_response_cb(http, th_http_handle_ws_upgrade_written); + return; +fail: + TH_LOG_ERROR("%p: Failed to prepare WS upgrade response: %s", (void*)http, th_strerror(err)); + th_response_reset(&http->response); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_INTERNAL_SERVER_ERROR)); +} + +TH_LOCAL(void) +th_http_write_error_response(th_http* http, th_err err) { - th_detail_small_string_init(&self->impl.small, allocator); + th_response_set_code(&http->response, TH_ERR_CODE(err)); + if (!http->response.is_file && th_string_len(&http->response.body) == 0) { + th_printf_body(&http->response, "%d %s", TH_ERR_CODE(err), th_http_strerror((int)TH_ERR_CODE(err))); + } + if (http->close) { + th_response_add_header(&http->response, TH_STR("Connection"), TH_STR("close")); + http->close = TH_HTTP_CLOSE; + } + th_http_write_response(http); } -TH_PRIVATE(th_err) -th_string_init_with(th_string* self, th_str str, th_allocator* allocator) +TH_LOCAL(void) +th_http_handle_error(th_http* http, th_err err) { - th_string_init(self, allocator); - return th_string_set(self, str); + th_http_code_type type = th_http_code_get_type(TH_ERR_CODE(err)); + switch (type) { + case TH_HTTP_CODE_TYPE_SERVER_ERROR: + http->close = TH_HTTP_CLOSE; + break; + case TH_HTTP_CODE_TYPE_CLIENT_ERROR: + break; + default: + TH_ASSERT(0 && "Invalid error type"); + break; + } + th_http_write_error_response(http, err); } TH_LOCAL(void) -th_detail_small_string_set(th_detail_small_string* self, th_str str) +th_http_handle_require_1_1(th_http* http) { - TH_ASSERT(str.len <= TH_STRING_SMALL_MAX_LEN); - if (str.len > 0) - memcpy(self->buf, str.ptr, str.len); - self->buf[str.len] = '\0'; - self->len = str.len & 0x7F; + TH_LOG_ERROR("%p: Trying send a HTTP/1.1 response to a HTTP/1.0 client, sending 400 Bad Request instead", (void*)http); + th_response_set_body(&http->response, TH_STR("HTTP/1.1 required for this request")); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_BAD_REQUEST)); } TH_LOCAL(th_err) -th_detail_large_string_set(th_detail_large_string* self, th_str str) +th_http_handle_options(th_router* router, th_request* request, th_response* response) { - size_t required_capacity = str.len + 1; - if (self->capacity < required_capacity) { - size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); - char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); - if (new_ptr == NULL) { - return TH_ERR_BAD_ALLOC; + // All the methods we gotta check + static const struct { + th_method method; + const char* allow; + } methods[] = { + {TH_METHOD_GET, "GET, HEAD"}, + {TH_METHOD_POST, "POST"}, + {TH_METHOD_PUT, "PUT"}, + {TH_METHOD_DELETE, "DELETE"}, + {TH_METHOD_PATCH, "PATCH"}, + }; + char allow[512] = {0}; + size_t pos = th_fmt_str_append(allow, 0, sizeof(allow), "OPTIONS"); // OPTIONS is always allowed + if (strcmp(th_string_data(&request->uri_path), "*") != 0) { + for (size_t i = 0; i < TH_ARRAY_SIZE(methods); i++) { + if (th_router_would_handle(router, methods[i].method, request)) { + pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, ", "); + pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, methods[i].allow); + } + } + } else { + for (size_t i = 0; i < TH_ARRAY_SIZE(methods); i++) { + pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, ", "); + pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, methods[i].allow); } - self->ptr = new_ptr; - self->capacity = new_capacity; } - self->len = str.len; - if (str.len > 0) - memcpy(self->ptr, str.ptr, str.len); - self->ptr[str.len] = '\0'; + th_err err = TH_ERR_OK; + if ((err = th_response_add_header(response, TH_STR("Allow"), th_str_make(allow, pos))) != TH_ERR_OK) + return err; + if ((err = th_response_add_header(response, TH_STR("Content-Type"), TH_STR("text/plain"))) != TH_ERR_OK) + return err; return TH_ERR_OK; } TH_LOCAL(th_err) -th_string_small_to_large(th_string* self, size_t capacity) +th_http_handle_route(th_router* router, th_request* request, th_response* response) { - TH_ASSERT(self->impl.small.small); - th_detail_large_string large = {0}; - capacity = TH_STRING_ALIGNUP(capacity); - large.capacity = capacity; - large.len = self->impl.small.len; - large.ptr = th_allocator_alloc(self->impl.small.allocator, capacity); - if (large.ptr == NULL) { - return TH_ERR_BAD_ALLOC; + if (request->method == TH_METHOD_OPTIONS) { + return th_http_handle_options(router, request, response); + } else { + return th_router_handle(router, request, response); } - large.allocator = self->impl.small.allocator; - memcpy(large.ptr, self->impl.small.buf, self->impl.small.len); - large.ptr[self->impl.small.len] = '\0'; - self->impl.large = large; - return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_string_set(th_string* self, th_str str) +TH_LOCAL(void) +th_http_prehandle_request(th_http* http) { - TH_ASSERT(str.ptr != NULL && "Invalid string"); - if (self->impl.small.small) { - if (str.len <= TH_STRING_SMALL_MAX_LEN) { - th_detail_small_string_set(&self->impl.small, str); - return TH_ERR_OK; - } else { - th_err err = th_string_small_to_large(self, str.len + 1); - if (err != TH_ERR_OK) - return err; + th_request* request = &http->request; + th_response* response = &http->response; + if (request->method == TH_METHOD_HEAD) { + response->only_headers = true; // only write headers + request->method = TH_METHOD_GET; // pretend it's a GET request + } +} + +TH_LOCAL(void) +th_http_handle_request_and_write_response(th_http* http) +{ + th_request* request = &http->request; + th_response* response = &http->response; + th_http_prehandle_request(http); + th_err err = th_http_error(th_http_handle_route(http->router, &http->request, &http->response)); + th_response_set_code(response, TH_ERR_CODE(err)); + switch (th_http_code_get_type(TH_ERR_CODE(err))) { + case TH_HTTP_CODE_TYPE_INFORMATIONAL: + if (request->version == 0) { + th_http_handle_require_1_1(http); + return; + } + if (TH_ERR_CODE(err) == TH_CODE_SWITCHING_PROTOCOLS) { + th_http_try_upgrade_ws(http); + return; + } + break; + case TH_HTTP_CODE_TYPE_SERVER_ERROR: + case TH_HTTP_CODE_TYPE_CLIENT_ERROR: + th_http_handle_error(http, err); + return; + default: + // All other types don't require any special handling + break; + } + // All good, write the response + if (request->close) { + th_response_add_header(response, TH_STR("Connection"), TH_STR("close")); + http->close = true; + } else { + th_response_add_header(response, TH_STR("Connection"), TH_STR("keep-alive")); + } + TH_LOG_TRACE("%p: Write response %p", http, response); + th_http_write_response(http); +} + +TH_LOCAL(void) +th_http_handle_read_request(void* user_data, size_t len, th_err err) +{ + th_http* http = user_data; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Read error: %s", http, th_strerror(err)); + http->close = TH_HTTP_CLOSE; // No other choice if we can't even read the request + th_http_complete(http); + return; + } + http->read_bytes += len; + size_t parsed = 0; + th_str parser_input = (th_str){.ptr = th_buf_vec_at(&http->buf, http->parsed_bytes), + .len = http->read_bytes - http->parsed_bytes}; + if ((err = th_request_parser_parse(&http->parser, &http->request, parser_input, &parsed)) != TH_ERR_OK) { + th_http_write_error_response(http, th_http_error(err)); + return; + } + if (th_request_parser_done(&http->parser)) { + th_http_handle_request_and_write_response(http); + return; + } + // If we haven't parsed the whole request, we need to read more data + http->parsed_bytes += parsed; + if (!th_request_parser_header_done(&http->parser)) { + if (http->read_bytes == th_buf_vec_size(&http->buf)) { + if (th_buf_vec_size(&http->buf) < TH_CONFIG_LARGE_HEADER_LEN) { + th_buf_vec_resize(&http->buf, TH_CONFIG_LARGE_HEADER_LEN); + } else { + th_http_write_error_response(http, TH_ERR_HTTP(TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE)); + return; + } + } + th_conn_recv(http->conn, th_buf_vec_at(&http->buf, http->read_bytes), + th_buf_vec_size(&http->buf) - http->read_bytes, false, th_http_handle_read_request, http); + } else { + size_t content_received = http->read_bytes - http->parsed_bytes; + size_t content_len = th_request_parser_content_len(&http->parser); + if (content_len > TH_MAX_BODY_LEN) { + TH_LOG_WARN("Request body too large, rejecting request"); + th_http_write_error_response(http, TH_ERR_HTTP(TH_CODE_PAYLOAD_TOO_LARGE)); + return; + } + size_t remaining = content_len - content_received; + if (http->read_bytes + remaining > th_buf_vec_size(&http->buf)) { + memmove(th_buf_vec_at(&http->buf, 0), th_buf_vec_at(&http->buf, http->parsed_bytes), content_received); + http->read_bytes = content_received; + http->parsed_bytes = 0; + if (content_len > th_buf_vec_size(&http->buf)) { + th_buf_vec_resize(&http->buf, content_len); + } } + th_conn_recv(http->conn, th_buf_vec_at(&http->buf, http->read_bytes), + remaining, true, th_http_handle_read_request, http); } - return th_detail_large_string_set(&self->impl.large, str); } TH_LOCAL(void) -th_detail_small_string_append(th_detail_small_string* self, th_str str) -{ - TH_ASSERT(self->len + str.len <= TH_STRING_SMALL_MAX_LEN); - memcpy(self->buf + self->len, str.ptr, str.len); - self->len += str.len & 0x7F; - self->buf[self->len] = '\0'; -} - -TH_LOCAL(th_err) -th_detail_large_string_append(th_detail_large_string* self, th_str str) -{ - size_t required_capacity = self->len + str.len + 1; - if (required_capacity > self->capacity) { - size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); - char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); - if (new_ptr == NULL) { - return TH_ERR_BAD_ALLOC; - } - self->ptr = new_ptr; - self->capacity = new_capacity; - } - memcpy(self->ptr + self->len, str.ptr, str.len); - self->len += str.len; - self->ptr[self->len] = '\0'; - return TH_ERR_OK; -} - -TH_PRIVATE(th_err) -th_string_append(th_string* self, th_str str) +th_http_handle_write_response(void* user_data, size_t len, th_err err) { - if (self->impl.small.small) { - if (self->impl.small.len + str.len <= TH_STRING_SMALL_MAX_LEN) { - th_detail_small_string_append(&self->impl.small, str); - return TH_ERR_OK; - } else { - th_err err = th_string_small_to_large(self, self->impl.small.len + str.len + 1); - if (err != TH_ERR_OK) - return err; - } + th_http* http = user_data; + (void)len; + if (err != TH_ERR_OK) { + TH_LOG_ERROR("%p: Write error: %s", (void*)http, th_strerror(err)); + http->close = TH_HTTP_CLOSE; // Connection is broken, close it + } else { + TH_LOG_TRACE("%p: Write response of %d bytes", http, (int)len); } - return th_detail_large_string_append(&self->impl.large, str); -} - -TH_PRIVATE(th_err) -th_string_append_cstr(th_string* self, const char* str) -{ - return th_string_append(self, th_str_make(str, strlen(str))); + th_http_complete(http); } -TH_PRIVATE(th_err) -th_string_push_back(th_string* self, char c) +TH_LOCAL(void) +th_http_start(void* self) { - return th_string_append(self, (th_str){&c, 1}); + th_http* http = self; + TH_LOG_TRACE("%p: Starting", http); + th_buf_vec_resize(&http->buf, TH_CONFIG_SMALL_HEADER_LEN); + th_conn_recv(http->conn, th_buf_vec_at(&http->buf, 0), th_buf_vec_size(&http->buf), false, th_http_handle_read_request, http); } TH_LOCAL(void) -th_detail_small_string_resize(th_detail_small_string* self, size_t new_len, char fill) +th_http_init(th_http* http, const th_conn_tracker* tracker, th_conn* conn, + th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) { - TH_ASSERT(new_len <= TH_STRING_SMALL_MAX_LEN && "Invalid length"); - if (new_len > self->len) - memset(self->buf + self->len, fill, new_len - self->len); - self->len = new_len & 0x7F; - self->buf[new_len] = '\0'; + allocator = allocator ? allocator : th_default_allocator_get(); + th_request_parser_init(&http->parser); + th_request_init(&http->request, allocator); + th_response_init(&http->response, dir_mgr, fcache, allocator); + th_buf_vec_init(&http->buf, allocator); + http->tracker = tracker; + http->conn = conn; + http->router = router; + http->dir_mgr = dir_mgr; + http->fcache = fcache; + http->allocator = allocator; + http->read_bytes = 0; + http->parsed_bytes = 0; + http->close = TH_HTTP_KEEP_ALIVE; } TH_LOCAL(th_err) -th_detail_large_string_resize(th_detail_large_string* self, size_t new_len, char fill) +th_http_create(th_http** out, const th_conn_tracker* tracker, th_conn* conn, + th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) { - size_t required_capacity = new_len + 1; - if (required_capacity > self->capacity) { - size_t new_capacity = TH_STRING_ALIGNUP(required_capacity); - char* new_ptr = th_allocator_realloc(self->allocator, self->ptr, new_capacity); - if (new_ptr == NULL) { - return TH_ERR_BAD_ALLOC; - } - self->ptr = new_ptr; - self->capacity = new_capacity; - } - if (new_len > self->len) - memset(self->ptr + self->len, fill, new_len - self->len); - self->len = new_len; - self->ptr[new_len] = '\0'; + th_http* http = th_allocator_alloc(allocator, sizeof(th_http)); + if (!http) + return TH_ERR_BAD_ALLOC; + th_http_init(http, tracker, conn, router, dir_mgr, fcache, allocator); + *out = http; return TH_ERR_OK; } -TH_PRIVATE(th_err) -th_string_resize(th_string* self, size_t new_len, char fill) +TH_LOCAL(void) +th_http_upgrader_upgrade(void* self, th_conn* conn) { - if (self->impl.small.small) { - if (new_len <= TH_STRING_SMALL_MAX_LEN) { - th_detail_small_string_resize(&self->impl.small, new_len, fill); - return TH_ERR_OK; - } else { - th_err err = th_string_small_to_large(self, new_len + 1); - if (err != TH_ERR_OK) - return err; - } + th_http_upgrader* upgrader = self; + th_http* http = NULL; + th_err err = TH_ERR_OK; + if ((err = th_http_create(&http, upgrader->tracker, conn, upgrader->router, upgrader->dir_mgr, upgrader->fcache, upgrader->allocator)) != TH_ERR_OK) { + TH_LOG_ERROR("Failed to create http instance: %s", th_strerror(err)); + th_conn_destroy(conn); + return; } - return th_detail_large_string_resize(&self->impl.large, new_len, fill); -} - -TH_PRIVATE(th_str) -th_string_view(const th_string* self) -{ - if (self->impl.small.small) { - return (th_str){self->impl.small.buf, self->impl.small.len}; - } else { - return (th_str){self->impl.large.ptr, self->impl.large.len}; + if (th_conn_tracker_count(upgrader->tracker) > TH_CONFIG_MAX_CONNECTIONS) { + TH_LOG_WARN("Too many connections, rejecting new connection"); + th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_SERVICE_UNAVAILABLE)); + return; } + th_http_start(http); } -TH_PRIVATE(const char*) -th_string_data(const th_string* self) +TH_PRIVATE(void) +th_http_upgrader_init(th_http_upgrader* upgrader, const th_conn_tracker* tracker, th_router* router, + th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) { - if (self->impl.small.small) { - return self->impl.small.buf; - } else { - return self->impl.large.ptr; - } + th_conn_upgrader_init(&upgrader->base, th_http_upgrader_upgrade); + upgrader->tracker = tracker; + upgrader->router = router; + upgrader->dir_mgr = dir_mgr; + upgrader->fcache = fcache; + upgrader->allocator = allocator; } +/* End of src/th_http.c */ +/* Start of src/th_fmt.c */ -TH_PRIVATE(char*) -th_string_at(th_string* self, size_t index) + +static const char* th_fmt_num_table[] = + {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", + "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", + "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", + "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", + "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", + "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", + "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", + "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", + "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", + "110", "111", "112", "113", "114", "115", "116", "117", "118", "119", + "120", "121", "122", "123", "124", "125", "126", "127", "128", "129", + "130", "131", "132", "133", "134", "135", "136", "137", "138", "139", + "140", "141", "142", "143", "144", "145", "146", "147", "148", "149", + "150", "151", "152", "153", "154", "155", "156", "157", "158", "159", + "160", "161", "162", "163", "164", "165", "166", "167", "168", "169", + "170", "171", "172", "173", "174", "175", "176", "177", "178", "179", + "180", "181", "182", "183", "184", "185", "186", "187", "188", "189", + "190", "191", "192", "193", "194", "195", "196", "197", "198", "199", + "200", "201", "202", "203", "204", "205", "206", "207", "208", "209", + "210", "211", "212", "213", "214", "215", "216", "217", "218", "219", + "220", "221", "222", "223", "224", "225", "226", "227", "228", "229", + "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", + "240", "241", "242", "243", "244", "245", "246", "247", "248", "249", + "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", + "260", "261", "262", "263", "264", "265", "266", "267", "268", "269", + "270", "271", "272", "273", "274", "275", "276", "277", "278", "279", + "280", "281", "282", "283", "284", "285", "286", "287", "288", "289", + "290", "291", "292", "293", "294", "295", "296", "297", "298", "299", + "300", "301", "302", "303", "304", "305", "306", "307", "308", "309", + "310", "311", "312", "313", "314", "315", "316", "317", "318", "319", + "320", "321", "322", "323", "324", "325", "326", "327", "328", "329", + "330", "331", "332", "333", "334", "335", "336", "337", "338", "339", + "340", "341", "342", "343", "344", "345", "346", "347", "348", "349", + "350", "351", "352", "353", "354", "355", "356", "357", "358", "359", + "360", "361", "362", "363", "364", "365", "366", "367", "368", "369", + "370", "371", "372", "373", "374", "375", "376", "377", "378", "379", + "380", "381", "382", "383", "384", "385", "386", "387", "388", "389", + "390", "391", "392", "393", "394", "395", "396", "397", "398", "399", + "400", "401", "402", "403", "404", "405", "406", "407", "408", "409", + "410", "411", "412", "413", "414", "415", "416", "417", "418", "419", + "420", "421", "422", "423", "424", "425", "426", "427", "428", "429", + "430", "431", "432", "433", "434", "435", "436", "437", "438", "439", + "440", "441", "442", "443", "444", "445", "446", "447", "448", "449", + "450", "451", "452", "453", "454", "455", "456", "457", "458", "459", + "460", "461", "462", "463", "464", "465", "466", "467", "468", "469", + "470", "471", "472", "473", "474", "475", "476", "477", "478", "479", + "480", "481", "482", "483", "484", "485", "486", "487", "488", "489", + "490", "491", "492", "493", "494", "495", "496", "497", "498", "499", + "500", "501", "502", "503", "504", "505", "506", "507", "508", "509", + "510", "511", "512", "513", "514", "515", "516", "517", "518", "519", + "520", "521", "522", "523", "524", "525", "526", "527", "528", "529", + "530", "531", "532", "533", "534", "535", "536", "537", "538", "539", + "540", "541", "542", "543", "544", "545", "546", "547", "548", "549", + "550", "551", "552", "553", "554", "555", "556", "557", "558", "559", + "560", "561", "562", "563", "564", "565", "566", "567", "568", "569", + "570", "571", "572", "573", "574", "575", "576", "577", "578", "579", + "580", "581", "582", "583", "584", "585", "586", "587", "588", "589", + "590", "591", "592", "593", "594", "595", "596", "597", "598", "599"}; + +TH_PRIVATE(const char*) +th_fmt_uint_to_str(char* buf, size_t len, unsigned int value) { - TH_ASSERT(index < th_string_len(self) && "Index out of bounds"); - if (self->impl.small.small) { - return &self->impl.small.buf[index]; - } else { - return &self->impl.large.ptr[index]; + if (value < TH_ARRAY_SIZE(th_fmt_num_table)) { + return th_fmt_num_table[value]; } -} -TH_PRIVATE(size_t) -th_string_len(const th_string* self) -{ - if (self->impl.small.small) { - return self->impl.small.len; - } else { - return self->impl.large.len; + buf[len - 1] = '\0'; + size_t i = len - 2; + unsigned int v = value; + while (v > 0 && i > 0) { + buf[i--] = '0' + (char)(v % 10); + v /= 10; } + return &buf[i + 1]; } -TH_PRIVATE(void) -th_string_clear(th_string* self) +TH_PRIVATE(const char*) +th_fmt_uint_to_str_ex(char* buf, size_t len, unsigned int val, size_t* out_len) { - if (self->impl.small.small) { - self->impl.small.len = 0; - self->impl.small.buf[0] = '\0'; - } else { - self->impl.large.len = 0; - self->impl.large.ptr[0] = '\0'; + if (val < TH_ARRAY_SIZE(th_fmt_num_table)) { + *out_len = val < 10 ? 1 : (val < 100 ? 2 : 3); + return th_fmt_num_table[val]; } -} -TH_PRIVATE(void) -th_string_to_lower(th_string* self) -{ - char* ptr = th_string_at(self, 0); - size_t n = th_string_len(self); - for (size_t i = 0; i < n; i++) { - ptr[i] = (char)tolower((int)ptr[i]); + buf[len - 1] = '\0'; + size_t i = len - 2; + unsigned int v = val; + while (v > 0 && i > 0) { + buf[i--] = '0' + (char)(v % 10); + v /= 10; } + *out_len = len - i - 2; + return &buf[i + 1]; } -TH_PRIVATE(bool) -th_string_eq(const th_string* self, th_str other) +TH_PRIVATE(size_t) +th_fmt_str_append(char* buf, size_t pos, size_t len, const char* str) { - const char* ptr = NULL; - size_t n = 0; - if (self->impl.small.small) { - ptr = self->impl.small.buf; - n = self->impl.small.len; - } else { - ptr = self->impl.large.ptr; - n = self->impl.large.len; + size_t i = 0; + while (str[i] != '\0' && pos < len - 1) { + buf[pos++] = str[i++]; } - return n == other.len && (n == 0 || memcmp(ptr, other.ptr, n) == 0); + buf[pos] = '\0'; + return i; } -// TH_PRIVATE(uint32_t) -// th_string_hash(const th_string* self) -//{ -// const char* ptr = NULL; -// size_t n = 0; -// if (self->impl.small.small) { -// ptr = self->impl.small.buf; -// n = self->impl.small.len; -// } else { -// ptr = self->impl.large.ptr; -// n = self->impl.large.len; -// } -// return th_hash_bytes(ptr, n); -// } - -TH_PRIVATE(void) -th_string_deinit(th_string* self) +TH_PRIVATE(size_t) +th_fmt_strn_append(char* buf, size_t pos, size_t len, const char* str, size_t n) { - if (!self->impl.small.small) { - th_allocator_free(self->impl.large.allocator, self->impl.large.ptr); + size_t i = 0; + while (str[i] != '\0' && i < n && pos < len - 1) { + buf[pos++] = str[i++]; } + buf[pos] = '\0'; + return i; } -/* End of src/th_string.c */ -/* Start of src/th_log.c */ -#include +TH_PRIVATE(size_t) +th_fmt_strtime(char* buf, size_t len, th_date date) +{ + static const char* weekday_table[] = + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; -/* global log instance */ + static const char* month_table[] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + size_t pos = 0; +#define ADVANCE_POS() pos += (pos < len - 1) + // Weekday + pos += th_fmt_strn_append(buf, pos, len, weekday_table[date.weekday], 3); + buf[pos] = ','; + ADVANCE_POS(); + buf[pos] = ' '; + ADVANCE_POS(); -static th_log* th_user_log = NULL; + // Day + char numbuf[16] = {0}; + size_t numlen = 0; + const char* day = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.day, &numlen); + pos += th_fmt_strn_append(buf, pos, len, day, numlen); + buf[pos] = ' '; + ADVANCE_POS(); -TH_PUBLIC(void) -th_log_set(th_log* log) -{ - th_user_log = log; -} + // Month + pos += th_fmt_strn_append(buf, pos, len, month_table[date.month], 3); + buf[pos] = ' '; + ADVANCE_POS(); -/** th_log_get - * @brief Get the current user log instance. - * @return The current user log instance, or the default log instance if no user log is set. - */ -TH_LOCAL(th_log*) -th_log_get(void) -{ - return th_user_log ? th_user_log : th_default_log_get(); -} + // Year + const char* year = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.year + 1900, &numlen); + pos += th_fmt_strn_append(buf, pos, len, year, numlen); + buf[pos] = ' '; + ADVANCE_POS(); -/* th_default_log implementation begin */ + // Hour + const char* hour = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.hour, &numlen); + pos += th_fmt_strn_append(buf, pos, len, hour, numlen); + buf[pos] = ':'; + ADVANCE_POS(); -/** th_default_log - * @brief Default log implementation, simply prints log messages to stderr. - */ -typedef struct th_default_log { - th_log base; -} th_default_log; + // Minute + const char* min = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.minute, &numlen); + pos += th_fmt_strn_append(buf, pos, len, min, numlen); + buf[pos] = ':'; + ADVANCE_POS(); -TH_LOCAL(void) -th_default_log_print(void* self, int level, const char* msg) -{ - (void)self; - (void)level; - fprintf(stderr, "%s\n", msg); + // Second + const char* sec = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.second, &numlen); + pos += th_fmt_strn_append(buf, pos, len, sec, numlen); + buf[pos] = ' '; + ADVANCE_POS(); + + // Timezone + pos += th_fmt_strn_append(buf, pos, len, "GMT", 3); + buf[pos] = '\0'; + return pos; +#undef ADVANCE_POS } +/* End of src/th_fmt.c */ +/* Start of src/th_date.c */ -TH_PRIVATE(th_log*) -th_default_log_get(void) +#include + + +TH_PUBLIC(th_duration) +th_seconds(int seconds) { - static th_default_log log = { - .base = { - .print = th_default_log_print, - }}; - return (th_log*)&log; + return (th_duration){.seconds = seconds}; } -TH_PRIVATE(void) -TH_PRINTF_FMT(2, 3) -th_log_printf(int level, const char* fmt, ...) +TH_PUBLIC(th_duration) +th_minutes(int minutes) { - th_log* log = th_log_get(); - char buffer[1024]; - va_list args; - va_start(args, fmt); - int ret = vsnprintf(buffer, sizeof(buffer), fmt, args); - va_end(args); - if (ret < 0 || (size_t)ret >= sizeof(buffer)) - goto on_error; - log->print(log, level, buffer); - return; -on_error: - log->print(log, TH_LOG_LEVEL_ERROR, "ERROR: [th_log] Failed to format log message"); + return th_seconds(minutes * 60); } -/* th_default_log implementation end */ -/* End of src/th_log.c */ -/* Start of src/th_http.c */ - -#include - -#undef TH_LOG_TAG -#define TH_LOG_TAG "http" - -#define TH_HTTP_CLOSE true -#define TH_HTTP_KEEP_ALIVE false - -TH_LOCAL(void) -th_http_destroy(void* self) +TH_PUBLIC(th_duration) +th_hours(int hours) { - th_http* http = self; - TH_LOG_TRACE("%p: Destroying http protocol instance", http); - th_conn_destroy(http->conn); - th_request_deinit(&http->request); - th_response_deinit(&http->response); - th_buf_vec_deinit(&http->buf); - th_allocator_free(http->allocator, http); + return th_minutes(hours * 60); } -TH_LOCAL(void) -th_http_handle_read_request(void* user_data, size_t len, th_err err); - -TH_LOCAL(void) -th_http_handle_write_response(void* user_data, size_t len, th_err err); - -TH_LOCAL(void) -th_http_restart(th_http* http) +TH_PUBLIC(th_duration) +th_days(int days) { - http->read_bytes = 0; - http->parsed_bytes = 0; - th_request_parser_reset(&http->parser); - th_request_reset(&http->request); - th_response_reset(&http->response); - th_conn_recv(http->conn, th_buf_vec_at(&http->buf, 0), th_buf_vec_size(&http->buf), false, th_http_handle_read_request, http); + return th_hours(days * 24); } -TH_LOCAL(void) -th_http_complete(th_http* http) +TH_PUBLIC(th_date) +th_date_now(void) { - if (http->close) { - th_http_destroy(http); - } else { - th_http_restart(http); - } + time_t t = time(NULL); + struct tm tm = {0}; + gmtime_r(&t, &tm); + th_date date = {0}; + date.year = (unsigned int)tm.tm_year & 0xFFFF; + date.month = (unsigned int)tm.tm_mon & 0xFF; + date.day = (unsigned int)tm.tm_mday & 0xFF; + date.weekday = (unsigned int)tm.tm_wday & 0xFF; + date.hour = (unsigned int)tm.tm_hour & 0xFF; + date.minute = (unsigned int)tm.tm_min & 0xFF; + date.second = (unsigned int)tm.tm_sec & 0xFF; + return date; } -TH_LOCAL(void) -th_http_write_response(th_http* http) +TH_PUBLIC(th_date) +th_date_add(th_date date, th_duration d) { - th_response_write_plan plan; - th_err err = th_response_prepare_write(&http->response, &plan); - if (err != TH_ERR_OK) { - th_http_handle_write_response(http, 0, err); - return; - } - th_conn_send(http->conn, plan.iov, plan.iovcnt, plan.file, plan.offset, plan.len, th_http_handle_write_response, http); + struct tm tm = {0}; + tm.tm_year = date.year; + tm.tm_mon = date.month; + tm.tm_mday = date.day; + tm.tm_hour = date.hour; + tm.tm_min = date.minute; + tm.tm_sec = date.second; + time_t t = mktime(&tm); + t += d.seconds; + gmtime_r(&t, &tm); + th_date new_date = {0}; + new_date.year = (unsigned int)tm.tm_year & 0xFFFF; + new_date.month = (unsigned int)tm.tm_mon & 0xFF; + new_date.day = (unsigned int)tm.tm_mday & 0xFF; + new_date.weekday = (unsigned int)tm.tm_wday & 0xFF; + new_date.hour = (unsigned int)tm.tm_hour & 0xFF; + new_date.minute = (unsigned int)tm.tm_min & 0xFF; + new_date.second = (unsigned int)tm.tm_sec & 0xFF; + return new_date; } +/* End of src/th_date.c */ +/* Start of src/th_clock.c */ -TH_LOCAL(void) -th_http_write_error_response(th_http* http, th_err err) +#ifdef TH_CONFIG_OS_POSIX +#include +#elif defined(TH_CONFIG_OS_WIN) +#include +#endif + +TH_LOCAL(th_err) +th_os_clock_monotonic_now(void* self, time_t* out) { - th_response_set_code(&http->response, TH_ERR_CODE(err)); - if (th_string_len(&http->request.uri_path) == 0) { - // Set default error message - th_printf_body(&http->response, "%d %s", TH_ERR_CODE(err), th_http_strerror((int)err)); - } - if (http->close) { - th_response_add_header(&http->response, TH_STR("Connection"), TH_STR("close")); - http->close = TH_HTTP_CLOSE; + (void)self; +#if defined(TH_CONFIG_OS_POSIX) + struct timespec ts = {0}; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return TH_ERR_SYSTEM(errno); } - th_http_write_response(http); + *out = ts.tv_sec; + return TH_ERR_OK; +#elif defined(TH_CONFIG_OS_WIN) + *out = (time_t)(GetTickCount64() / 1000); + return TH_ERR_OK; +#else + (void)out; + return TH_ERR_NOSUPPORT; +#endif } -TH_LOCAL(void) -th_http_handle_error(th_http* http, th_err err) +TH_PRIVATE(th_clock*) +th_clock_os(void) { - th_http_code_type type = th_http_code_get_type(TH_ERR_CODE(err)); - switch (type) { - case TH_HTTP_CODE_TYPE_SERVER_ERROR: - http->close = TH_HTTP_CLOSE; - break; - case TH_HTTP_CODE_TYPE_CLIENT_ERROR: - break; - default: - TH_ASSERT(0 && "Invalid error type"); - break; - } - th_http_write_error_response(http, err); + static th_clock os_clock = { + .monotonic_now = th_os_clock_monotonic_now, + }; + return &os_clock; } +/* End of src/th_clock.c */ +/* Start of src/th_timer.c */ -TH_LOCAL(void) -th_http_handle_require_1_1(th_http* http) +TH_PRIVATE(void) +th_timer_init(th_timer* timer, th_clock* clock) { - TH_LOG_ERROR("%p: Trying send a HTTP/1.1 response to a HTTP/1.0 client, sending 400 Bad Request instead", (void*)http); - th_response_set_body(&http->response, TH_STR("HTTP/1.1 required for this request")); - th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_BAD_REQUEST)); + timer->clock = clock; + timer->expire = 0; } -TH_LOCAL(th_err) -th_http_handle_options(th_router* router, th_request* request, th_response* response) +TH_PRIVATE(th_err) +th_timer_set(th_timer* timer, th_duration duration) { - // All the methods we gotta check - static const struct { - th_method method; - const char* allow; - } methods[] = { - {TH_METHOD_GET, "GET, HEAD"}, - {TH_METHOD_POST, "POST"}, - {TH_METHOD_PUT, "PUT"}, - {TH_METHOD_DELETE, "DELETE"}, - {TH_METHOD_PATCH, "PATCH"}, - }; - char allow[512] = {0}; - size_t pos = th_fmt_str_append(allow, 0, sizeof(allow), "OPTIONS"); // OPTIONS is always allowed - if (strcmp(th_string_data(&request->uri_path), "*") != 0) { - for (size_t i = 0; i < TH_ARRAY_SIZE(methods); i++) { - if (th_router_would_handle(router, methods[i].method, request)) { - pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, ", "); - pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, methods[i].allow); - } - } - } else { - for (size_t i = 0; i < TH_ARRAY_SIZE(methods); i++) { - pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, ", "); - pos += th_fmt_str_append(allow, pos, sizeof(allow) - pos, methods[i].allow); - } - } - th_err err = TH_ERR_OK; - if ((err = th_response_add_header(response, TH_STR("Allow"), th_str_make(allow, pos))) != TH_ERR_OK) - return err; - if ((err = th_response_add_header(response, TH_STR("Content-Type"), TH_STR("text/plain"))) != TH_ERR_OK) + time_t now = 0; + th_err err = timer->clock->monotonic_now(timer->clock, &now); + TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); + if (err != TH_ERR_OK) return err; + timer->expire = now + duration.seconds; return TH_ERR_OK; } -TH_LOCAL(th_err) -th_http_handle_route(th_router* router, th_request* request, th_response* response) -{ - if (request->method == TH_METHOD_OPTIONS) { - return th_http_handle_options(router, request, response); - } else { - return th_router_handle(router, request, response); - } -} - -TH_LOCAL(void) -th_http_prehandle_request(th_http* http) +TH_PRIVATE(bool) +th_timer_expired(th_timer* timer) { - th_request* request = &http->request; - th_response* response = &http->response; - if (request->method == TH_METHOD_HEAD) { - response->only_headers = true; // only write headers - request->method = TH_METHOD_GET; // pretend it's a GET request - } + time_t now = 0; + th_err err = timer->clock->monotonic_now(timer->clock, &now); + TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); + /* We don't return the error here, as it's already handled in th_timer_set + * and we can safely assume that the error won't happen here. */ + if (err != TH_ERR_OK) + return true; + return now >= timer->expire; } -TH_LOCAL(void) -th_http_handle_request_and_write_response(th_http* http) +TH_PRIVATE(th_timer) +th_timer_from_duration(th_clock* clock, th_duration duration) { - th_request* request = &http->request; - th_response* response = &http->response; - th_http_prehandle_request(http); - th_err err = th_http_error(th_http_handle_route(http->router, &http->request, &http->response)); - switch (th_http_code_get_type(TH_ERR_CODE(err))) { - case TH_HTTP_CODE_TYPE_INFORMATIONAL: - if (request->version == 0) { - th_http_handle_require_1_1(http); - return; - } - break; - case TH_HTTP_CODE_TYPE_SERVER_ERROR: - case TH_HTTP_CODE_TYPE_CLIENT_ERROR: - th_http_handle_error(http, err); - return; - default: - // All other types don't require any special handling - break; - } - // All good, write the response - if (request->close) { - th_response_add_header(response, TH_STR("Connection"), TH_STR("close")); - http->close = true; - } else { - th_response_add_header(response, TH_STR("Connection"), TH_STR("keep-alive")); - } - TH_LOG_TRACE("%p: Write response %p", http, response); - th_http_write_response(http); + th_timer timer; + th_timer_init(&timer, clock); + th_timer_set(&timer, duration); + return timer; } -TH_LOCAL(void) -th_http_handle_read_request(void* user_data, size_t len, th_err err) +TH_PRIVATE(th_duration) +th_timer_remaining(const th_timer* timer) { - th_http* http = user_data; - if (err != TH_ERR_OK) { - TH_LOG_DEBUG("%p: Read error: %s", http, th_strerror(err)); - http->close = TH_HTTP_CLOSE; // No other choice if we can't even read the request - th_http_complete(http); - return; - } - http->read_bytes += len; - size_t parsed = 0; - th_str parser_input = (th_str){.ptr = th_buf_vec_at(&http->buf, http->parsed_bytes), - .len = http->read_bytes - http->parsed_bytes}; - if ((err = th_request_parser_parse(&http->parser, &http->request, parser_input, &parsed)) != TH_ERR_OK) { - th_http_write_error_response(http, th_http_error(err)); - return; - } - if (th_request_parser_done(&http->parser)) { - th_http_handle_request_and_write_response(http); - return; - } - // If we haven't parsed the whole request, we need to read more data - http->parsed_bytes += parsed; - if (!th_request_parser_header_done(&http->parser)) { - if (http->read_bytes == th_buf_vec_size(&http->buf)) { - if (th_buf_vec_size(&http->buf) < TH_CONFIG_LARGE_HEADER_LEN) { - th_buf_vec_resize(&http->buf, TH_CONFIG_LARGE_HEADER_LEN); - } else { - th_http_write_error_response(http, TH_ERR_HTTP(TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE)); - return; - } - } - th_conn_recv(http->conn, th_buf_vec_at(&http->buf, http->read_bytes), - th_buf_vec_size(&http->buf) - http->read_bytes, false, th_http_handle_read_request, http); - } else { - size_t content_received = http->read_bytes - http->parsed_bytes; - size_t content_len = th_request_parser_content_len(&http->parser); - if (content_len > TH_MAX_BODY_LEN) { - TH_LOG_WARN("Request body too large, rejecting request"); - th_http_write_error_response(http, TH_ERR_HTTP(TH_CODE_PAYLOAD_TOO_LARGE)); - return; - } - size_t remaining = content_len - content_received; - if (http->read_bytes + remaining > th_buf_vec_size(&http->buf)) { - memmove(th_buf_vec_at(&http->buf, 0), th_buf_vec_at(&http->buf, http->parsed_bytes), content_received); - http->read_bytes = content_received; - http->parsed_bytes = 0; - if (content_len > th_buf_vec_size(&http->buf)) { - th_buf_vec_resize(&http->buf, content_len); - } - } - th_conn_recv(http->conn, th_buf_vec_at(&http->buf, http->read_bytes), - remaining, true, th_http_handle_read_request, http); - } + time_t now = 0; + th_err err = timer->clock->monotonic_now(timer->clock, &now); + TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); + if (err != TH_ERR_OK) + return th_seconds(0); + return th_seconds(TH_MAX((int)(timer->expire - now), 0)); } -TH_LOCAL(void) -th_http_handle_write_response(void* user_data, size_t len, th_err err) +TH_PRIVATE(bool) +th_timer_less(const th_timer* a, const th_timer* b) { - th_http* http = user_data; - (void)len; - if (err != TH_ERR_OK) { - TH_LOG_ERROR("%p: Write error: %s", (void*)http, th_strerror(err)); - http->close = TH_HTTP_CLOSE; // Connection is broken, close it - } else { - TH_LOG_TRACE("%p: Write response of %d bytes", http, (int)len); - } - th_http_complete(http); + return a->expire < b->expire; } +/* End of src/th_timer.c */ +/* Start of src/th_conn_tracker.c */ TH_LOCAL(void) -th_http_start(void* self) +th_conn_tracker_on_conn_init(th_conn_observer* observer, th_conn_observable* observable) { - th_http* http = self; - TH_LOG_TRACE("%p: Starting", http); - th_buf_vec_resize(&http->buf, TH_CONFIG_SMALL_HEADER_LEN); - th_conn_recv(http->conn, th_buf_vec_at(&http->buf, 0), th_buf_vec_size(&http->buf), false, th_http_handle_read_request, http); + th_conn_tracker* tracker = (th_conn_tracker*)observer; + th_conn_observable_list_push_back(&tracker->observables, observable); + ++tracker->count; } TH_LOCAL(void) -th_http_init(th_http* http, const th_conn_tracker* tracker, th_conn* conn, - th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) +th_conn_tracker_on_conn_deinit(th_conn_observer* observer, th_conn_observable* observable) { - allocator = allocator ? allocator : th_default_allocator_get(); - th_request_parser_init(&http->parser); - th_request_init(&http->request, allocator); - th_response_init(&http->response, dir_mgr, fcache, allocator); - th_buf_vec_init(&http->buf, allocator); - http->tracker = tracker; - http->conn = conn; - http->router = router; - http->dir_mgr = dir_mgr; - http->fcache = fcache; - http->allocator = allocator; - http->read_bytes = 0; - http->parsed_bytes = 0; - http->close = TH_HTTP_KEEP_ALIVE; + th_conn_tracker* tracker = (th_conn_tracker*)observer; + th_conn_observable_list_erase(&tracker->observables, observable); + --tracker->count; + if (tracker->task) { + th_task* task = TH_MOVE_PTR(tracker->task); + th_task_complete(task); + } +} + +TH_PRIVATE(void) +th_conn_tracker_init(th_conn_tracker* tracker) +{ + tracker->base.on_init = th_conn_tracker_on_conn_init; + tracker->base.on_deinit = th_conn_tracker_on_conn_deinit; + tracker->observables = (th_conn_observable_list){0}; + tracker->task = NULL; + tracker->count = 0; +} + +TH_PRIVATE(void) +th_conn_tracker_cancel_all(th_conn_tracker* conn_tracker) +{ + th_conn_observable* observable = NULL; + for (observable = th_conn_observable_list_front(&conn_tracker->observables); + observable != NULL; + observable = th_conn_observable_list_next(observable)) { + th_conn* client = &observable->base; + th_conn_cancel(client); + } +} + +TH_PRIVATE(void) +th_conn_tracker_async_wait(th_conn_tracker* conn_tracker, th_task* task) +{ + TH_ASSERT(conn_tracker->task == NULL && "Task already set"); + TH_ASSERT(th_conn_observable_list_front(&conn_tracker->observables) != NULL && "No clients to wait for"); + conn_tracker->task = task; } +TH_PRIVATE(size_t) +th_conn_tracker_count(const th_conn_tracker* conn_tracker) +{ + return conn_tracker->count; +} + +TH_PRIVATE(void) +th_conn_tracker_deinit(th_conn_tracker* tracker) +{ + (void)tracker; + TH_ASSERT(th_conn_observable_list_front(&tracker->observables) == NULL && "All clients must be destroyed before deinit"); +} +/* End of src/th_conn_tracker.c */ +/* Start of src/th_url_decode.c */ + TH_LOCAL(th_err) -th_http_create(th_http** out, const th_conn_tracker* tracker, th_conn* conn, - th_router* router, th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) +th_url_decode_next(th_str str, size_t* pos, char* out, th_url_decode_type type) { - th_http* http = th_allocator_alloc(allocator, sizeof(th_http)); - if (!http) - return TH_ERR_BAD_ALLOC; - th_http_init(http, tracker, conn, router, dir_mgr, fcache, allocator); - *out = http; + size_t i = *pos; + if (str.ptr[i] == '%') { + int c = 0; + for (size_t k = 0; k < 2; k++) { + if (i + 1 + k >= str.len) + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + c <<= 4; + if (str.ptr[i + 1 + k] >= '0' && str.ptr[i + 1 + k] <= '9') { + c |= str.ptr[i + 1 + k] - '0'; + } else if (str.ptr[i + 1 + k] >= 'a' && str.ptr[i + 1 + k] <= 'f') { + c |= str.ptr[i + 1 + k] - 'a' + 10; + } else if (str.ptr[i + 1 + k] >= 'A' && str.ptr[i + 1 + k] <= 'F') { + c |= str.ptr[i + 1 + k] - 'A' + 10; + } else { + return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); + } + } + *out = (char)c; + i += 3; + } else if (type == TH_URL_DECODE_TYPE_QUERY && str.ptr[i] == '+') { + *out = ' '; + i++; + } else { + *out = str.ptr[i++]; + } + *pos = i; return TH_ERR_OK; } -TH_LOCAL(void) -th_http_upgrader_upgrade(void* self, th_conn* conn) +TH_LOCAL(size_t) +th_url_decode_literal_run(th_str input, size_t pos, th_url_decode_type type) { - th_http_upgrader* upgrader = self; - th_http* http = NULL; + size_t start = pos; + while (pos < input.len && input.ptr[pos] != '%' + && !(type == TH_URL_DECODE_TYPE_QUERY && input.ptr[pos] == '+')) + pos++; + return pos - start; +} + +TH_PRIVATE(th_err) +th_url_decode_string(th_str input, th_string* output, th_url_decode_type type) +{ + th_string_clear(output); + th_err err = TH_ERR_OK; - if ((err = th_http_create(&http, upgrader->tracker, conn, upgrader->router, upgrader->dir_mgr, upgrader->fcache, upgrader->allocator)) != TH_ERR_OK) { - TH_LOG_ERROR("Failed to create http instance: %s", th_strerror(err)); - th_conn_destroy(conn); - return; - } - if (th_conn_tracker_count(upgrader->tracker) > TH_CONFIG_MAX_CONNECTIONS) { - TH_LOG_WARN("Too many connections, rejecting new connection"); - th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_SERVICE_UNAVAILABLE)); - return; + if (input.len == 0) + return TH_ERR_OK; + size_t i = 0; + while (i < input.len) { + size_t run = th_url_decode_literal_run(input, i, type); + if (run > 0) { + if ((err = th_string_append(output, th_str_substr(input, i, run))) != TH_ERR_OK) + return err; + i += run; + continue; + } + char c; + if ((err = th_url_decode_next(input, &i, &c, type)) != TH_ERR_OK) { + return err; + } + if ((err = th_string_push_back(output, c)) != TH_ERR_OK) { + return err; + } } - th_http_start(http); + return TH_ERR_OK; } +/* End of src/th_url_decode.c */ +/* Start of src/th_sha1.c */ -TH_PRIVATE(void) -th_http_upgrader_init(th_http_upgrader* upgrader, const th_conn_tracker* tracker, th_router* router, - th_dir_mgr* dir_mgr, th_fcache* fcache, th_allocator* allocator) +#include +#include + +TH_LOCAL(uint32_t) +th_sha1_rotl(uint32_t x, int n) { - th_conn_upgrader_init(&upgrader->base, th_http_upgrader_upgrade); - upgrader->tracker = tracker; - upgrader->router = router; - upgrader->dir_mgr = dir_mgr; - upgrader->fcache = fcache; - upgrader->allocator = allocator; + return (x << n) | (x >> (32 - n)); } -/* End of src/th_http.c */ -/* Start of src/th_fmt.c */ - -static const char* th_fmt_num_table[] = - {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", - "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", - "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", - "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", - "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", - "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", - "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", - "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", - "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", - "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", - "110", "111", "112", "113", "114", "115", "116", "117", "118", "119", - "120", "121", "122", "123", "124", "125", "126", "127", "128", "129", - "130", "131", "132", "133", "134", "135", "136", "137", "138", "139", - "140", "141", "142", "143", "144", "145", "146", "147", "148", "149", - "150", "151", "152", "153", "154", "155", "156", "157", "158", "159", - "160", "161", "162", "163", "164", "165", "166", "167", "168", "169", - "170", "171", "172", "173", "174", "175", "176", "177", "178", "179", - "180", "181", "182", "183", "184", "185", "186", "187", "188", "189", - "190", "191", "192", "193", "194", "195", "196", "197", "198", "199", - "200", "201", "202", "203", "204", "205", "206", "207", "208", "209", - "210", "211", "212", "213", "214", "215", "216", "217", "218", "219", - "220", "221", "222", "223", "224", "225", "226", "227", "228", "229", - "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", - "240", "241", "242", "243", "244", "245", "246", "247", "248", "249", - "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", - "260", "261", "262", "263", "264", "265", "266", "267", "268", "269", - "270", "271", "272", "273", "274", "275", "276", "277", "278", "279", - "280", "281", "282", "283", "284", "285", "286", "287", "288", "289", - "290", "291", "292", "293", "294", "295", "296", "297", "298", "299", - "300", "301", "302", "303", "304", "305", "306", "307", "308", "309", - "310", "311", "312", "313", "314", "315", "316", "317", "318", "319", - "320", "321", "322", "323", "324", "325", "326", "327", "328", "329", - "330", "331", "332", "333", "334", "335", "336", "337", "338", "339", - "340", "341", "342", "343", "344", "345", "346", "347", "348", "349", - "350", "351", "352", "353", "354", "355", "356", "357", "358", "359", - "360", "361", "362", "363", "364", "365", "366", "367", "368", "369", - "370", "371", "372", "373", "374", "375", "376", "377", "378", "379", - "380", "381", "382", "383", "384", "385", "386", "387", "388", "389", - "390", "391", "392", "393", "394", "395", "396", "397", "398", "399", - "400", "401", "402", "403", "404", "405", "406", "407", "408", "409", - "410", "411", "412", "413", "414", "415", "416", "417", "418", "419", - "420", "421", "422", "423", "424", "425", "426", "427", "428", "429", - "430", "431", "432", "433", "434", "435", "436", "437", "438", "439", - "440", "441", "442", "443", "444", "445", "446", "447", "448", "449", - "450", "451", "452", "453", "454", "455", "456", "457", "458", "459", - "460", "461", "462", "463", "464", "465", "466", "467", "468", "469", - "470", "471", "472", "473", "474", "475", "476", "477", "478", "479", - "480", "481", "482", "483", "484", "485", "486", "487", "488", "489", - "490", "491", "492", "493", "494", "495", "496", "497", "498", "499", - "500", "501", "502", "503", "504", "505", "506", "507", "508", "509", - "510", "511", "512", "513", "514", "515", "516", "517", "518", "519", - "520", "521", "522", "523", "524", "525", "526", "527", "528", "529", - "530", "531", "532", "533", "534", "535", "536", "537", "538", "539", - "540", "541", "542", "543", "544", "545", "546", "547", "548", "549", - "550", "551", "552", "553", "554", "555", "556", "557", "558", "559", - "560", "561", "562", "563", "564", "565", "566", "567", "568", "569", - "570", "571", "572", "573", "574", "575", "576", "577", "578", "579", - "580", "581", "582", "583", "584", "585", "586", "587", "588", "589", - "590", "591", "592", "593", "594", "595", "596", "597", "598", "599"}; +TH_LOCAL(void) +th_sha1_process_block(uint32_t state[5], const unsigned char block[64]) +{ + uint32_t w[80]; + for (int i = 0; i < 16; ++i) { + w[i] = ((uint32_t)block[i * 4] << 24) | ((uint32_t)block[i * 4 + 1] << 16) + | ((uint32_t)block[i * 4 + 2] << 8) | (uint32_t)block[i * 4 + 3]; + } + for (int i = 16; i < 80; ++i) { + w[i] = th_sha1_rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + uint32_t a = state[0], b = state[1], c = state[2], d = state[3], e = state[4]; + for (int i = 0; i < 80; ++i) { + uint32_t f, k; + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + uint32_t temp = th_sha1_rotl(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = th_sha1_rotl(b, 30); + b = a; + a = temp; + } -TH_PRIVATE(const char*) -th_fmt_uint_to_str(char* buf, size_t len, unsigned int value) + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; +} + +TH_PRIVATE(void) +th_sha1(th_buffer data, unsigned char digest[TH_SHA1_DIGEST_LEN]) { - if (value < TH_ARRAY_SIZE(th_fmt_num_table)) { - return th_fmt_num_table[value]; + uint32_t state[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u}; + const unsigned char* bytes = (const unsigned char*)data.ptr; + size_t len = data.len; + size_t full_blocks = len / 64; + for (size_t i = 0; i < full_blocks; ++i) { + th_sha1_process_block(state, bytes + i * 64); } - buf[len - 1] = '\0'; - size_t i = len - 2; - unsigned int v = value; - while (v > 0 && i > 0) { - buf[i--] = '0' + (char)(v % 10); - v /= 10; + unsigned char tail[128] = {0}; + size_t tail_len = len - full_blocks * 64; + memcpy(tail, bytes + full_blocks * 64, tail_len); + tail[tail_len] = 0x80; + size_t padded_len = tail_len < 56 ? 64 : 128; + uint64_t bit_len = (uint64_t)len * 8; + for (size_t i = 0; i < 8; ++i) { + tail[padded_len - 1 - i] = (unsigned char)(bit_len >> (8 * i)); + } + th_sha1_process_block(state, tail); + if (padded_len == 128) { + th_sha1_process_block(state, tail + 64); + } + + for (int i = 0; i < 5; ++i) { + digest[i * 4] = (unsigned char)(state[i] >> 24); + digest[i * 4 + 1] = (unsigned char)(state[i] >> 16); + digest[i * 4 + 2] = (unsigned char)(state[i] >> 8); + digest[i * 4 + 3] = (unsigned char)state[i]; } - return &buf[i + 1]; } +/* End of src/th_sha1.c */ +/* Start of src/th_base64.c */ -TH_PRIVATE(const char*) -th_fmt_uint_to_str_ex(char* buf, size_t len, unsigned int val, size_t* out_len) +#include + +static const char th_base64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +TH_LOCAL(size_t) +th_base64_encoded_len(size_t len) { - if (val < TH_ARRAY_SIZE(th_fmt_num_table)) { - *out_len = val < 10 ? 1 : (val < 100 ? 2 : 3); - return th_fmt_num_table[val]; - } + return ((len + 2) / 3) * 4; +} - buf[len - 1] = '\0'; - size_t i = len - 2; - unsigned int v = val; - while (v > 0 && i > 0) { - buf[i--] = '0' + (char)(v % 10); - v /= 10; +TH_PRIVATE(th_err) +th_base64_encode(th_str input, th_string* output) +{ + th_err err = th_string_resize(output, th_base64_encoded_len(input.len), '\0'); + if (err != TH_ERR_OK) + return err; + if (input.len == 0) + return TH_ERR_OK; + + const unsigned char* bytes = (const unsigned char*)input.ptr; + char* out = th_string_at(output, 0); + size_t i = 0; + size_t o = 0; + for (; i + 3 <= input.len; i += 3) { + uint32_t n = ((uint32_t)bytes[i] << 16) | ((uint32_t)bytes[i + 1] << 8) | (uint32_t)bytes[i + 2]; + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 6) & 0x3F]; + out[o++] = th_base64_alphabet[n & 0x3F]; + } + size_t remaining = input.len - i; + if (remaining == 1) { + uint32_t n = (uint32_t)bytes[i] << 16; + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = '='; + out[o++] = '='; + } else if (remaining == 2) { + uint32_t n = ((uint32_t)bytes[i] << 16) | ((uint32_t)bytes[i + 1] << 8); + out[o++] = th_base64_alphabet[(n >> 18) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 12) & 0x3F]; + out[o++] = th_base64_alphabet[(n >> 6) & 0x3F]; + out[o++] = '='; } - *out_len = len - i - 2; - return &buf[i + 1]; + return TH_ERR_OK; } +/* End of src/th_base64.c */ +/* Start of src/th_ws_handshake.c */ -TH_PRIVATE(size_t) -th_fmt_str_append(char* buf, size_t pos, size_t len, const char* str) + +#include + +// Max length of a base64-encoded 16-byte nonce, per RFC 6455. +#define TH_WS_HANDSHAKE_KEY_MAX_LEN 24 +#define TH_WS_HANDSHAKE_GUID_LEN 36 +#define TH_WS_HANDSHAKE_GUID TH_STR("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +TH_LOCAL(bool) +th_ws_connection_has_upgrade_token(th_str value) { - size_t i = 0; - while (str[i] != '\0' && pos < len - 1) { - buf[pos++] = str[i++]; + size_t pos = 0; + while (pos <= value.len) { + size_t comma = th_str_find_first(value, pos, ','); + size_t end = comma == th_str_npos ? value.len : comma; + th_str token = th_str_trim(th_str_substr(value, pos, end - pos)); + if (th_str_ieq(token, TH_STR("upgrade"))) + return true; + if (comma == th_str_npos) + break; + pos = comma + 1; } - buf[pos] = '\0'; - return i; + return false; } -TH_PRIVATE(size_t) -th_fmt_strn_append(char* buf, size_t pos, size_t len, const char* str, size_t n) +TH_PRIVATE(bool) +th_ws_is_handshake(th_request* request) { - size_t i = 0; - while (str[i] != '\0' && i < n && pos < len - 1) { - buf[pos++] = str[i++]; + if (request->method != TH_METHOD_GET) + return false; + if (!th_str_ieq(th_request_get_header(request, TH_STR("upgrade")), TH_STR("websocket"))) + return false; + if (!th_ws_connection_has_upgrade_token(th_request_get_header(request, TH_STR("connection")))) + return false; + if (th_str_empty(th_request_get_header(request, TH_STR("sec-websocket-key")))) + return false; + if (!th_str_eq(th_request_get_header(request, TH_STR("sec-websocket-version")), TH_STR("13"))) + return false; + return true; +} + +TH_PRIVATE(th_err) +th_ws_handshake_accept_key(th_str key, th_string* out) +{ + if (key.len > TH_WS_HANDSHAKE_KEY_MAX_LEN) + return TH_ERR_INVALID_ARG; + th_str guid = TH_WS_HANDSHAKE_GUID; + char concat[TH_WS_HANDSHAKE_KEY_MAX_LEN + TH_WS_HANDSHAKE_GUID_LEN]; + memcpy(concat, key.ptr, key.len); + memcpy(concat + key.len, guid.ptr, guid.len); + + unsigned char digest[TH_SHA1_DIGEST_LEN]; + th_sha1((th_buffer){concat, key.len + guid.len}, digest); + + return th_base64_encode(th_str_make((const char*)digest, TH_SHA1_DIGEST_LEN), out); +} +/* End of src/th_ws_handshake.c */ +/* Start of src/th_ws_frame.c */ + +TH_LOCAL(unsigned char) +th_ws_frame_opcode(th_ws_frame_type type) +{ + switch (type) { + case TH_WS_FRAME_TEXT: + return 0x1; + case TH_WS_FRAME_BINARY: + return 0x2; + case TH_WS_FRAME_CLOSE: + return 0x8; + case TH_WS_FRAME_PING: + return 0x9; + default: + return 0xA; // TH_WS_FRAME_PONG } - buf[pos] = '\0'; - return i; } TH_PRIVATE(size_t) -th_fmt_strtime(char* buf, size_t len, th_date date) +th_ws_frame_header_write(unsigned char* header, th_ws_frame_type type, size_t len) { - static const char* weekday_table[] = - {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + header[0] = 0x80 | th_ws_frame_opcode(type); // FIN=1, no fragmentation on send + if (len < 126) { + header[1] = (unsigned char)len; + return 2; + } + if (len <= 0xFFFF) { + header[1] = 126; + header[2] = (unsigned char)(len >> 8); + header[3] = (unsigned char)len; + return 4; + } + header[1] = 127; + for (int i = 0; i < 8; ++i) + header[2 + i] = (unsigned char)(len >> (8 * (7 - i))); + return 10; +} +/* End of src/th_ws_frame.c */ +/* Start of src/th_ws_frame_parser.c */ - static const char* month_table[] = - {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - size_t pos = 0; -#define ADVANCE_POS() pos += (pos < len - 1) - // Weekday - pos += th_fmt_strn_append(buf, pos, len, weekday_table[date.weekday], 3); - buf[pos] = ','; - ADVANCE_POS(); - buf[pos] = ' '; - ADVANCE_POS(); - // Day - char numbuf[16] = {0}; - size_t numlen = 0; - const char* day = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.day, &numlen); - pos += th_fmt_strn_append(buf, pos, len, day, numlen); - buf[pos] = ' '; - ADVANCE_POS(); +#include - // Month - pos += th_fmt_strn_append(buf, pos, len, month_table[date.month], 3); - buf[pos] = ' '; - ADVANCE_POS(); +#define TH_WS_OPCODE_CONTINUATION 0x0 +#define TH_WS_OPCODE_TEXT 0x1 +#define TH_WS_OPCODE_BINARY 0x2 +#define TH_WS_OPCODE_CLOSE 0x8 +#define TH_WS_OPCODE_PING 0x9 +#define TH_WS_OPCODE_PONG 0xA - // Year - const char* year = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.year + 1900, &numlen); - pos += th_fmt_strn_append(buf, pos, len, year, numlen); - buf[pos] = ' '; - ADVANCE_POS(); +TH_LOCAL(bool) +th_ws_opcode_is_control(unsigned char opcode) +{ + return opcode >= TH_WS_OPCODE_CLOSE; +} - // Hour - const char* hour = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.hour, &numlen); - pos += th_fmt_strn_append(buf, pos, len, hour, numlen); - buf[pos] = ':'; - ADVANCE_POS(); +TH_LOCAL(size_t) +th_ws_frame_parser_fill_header(th_ws_frame_parser* parser, const char* data, size_t len, size_t needed) +{ + size_t remaining = needed - parser->header_len; + size_t n = len < remaining ? len : remaining; + memcpy(parser->header_buf + parser->header_len, data, n); + parser->header_len += n; + return n; +} - // Minute - const char* min = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.minute, &numlen); - pos += th_fmt_strn_append(buf, pos, len, min, numlen); - buf[pos] = ':'; - ADVANCE_POS(); +TH_LOCAL(th_err) +th_ws_frame_parser_validate_base_header(th_ws_frame_parser* parser, size_t* header_len) +{ + unsigned char byte0 = parser->header_buf[0]; + unsigned char byte1 = parser->header_buf[1]; + if ((byte0 & 0x70) != 0) // RSV1-3 must be 0, no extensions negotiated + return TH_ERR_SYSTEM(TH_EPROTO); + if ((byte1 & 0x80) == 0) // client frames must be masked + return TH_ERR_SYSTEM(TH_EPROTO); + + unsigned char opcode = byte0 & 0x0F; + bool fin = (byte0 & 0x80) != 0; + unsigned char len7 = byte1 & 0x7F; + + bool known_opcode = opcode == TH_WS_OPCODE_CONTINUATION || opcode == TH_WS_OPCODE_TEXT + || opcode == TH_WS_OPCODE_BINARY || opcode == TH_WS_OPCODE_CLOSE || opcode == TH_WS_OPCODE_PING + || opcode == TH_WS_OPCODE_PONG; + if (!known_opcode) + return TH_ERR_SYSTEM(TH_EPROTO); + if (th_ws_opcode_is_control(opcode) && !fin) // control frames can't be fragmented + return TH_ERR_SYSTEM(TH_EPROTO); + if (th_ws_opcode_is_control(opcode) && len7 > 125) // RFC 6455 5.5 + return TH_ERR_SYSTEM(TH_EPROTO); + if (opcode == TH_WS_OPCODE_CONTINUATION && parser->message_opcode == 0) // nothing to continue + return TH_ERR_SYSTEM(TH_EPROTO); + if ((opcode == TH_WS_OPCODE_TEXT || opcode == TH_WS_OPCODE_BINARY) && parser->message_opcode != 0) + return TH_ERR_SYSTEM(TH_EPROTO); // data frame while a fragmented message is still in progress + + size_t ext_len_bytes = len7 == 126 ? 2 : len7 == 127 ? 8 : 0; + *header_len = 2 + ext_len_bytes + 4; + return TH_ERR_OK; +} - // Second - const char* sec = th_fmt_uint_to_str_ex(numbuf, sizeof(numbuf), date.second, &numlen); - pos += th_fmt_strn_append(buf, pos, len, sec, numlen); - buf[pos] = ' '; - ADVANCE_POS(); +TH_LOCAL(th_err) +th_ws_frame_parser_finish_header(th_ws_frame_parser* parser, th_buf_vec* payload) +{ + unsigned char byte1 = parser->header_buf[1]; + unsigned char len7 = byte1 & 0x7F; + size_t ext_len_bytes = len7 == 126 ? 2 : len7 == 127 ? 8 : 0; - // Timezone - pos += th_fmt_strn_append(buf, pos, len, "GMT", 3); - buf[pos] = '\0'; - return pos; -#undef ADVANCE_POS + parser->fin = (parser->header_buf[0] & 0x80) != 0; + parser->opcode = parser->header_buf[0] & 0x0F; + + uint64_t payload_len = len7; + if (ext_len_bytes > 0) { + payload_len = 0; + for (size_t i = 0; i < ext_len_bytes; ++i) + payload_len = (payload_len << 8) | parser->header_buf[2 + i]; + } + if (!th_ws_opcode_is_control(parser->opcode) && th_buf_vec_size(payload) + payload_len > TH_CONFIG_WS_MAX_MESSAGE_LEN) + return TH_ERR_SYSTEM(TH_EPROTO); + + memcpy(parser->mask_key, parser->header_buf + 2 + ext_len_bytes, 4); + parser->payload_len = payload_len; + parser->payload_read = 0; + parser->state = TH_WS_FRAME_PARSER_STATE_PAYLOAD; + return TH_ERR_OK; } -/* End of src/th_fmt.c */ -/* Start of src/th_date.c */ -#include +TH_LOCAL(th_err) +th_ws_frame_parser_do_header(th_ws_frame_parser* parser, th_buf_vec* payload, const char* data, size_t len, + size_t* parsed) +{ + *parsed = th_ws_frame_parser_fill_header(parser, data, len, 2); + if (parser->header_len < 2) + return TH_ERR_OK; + size_t header_len = 0; + th_err err = th_ws_frame_parser_validate_base_header(parser, &header_len); + if (err != TH_ERR_OK) + return err; -TH_PUBLIC(th_duration) -th_seconds(int seconds) + *parsed += th_ws_frame_parser_fill_header(parser, data + *parsed, len - *parsed, header_len); + if (parser->header_len < header_len) + return TH_ERR_OK; + + return th_ws_frame_parser_finish_header(parser, payload); +} + +TH_LOCAL(th_err) +th_ws_frame_parser_append_payload(th_buf_vec* payload, const unsigned char* data, size_t len) { - return (th_duration){.seconds = seconds}; + if (len == 0) + return TH_ERR_OK; + size_t start = th_buf_vec_size(payload); + th_err err = th_buf_vec_resize(payload, start + len); + if (err != TH_ERR_OK) + return err; + memcpy(th_buf_vec_at(payload, start), data, len); + return TH_ERR_OK; } -TH_PUBLIC(th_duration) -th_minutes(int minutes) +TH_LOCAL(void) +th_ws_frame_parser_frame_done(th_ws_frame_parser* parser, bool* message_done, th_ws_frame_type* type) { - return th_seconds(minutes * 60); + if (th_ws_opcode_is_control(parser->opcode)) { + *type = parser->opcode == TH_WS_OPCODE_CLOSE ? TH_WS_FRAME_CLOSE + : parser->opcode == TH_WS_OPCODE_PING ? TH_WS_FRAME_PING + : TH_WS_FRAME_PONG; + *message_done = true; + } else { + if (parser->opcode != TH_WS_OPCODE_CONTINUATION) + parser->message_opcode = parser->opcode; + if (parser->fin) { + *type = parser->message_opcode == TH_WS_OPCODE_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; + parser->message_opcode = 0; + *message_done = true; + } + } + parser->state = TH_WS_FRAME_PARSER_STATE_HEADER; + parser->header_len = 0; } -TH_PUBLIC(th_duration) -th_hours(int hours) +// mask_key indexing uses payload_read so it stays correct across chunks. +TH_LOCAL(th_err) +th_ws_frame_parser_do_payload(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + bool* message_done, th_ws_frame_type* type) { - return th_minutes(hours * 60); + uint64_t remaining = parser->payload_len - parser->payload_read; + size_t n = (uint64_t)len < remaining ? len : (size_t)remaining; + *parsed = n; + + for (size_t i = 0; i < n; ++i) + data[i] = (char)((unsigned char)data[i] ^ parser->mask_key[(parser->payload_read + i) % 4]); + + th_err err = TH_ERR_OK; + if (!th_ws_opcode_is_control(parser->opcode)) + err = th_ws_frame_parser_append_payload(payload, (const unsigned char*)data, n); + parser->payload_read += n; + if (err != TH_ERR_OK) + return err; + + *message_done = false; + if (parser->payload_read == parser->payload_len) + th_ws_frame_parser_frame_done(parser, message_done, type); + return TH_ERR_OK; } -TH_PUBLIC(th_duration) -th_days(int days) +TH_LOCAL(th_err) +th_ws_frame_parser_parse_next(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + bool* message_done, th_ws_frame_type* type) { - return th_hours(days * 24); + switch (parser->state) { + case TH_WS_FRAME_PARSER_STATE_HEADER: + *message_done = false; + return th_ws_frame_parser_do_header(parser, payload, data, len, parsed); + case TH_WS_FRAME_PARSER_STATE_PAYLOAD: + return th_ws_frame_parser_do_payload(parser, data, len, payload, parsed, message_done, type); + default: + *parsed = 0; + *message_done = false; + return TH_ERR_OK; + } } -TH_PUBLIC(th_date) -th_date_now(void) +TH_PRIVATE(th_err) +th_ws_frame_parser_parse(th_ws_frame_parser* parser, char* data, size_t len, th_buf_vec* payload, size_t* parsed, + th_ws_frame_type* type) { - time_t t = time(NULL); - struct tm tm = {0}; - gmtime_r(&t, &tm); - th_date date = {0}; - date.year = (unsigned int)tm.tm_year & 0xFFFF; - date.month = (unsigned int)tm.tm_mon & 0xFF; - date.day = (unsigned int)tm.tm_mday & 0xFF; - date.weekday = (unsigned int)tm.tm_wday & 0xFF; - date.hour = (unsigned int)tm.tm_hour & 0xFF; - date.minute = (unsigned int)tm.tm_min & 0xFF; - date.second = (unsigned int)tm.tm_sec & 0xFF; - return date; + th_err err = TH_ERR_OK; + *parsed = 0; + for (;;) { + size_t p = 0; + bool message_done = false; + if ((err = th_ws_frame_parser_parse_next(parser, data, len, payload, &p, &message_done, type)) != TH_ERR_OK) { + *parsed += p; + return err; + } + data += p; + len -= p; + *parsed += p; + if (message_done) + return TH_ERR_OK; + // check message_done first: a zero-length payload also has p == 0 + if (p == 0 && len == 0) + return TH_ERR_SYSTEM(TH_EAGAIN); + } } +/* End of src/th_ws_frame_parser.c */ +/* Start of src/th_ring.c */ -TH_PUBLIC(th_date) -th_date_add(th_date date, th_duration d) + +#include + +// Chunk header + backing buffer live in one allocation - data points at +// an offset into the same block, rounded up so it's th_max_align-aligned. +#define TH_RING_CHUNK_HEADER_LEN TH_ALIGNUP(sizeof(th_ring_chunk), TH_ALIGNOF(th_max_align)) + +TH_LOCAL(th_ring_chunk*) +th_ring_chunk_create(th_allocator* allocator, size_t capacity) { - struct tm tm = {0}; - tm.tm_year = date.year; - tm.tm_mon = date.month; - tm.tm_mday = date.day; - tm.tm_hour = date.hour; - tm.tm_min = date.minute; - tm.tm_sec = date.second; - time_t t = mktime(&tm); - t += d.seconds; - gmtime_r(&t, &tm); - th_date new_date = {0}; - new_date.year = (unsigned int)tm.tm_year & 0xFFFF; - new_date.month = (unsigned int)tm.tm_mon & 0xFF; - new_date.day = (unsigned int)tm.tm_mday & 0xFF; - new_date.weekday = (unsigned int)tm.tm_wday & 0xFF; - new_date.hour = (unsigned int)tm.tm_hour & 0xFF; - new_date.minute = (unsigned int)tm.tm_min & 0xFF; - new_date.second = (unsigned int)tm.tm_sec & 0xFF; - return new_date; + th_ring_chunk* chunk = th_allocator_alloc(allocator, TH_RING_CHUNK_HEADER_LEN + capacity); + if (!chunk) + return NULL; + chunk->data = (unsigned char*)chunk + TH_RING_CHUNK_HEADER_LEN; + chunk->capacity = capacity; + chunk->head = 0; + chunk->tail = 0; + return chunk; +} + +TH_LOCAL(size_t) +th_ring_chunk_len(const th_ring_chunk* chunk) +{ + return chunk->tail - chunk->head; } -/* End of src/th_date.c */ -/* Start of src/th_clock.c */ -#ifdef TH_CONFIG_OS_POSIX -#include -#elif defined(TH_CONFIG_OS_WIN) -#include -#endif +TH_LOCAL(size_t) +th_ring_chunk_free_space(const th_ring_chunk* chunk) +{ + return chunk->capacity - th_ring_chunk_len(chunk); +} -TH_LOCAL(th_err) -th_os_clock_monotonic_now(void* self, time_t* out) +TH_LOCAL(void) +th_ring_chunk_write(th_ring_chunk* chunk, const void* data, size_t len) { - (void)self; -#if defined(TH_CONFIG_OS_POSIX) - struct timespec ts = {0}; - if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { - return TH_ERR_SYSTEM(errno); - } - *out = ts.tv_sec; - return TH_ERR_OK; -#elif defined(TH_CONFIG_OS_WIN) - *out = (time_t)(GetTickCount64() / 1000); - return TH_ERR_OK; -#else - (void)out; - return TH_ERR_NOSUPPORT; -#endif + if (len == 0) + return; + size_t offset = chunk->tail % chunk->capacity; + size_t first = chunk->capacity - offset < len ? chunk->capacity - offset : len; + memcpy(chunk->data + offset, data, first); + memcpy(chunk->data, (const unsigned char*)data + first, len - first); + chunk->tail += len; } -TH_PRIVATE(th_clock*) -th_clock_os(void) +TH_PRIVATE(void) +th_ring_init(th_ring* rb, th_allocator* allocator, size_t initial_capacity, size_t max_len) { - static th_clock os_clock = { - .monotonic_now = th_os_clock_monotonic_now, - }; - return &os_clock; + rb->chunks = th_ring_chunk_queue_make(); + rb->len = 0; + rb->initial_capacity = initial_capacity; + rb->max_len = max_len; + rb->allocator = allocator ? allocator : th_default_allocator_get(); } -/* End of src/th_clock.c */ -/* Start of src/th_timer.c */ TH_PRIVATE(void) -th_timer_init(th_timer* timer, th_clock* clock) +th_ring_deinit(th_ring* rb) { - timer->clock = clock; - timer->expire = 0; + th_ring_chunk* chunk; + while ((chunk = th_ring_chunk_queue_pop(&rb->chunks)) != NULL) + th_allocator_free(rb->allocator, chunk); +} + +TH_LOCAL(size_t) +th_ring_parts_len(const th_iov* parts, size_t partcnt) +{ + size_t len = 0; + for (size_t i = 0; i < partcnt; ++i) + len += parts[i].len; + return len; } TH_PRIVATE(th_err) -th_timer_set(th_timer* timer, th_duration duration) +th_ring_write(th_ring* rb, const th_iov* parts, size_t partcnt) { - time_t now = 0; - th_err err = timer->clock->monotonic_now(timer->clock, &now); - TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); - if (err != TH_ERR_OK) - return err; - timer->expire = now + duration.seconds; + size_t len = th_ring_parts_len(parts, partcnt); + if (rb->len + len > rb->max_len) + return TH_ERR_INVALID_ARG; + + th_ring_chunk* tail_chunk = rb->chunks.tail; + if (!tail_chunk || th_ring_chunk_free_space(tail_chunk) < len) { + size_t capacity = tail_chunk ? tail_chunk->capacity * 2 : rb->initial_capacity; + if (capacity < len) + capacity = len; + th_ring_chunk* chunk = th_ring_chunk_create(rb->allocator, capacity); + if (!chunk) + return TH_ERR_SYSTEM(TH_EAGAIN); + + // an empty tail chunk is unreachable once anything is queued + // behind it - peek/consume only ever advance from chunks.head + if (tail_chunk && th_ring_chunk_len(tail_chunk) == 0) { + th_ring_chunk_queue_pop(&rb->chunks); + th_allocator_free(rb->allocator, tail_chunk); + } + th_ring_chunk_queue_push(&rb->chunks, chunk); + tail_chunk = chunk; + } + + for (size_t i = 0; i < partcnt; ++i) + th_ring_chunk_write(tail_chunk, parts[i].base, parts[i].len); + rb->len += len; return TH_ERR_OK; } -TH_PRIVATE(bool) -th_timer_expired(th_timer* timer) +TH_PRIVATE(size_t) +th_ring_peek(th_ring* rb, th_iov iov[2]) { - time_t now = 0; - th_err err = timer->clock->monotonic_now(timer->clock, &now); - TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); - /* We don't return the error here, as it's already handled in th_timer_set - * and we can safely assume that the error won't happen here. */ - if (err != TH_ERR_OK) - return true; - return now >= timer->expire; + th_ring_chunk* chunk = rb->chunks.head; + size_t len = chunk ? th_ring_chunk_len(chunk) : 0; + if (len == 0) + return 0; + + size_t offset = chunk->head % chunk->capacity; + size_t first = chunk->capacity - offset < len ? chunk->capacity - offset : len; + iov[0].base = chunk->data + offset; + iov[0].len = first; + if (first == len) + return 1; + + iov[1].base = chunk->data; + iov[1].len = len - first; + return 2; } -TH_PRIVATE(th_timer) -th_timer_from_duration(th_clock* clock, th_duration duration) +TH_PRIVATE(void) +th_ring_consume(th_ring* rb, size_t len) { - th_timer timer; - th_timer_init(&timer, clock); - th_timer_set(&timer, duration); - return timer; + th_ring_chunk* chunk = rb->chunks.head; + chunk->head += len; + rb->len -= len; + + bool drained = th_ring_chunk_len(chunk) == 0; + bool sole_chunk = chunk == rb->chunks.tail; + if (!drained || (sole_chunk && chunk->capacity <= rb->initial_capacity)) + return; + + th_ring_chunk_queue_pop(&rb->chunks); + th_allocator_free(rb->allocator, chunk); } +/* End of src/th_ring.c */ +/* Start of src/th_ws.c */ -TH_PRIVATE(th_duration) -th_timer_remaining(const th_timer* timer) + +#undef TH_LOG_TAG +#define TH_LOG_TAG "ws" + +TH_PRIVATE(void) +th_ws_init(th_ws* ws, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator) { - time_t now = 0; - th_err err = timer->clock->monotonic_now(timer->clock, &now); - TH_ASSERT(err == TH_ERR_OK && "clock->monotonic_now failed"); - if (err != TH_ERR_OK) - return th_seconds(0); - return th_seconds(TH_MAX((int)(timer->expire - now), 0)); + ws->conn = conn; + ws->handler = handler; + ws->user_data = user_data; + ws->allocator = allocator ? allocator : th_default_allocator_get(); + ws->parser = (th_ws_frame_parser){0}; + th_buf_vec_init(&ws->payload, ws->allocator); + th_ring_init(&ws->send_ring, ws->allocator, TH_CONFIG_WS_SEND_RING_LEN, TH_CONFIG_WS_SEND_MAX_LEN); + ws->sending = false; + ws->closing = false; } -TH_PRIVATE(bool) -th_timer_less(const th_timer* a, const th_timer* b) +TH_PRIVATE(void) +th_ws_deinit(th_ws* ws) { - return a->expire < b->expire; + th_buf_vec_deinit(&ws->payload); + th_ring_deinit(&ws->send_ring); + th_conn_destroy(ws->conn); +} + +TH_PRIVATE(th_err) +th_ws_create(th_ws** out, th_conn* conn, th_ws_handler handler, void* user_data, th_allocator* allocator) +{ + allocator = allocator ? allocator : th_default_allocator_get(); + th_ws* ws = th_allocator_alloc(allocator, sizeof(th_ws)); + if (!ws) + return TH_ERR_BAD_ALLOC; + th_ws_init(ws, conn, handler, user_data, allocator); + *out = ws; + return TH_ERR_OK; } -/* End of src/th_timer.c */ -/* Start of src/th_conn_tracker.c */ TH_LOCAL(void) -th_conn_tracker_on_conn_init(th_conn_observer* observer, th_conn_observable* observable) +th_ws_destroy(th_ws* ws) { - th_conn_tracker* tracker = (th_conn_tracker*)observer; - th_conn_observable_list_push_back(&tracker->observables, observable); - ++tracker->count; + th_allocator* allocator = ws->allocator; + th_ws_deinit(ws); + th_allocator_free(allocator, ws); } TH_LOCAL(void) -th_conn_tracker_on_conn_deinit(th_conn_observer* observer, th_conn_observable* observable) +th_ws_close_and_destroy(th_ws* ws) { - th_conn_tracker* tracker = (th_conn_tracker*)observer; - th_conn_observable_list_erase(&tracker->observables, observable); - --tracker->count; - if (tracker->task) { - th_task* task = TH_MOVE_PTR(tracker->task); - th_task_complete(task); - } + (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}, TH_WS_TEXT); + th_ws_destroy(ws); } -TH_PRIVATE(void) -th_conn_tracker_init(th_conn_tracker* tracker) +TH_LOCAL(void) +th_ws_handle_recv(void* user_data, size_t len, th_err err); + +TH_LOCAL(th_err) +th_ws_queue_frame(th_ws* ws, th_ws_frame_type frame_type, th_buffer data); + +TH_LOCAL(bool) +th_ws_consume(th_ws* ws, char* data, size_t len) { - tracker->base.on_init = th_conn_tracker_on_conn_init; - tracker->base.on_deinit = th_conn_tracker_on_conn_deinit; - tracker->observables = (th_conn_observable_list){0}; - tracker->task = NULL; - tracker->count = 0; + while (len > 0) { + size_t parsed = 0; + th_ws_frame_type type; + th_err err = th_ws_frame_parser_parse(&ws->parser, data, len, &ws->payload, &parsed, &type); + data += parsed; + len -= parsed; + if (err == TH_ERR_SYSTEM(TH_EAGAIN)) + return true; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Invalid frame: %s", (void*)ws, th_strerror(err)); + return false; + } + + if (type == TH_WS_FRAME_CLOSE) { + if (!ws->closing) { + ws->closing = true; + th_ws_queue_frame(ws, TH_WS_FRAME_CLOSE, (th_buffer){0}); + } + return false; + } + if (type == TH_WS_FRAME_PING || type == TH_WS_FRAME_PONG) + continue; + + th_buffer message = {th_buf_vec_begin(&ws->payload), th_buf_vec_size(&ws->payload)}; + th_ws_type msg_type = type == TH_WS_FRAME_TEXT ? TH_WS_TEXT : TH_WS_BINARY; + err = ws->handler(ws->user_data, ws, TH_WS_EVENT_DATA, message, msg_type); + th_buf_vec_clear(&ws->payload); + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: DATA handler returned %s, closing", (void*)ws, th_strerror(err)); + return false; + } + } + return true; } -TH_PRIVATE(void) -th_conn_tracker_cancel_all(th_conn_tracker* conn_tracker) +TH_LOCAL(void) +th_ws_handle_recv(void* user_data, size_t len, th_err err) { - th_conn_observable* observable = NULL; - for (observable = th_conn_observable_list_front(&conn_tracker->observables); - observable != NULL; - observable = th_conn_observable_list_next(observable)) { - th_conn* client = &observable->base; - th_conn_cancel(client); + th_ws* ws = user_data; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Connection closed: %s", (void*)ws, th_strerror(err)); + th_ws_close_and_destroy(ws); + return; } + if (!th_ws_consume(ws, ws->scratch, len)) { + // if closing, a CLOSE frame is now queued/in flight - the send + // path destroys once it drains, so as not to cut it off mid-send + if (!ws->closing) + th_ws_close_and_destroy(ws); + return; + } + th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); } TH_PRIVATE(void) -th_conn_tracker_async_wait(th_conn_tracker* conn_tracker, th_task* task) +th_ws_start(th_ws* ws) { - TH_ASSERT(conn_tracker->task == NULL && "Task already set"); - TH_ASSERT(th_conn_observable_list_front(&conn_tracker->observables) != NULL && "No clients to wait for"); - conn_tracker->task = task; + th_err err = ws->handler(ws->user_data, ws, TH_WS_EVENT_OPEN, (th_buffer){0}, TH_WS_TEXT); + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: OPEN handler returned %s, closing", (void*)ws, th_strerror(err)); + th_ws_destroy(ws); + return; + } + th_conn_recv(ws->conn, ws->scratch, sizeof(ws->scratch), false, th_ws_handle_recv, ws); } -TH_PRIVATE(size_t) -th_conn_tracker_count(const th_conn_tracker* conn_tracker) +TH_LOCAL(void) +th_ws_handle_send(void* user_data, size_t len, th_err err); + +TH_LOCAL(void) +th_ws_send_drain(th_ws* ws) { - return conn_tracker->count; + size_t iovcnt = th_ring_peek(&ws->send_ring, ws->send_iov); + if (iovcnt == 0) { + ws->sending = false; + if (ws->closing) + th_ws_close_and_destroy(ws); + return; + } + ws->sending = true; + th_conn_send(ws->conn, ws->send_iov, iovcnt, NULL, 0, 0, th_ws_handle_send, ws); } -TH_PRIVATE(void) -th_conn_tracker_deinit(th_conn_tracker* tracker) +TH_LOCAL(void) +th_ws_handle_send(void* user_data, size_t len, th_err err) { - (void)tracker; - TH_ASSERT(th_conn_observable_list_front(&tracker->observables) == NULL && "All clients must be destroyed before deinit"); + th_ws* ws = user_data; + if (err != TH_ERR_OK) { + TH_LOG_DEBUG("%p: Send error: %s, closing", (void*)ws, th_strerror(err)); + th_ws_close_and_destroy(ws); + return; + } + th_ring_consume(&ws->send_ring, len); + th_ws_send_drain(ws); } -/* End of src/th_conn_tracker.c */ -/* Start of src/th_url_decode.c */ TH_LOCAL(th_err) -th_url_decode_next(th_str str, size_t* pos, char* out, th_url_decode_type type) +th_ws_queue_frame(th_ws* ws, th_ws_frame_type frame_type, th_buffer data) { - size_t i = *pos; - if (str.ptr[i] == '%') { - int c = 0; - for (size_t k = 0; k < 2; k++) { - if (i + 1 + k >= str.len) - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - c <<= 4; - if (str.ptr[i + 1 + k] >= '0' && str.ptr[i + 1 + k] <= '9') { - c |= str.ptr[i + 1 + k] - '0'; - } else if (str.ptr[i + 1 + k] >= 'a' && str.ptr[i + 1 + k] <= 'f') { - c |= str.ptr[i + 1 + k] - 'a' + 10; - } else if (str.ptr[i + 1 + k] >= 'A' && str.ptr[i + 1 + k] <= 'F') { - c |= str.ptr[i + 1 + k] - 'A' + 10; - } else { - return TH_ERR_HTTP(TH_CODE_BAD_REQUEST); - } - } - *out = (char)c; - i += 3; - } else if (type == TH_URL_DECODE_TYPE_QUERY && str.ptr[i] == '+') { - *out = ' '; - i++; - } else { - *out = str.ptr[i++]; - } - *pos = i; + unsigned char header[TH_WS_FRAME_HEADER_MAX_LEN]; + size_t header_len = th_ws_frame_header_write(header, frame_type, data.len); + + th_iov parts[2] = { + {header, header_len}, + {(void*)data.ptr, data.len}, + }; + th_err err = th_ring_write(&ws->send_ring, parts, 2); + if (err != TH_ERR_OK) + return err; + + if (!ws->sending) + th_ws_send_drain(ws); return TH_ERR_OK; } -TH_LOCAL(size_t) -th_url_decode_literal_run(th_str input, size_t pos, th_url_decode_type type) +TH_PUBLIC(th_err) +th_ws_send(th_ws* ws, th_buffer data, th_ws_type type) { - size_t start = pos; - while (pos < input.len && input.ptr[pos] != '%' - && !(type == TH_URL_DECODE_TYPE_QUERY && input.ptr[pos] == '+')) - pos++; - return pos - start; + if (ws->closing) + return TH_ERR_INVALID_ARG; + th_ws_frame_type frame_type = type == TH_WS_TEXT ? TH_WS_FRAME_TEXT : TH_WS_FRAME_BINARY; + return th_ws_queue_frame(ws, frame_type, data); } -TH_PRIVATE(th_err) -th_url_decode_string(th_str input, th_string* output, th_url_decode_type type) +TH_PUBLIC(th_err) +th_ws_close(th_ws* ws) { - th_string_clear(output); - - th_err err = TH_ERR_OK; - if (input.len == 0) - return TH_ERR_OK; - size_t i = 0; - while (i < input.len) { - size_t run = th_url_decode_literal_run(input, i, type); - if (run > 0) { - if ((err = th_string_append(output, th_str_substr(input, i, run))) != TH_ERR_OK) - return err; - i += run; - continue; - } - char c; - if ((err = th_url_decode_next(input, &i, &c, type)) != TH_ERR_OK) { - return err; - } - if ((err = th_string_push_back(output, c)) != TH_ERR_OK) { - return err; - } - } - return TH_ERR_OK; + if (ws->closing) + return TH_ERR_INVALID_ARG; + ws->closing = true; + return th_ws_queue_frame(ws, TH_WS_FRAME_CLOSE, (th_buffer){0}); } -/* End of src/th_url_decode.c */ +/* End of src/th_ws.c */ /* Start of src/th_ssl_smem_bio.c */ #if TH_WITH_SSL diff --git a/th.h b/th.h index 83cdc93..e95f4cd 100644 --- a/th.h +++ b/th.h @@ -53,6 +53,7 @@ typedef struct th_buffer { #define TH_ERRC_EOF 3 #define TH_ERRC_NOSUPPORT 4 #define TH_ERRC_UNKNOWN 5 +#define TH_ERRC_BUSY 6 /* error pack/unpack macros */ #define TH_ERR_CODE_BITS (sizeof(unsigned) * 8 - 4) @@ -70,6 +71,7 @@ typedef enum th_err { TH_ERR_EOF = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_EOF), TH_ERR_NOSUPPORT = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_NOSUPPORT), TH_ERR_UNKNOWN = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_UNKNOWN), + TH_ERR_BUSY = TH_ERR_PACK(TH_ERR_CATEGORY_OTHER, TH_ERRC_BUSY), } th_err; #define TH_ERR(category, code) (th_err)(TH_ERR_PACK(category, code)) @@ -131,6 +133,7 @@ typedef enum th_method { } th_method; typedef enum th_code { + TH_CODE_SWITCHING_PROTOCOLS = 101, TH_CODE_OK = 200, TH_CODE_MOVED_PERMANENTLY = 301, TH_CODE_BAD_REQUEST = 400, @@ -143,6 +146,7 @@ typedef enum th_code { TH_CODE_URI_TOO_LONG = 414, TH_CODE_UNSUPPORTED_MEDIA_TYPE = 415, TH_CODE_RANGE_NOT_SATISFIABLE = 416, + TH_CODE_UPGRADE_REQUIRED = 426, TH_CODE_TOO_MANY_REQUESTS = 429, TH_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431, TH_CODE_INTERNAL_SERVER_ERROR = 500, @@ -322,6 +326,46 @@ th_err th_add_cookie(th_response* resp, const char* key, const char* value, th_c */ typedef th_err (*th_handler)(void* userp, const th_request* req, th_response* resp); +/** th_ws_event + * @brief The kind of event delivered to a th_ws_handler. + */ +typedef enum th_ws_event { + TH_WS_EVENT_OPEN, // connection upgraded, ready to send/receive + TH_WS_EVENT_DATA, // one complete message received (data holds its payload) + TH_WS_EVENT_CLOSE, // connection closed, ws is no longer valid after this call returns +} th_ws_event; + +typedef struct th_ws th_ws; + +/** th_ws_type + * @brief Text vs binary, for both received messages and th_ws_send. + */ +typedef enum th_ws_type { + TH_WS_TEXT, + TH_WS_BINARY, +} th_ws_type; + +/** th_ws_handler + * @brief WebSocket event callback. data is empty for TH_WS_EVENT_OPEN/CLOSE, + * data and type is only useful for TH_WS_EVENT_DATA + * ws must not be used after TH_WS_EVENT_CLOSE has been delivered. + */ +typedef th_err (*th_ws_handler)(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type); + +/** th_ws_send + * @brief Queues one WebSocket message for sending. + * @return TH_ERR_SYSTEM(TH_EAGAIN) if the send queue is full. + * @return TH_ERR_INVALID_ARG if the connection is closing/closed. + */ +th_err th_ws_send(th_ws* ws, th_buffer data, th_ws_type type); + +/** th_ws_close + * @brief Starts closing the connection. TH_WS_EVENT_CLOSE will be + * delivered once the close handshake completes. + * @return TH_ERR_INVALID_ARG if the connection is already closing/closed. + */ +th_err th_ws_close(th_ws* ws); + typedef struct th_server th_server; /** th_server_create @@ -345,7 +389,17 @@ th_err th_bind(th_server* server, const char* addr, const char* port, th_bind_op th_err th_route(th_server* server, th_method method, const char* route, th_handler handler, void* userp); -/** th_err +/** th_route_ws + * @brief Registers a WebSocket endpoint at path. Any request to path with + * a valid WebSocket handshake is upgraded, after which handler(userp, ...) + * receives its events. To gate the upgrade (e.g. auth), also register a + * plain th_route on the same path with TH_METHOD_GET: if it returns an + * error, that error is sent as a normal HTTP response and no upgrade + * happens; if it returns TH_ERR_OK (or is absent), the upgrade proceeds. + */ +th_err th_route_ws(th_server* server, const char* path, th_ws_handler handler, void* userp); + +/** th_add_dir * @brief Add a directory to the server (for serving or storing files). * @param name Label for the directory. * @param path File system path. From af18ac297fc421ff4cf75554cfa28ce4b5490090 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:40:30 -0500 Subject: [PATCH 11/13] docs: update readme - fix typos - Remove remark that OpenSSL is slow, should be quite OK by now - Websocket support is now feature --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 557c56a..52b6d47 100644 --- a/README.md +++ b/README.md @@ -45,21 +45,18 @@ I wrote this library because I wanted a simple drop-in solution for the legacy C ## Features - Simple integration (just copy the `th.h` and `th.c` files to your project) -- HTTPS support (via OpenSSL) (Works, but still needs to be optimized as it's quite slow) +- HTTPS support (via OpenSSL) - Path capturing (e.g. `/user/{id}`) - Supports Linux and MacOS (Windows support is planned) - Fully customizable memory allocation and logging - File Uploads (Multipart form data) - -## Planned features - - Websockets ## Dependencies - The C standard library - OpenSSL (optional, for HTTPS support) -- gperf (optinal, for binary builds and amalgamation) +- gperf (optional, for binary builds and amalgamation) - python3 (optional, for running the amalgamation script) ## Enabling HTTPS From 492329ae71eaf4b33c0d4324d4457744f2b684f5 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:49:21 -0500 Subject: [PATCH 12/13] chore: add codecov.yml to ignore examples, tests, benchmarks Codecov applies its own coverage checks independent of the uploaded XML, so examples/tests/benchmarks need to be excluded there too. --- codecov.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..7459771 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +ignore: + - "examples" + - ".github" + - "src/*_test.c" + - "src/*_test.h" + - "src/*_bench.c" + - "src/th_bench.h" From 3db300b87e57e12261cf84804f86958787599882 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Tue, 11 Aug 2026 16:52:47 -0500 Subject: [PATCH 13/13] docs: add missing websocket example --- examples/websocket.c | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/websocket.c diff --git a/examples/websocket.c b/examples/websocket.c new file mode 100644 index 0000000..93abeba --- /dev/null +++ b/examples/websocket.c @@ -0,0 +1,58 @@ +#include + +#include +#include +#include + +static sig_atomic_t stop = 0; + +static void +sigint_handler(int signum) +{ + stop = signum; +} + +static th_err +ws_handler(void* userp, th_ws* ws, th_ws_event ev, th_buffer data, th_ws_type type) +{ + (void)userp; + switch (ev) { + case TH_WS_EVENT_OPEN: + fprintf(stderr, "ws: open\n"); + break; + case TH_WS_EVENT_DATA: + fprintf(stderr, "ws: received %zu bytes: %.*s\n", data.len, (int)data.len, (const char*)data.ptr); + th_ws_send(ws, data, type); + break; + case TH_WS_EVENT_CLOSE: + fprintf(stderr, "ws: close\n"); + break; + } + return TH_ERR_OK; +} + +int main(void) +{ + signal(SIGINT, sigint_handler); + th_server* server = NULL; + th_err err = TH_ERR_OK; + if ((err = th_server_create(&server, NULL)) != TH_ERR_OK) { + fprintf(stderr, "Failed to create server: %s\n", th_strerror(err)); + return EXIT_FAILURE; + } + if ((err = th_bind(server, "0.0.0.0", "8080", NULL)) != TH_ERR_OK) + goto cleanup; + if ((err = th_route_ws(server, "/ws", ws_handler, NULL)) != TH_ERR_OK) + goto cleanup; + while (!stop) { + th_poll(server, 1000); + } + fprintf(stderr, "Shutting down...\n"); +cleanup: + th_server_destroy(server); + if (err != TH_ERR_OK) { + fprintf(stderr, "Error: %s\n", th_strerror(err)); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +}