diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4011919..cb221de 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -71,6 +71,13 @@ 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_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
src/th_ssl_context.c
@@ -197,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
@@ -216,6 +224,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
@@ -236,8 +245,15 @@ 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_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
src/th_fcache_test.c
src/th_filepath_test.c
src/th_file_test.c
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
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"
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;
+}
diff --git a/include/th.h b/include/th.h
index 83cdc93..e95f4cd 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,
@@ -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.
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_config.h b/src/th_config.h
index 146e03c..b7cd2f3 100644
--- a/src/th_config.h
+++ b/src/th_config.h
@@ -30,6 +30,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__)
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_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.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 d236b78..433a778 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;
@@ -85,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..90aedc4 100644
--- a/src/th_http_test.c
+++ b/src/th_http_test.c
@@ -194,6 +194,17 @@ 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, th_ws_type type)
+{
+ (void)userp;
+ (void)ws;
+ (void)ev;
+ (void)data;
+ (void)type;
+ return TH_ERR_OK;
+}
+
TH_TEST_BEGIN(http)
{
th_conn_tracker tracker;
@@ -204,6 +215,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 +245,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 +482,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_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_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_ring.c b/src/th_ring.c
new file mode 100644
index 0000000..1521ab2
--- /dev/null
+++ b/src/th_ring.c
@@ -0,0 +1,142 @@
+#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)
+{
+ 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(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_router.c b/src/th_router.c
index 1fb5cad..9bc8064 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)
{
@@ -27,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;
}
@@ -86,7 +83,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 +108,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 +136,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 +161,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 +201,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)
@@ -238,8 +266,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,11 +306,51 @@ th_router_add_route(th_router* router, th_method method, th_str path, th_handler
}
}
}
+ *out = route;
+ 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)
+{
+ 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;
+ 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;
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;
+ // 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 74dcbb9..a6600cd 100644
--- a/src/th_router.h
+++ b/src/th_router.h
@@ -19,6 +19,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,
@@ -26,11 +31,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 +71,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..101026f 100644
--- a/src/th_router_test.c
+++ b/src/th_router_test.c
@@ -32,6 +32,17 @@ 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, th_ws_type type)
+{
+ (void)userp;
+ (void)ws;
+ (void)ev;
+ (void)data;
+ (void)type;
+ return TH_ERR_OK;
+}
+
TH_TEST_BEGIN(router)
{
TH_TEST_CASE_BEGIN(router_init)
@@ -359,5 +370,90 @@ 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_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_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
new file mode 100644
index 0000000..7797f33
--- /dev/null
+++ b/src/th_ws.c
@@ -0,0 +1,198 @@
+#include "th_ws.h"
+
+#include "th_log.h"
+#include "th_system_error.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();
+ 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(void)
+th_ws_deinit(th_ws* ws)
+{
+ 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;
+}
+
+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_close_and_destroy(th_ws* ws)
+{
+ (void)ws->handler(ws->user_data, ws, TH_WS_EVENT_CLOSE, (th_buffer){0}, TH_WS_TEXT);
+ th_ws_destroy(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)
+{
+ 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_LOCAL(void)
+th_ws_handle_recv(void* user_data, size_t len, th_err err)
+{
+ 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_ws_start(th_ws* ws)
+{
+ 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_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;
+ 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_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_LOCAL(th_err)
+th_ws_queue_frame(th_ws* ws, th_ws_frame_type frame_type, th_buffer data)
+{
+ 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)
+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)
+{
+ 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
new file mode 100644
index 0000000..5a3786d
--- /dev/null
+++ b/src/th_ws.h
@@ -0,0 +1,45 @@
+#ifndef TH_WS_H
+#define TH_WS_H
+
+#include
+
+#include "th_conn.h"
+#include "th_iov.h"
+#include "th_ring.h"
+#include "th_vec.h"
+#include "th_ws_frame_parser.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);
+
+#endif
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
new file mode 100644
index 0000000..d653914
--- /dev/null
+++ b/src/th_ws_frame.h
@@ -0,0 +1,28 @@
+#ifndef TH_WS_FRAME_H
+#define TH_WS_FRAME_H
+
+#include
+
+#include "th_config.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);
+
+#endif
diff --git a/src/th_ws_frame_parser.c b/src/th_ws_frame_parser.c
new file mode 100644
index 0000000..6817ea4
--- /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) {
+ *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;
+}
+
+// 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..c19d63c
--- /dev/null
+++ b/src/th_ws_frame_parser_test.c
@@ -0,0 +1,245 @@
+#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_TEXT);
+ 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_TEXT);
+ 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_TEXT);
+ 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(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);
+ }
+ 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_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_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..9acaefe
--- /dev/null
+++ b/src/th_ws_test.c
@@ -0,0 +1,517 @@
+#include "th_system_error.h"
+#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;
+
+ 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*
+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->recv_addr = addr;
+ 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)file;
+ (void)offset;
+ (void)len;
+ 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
+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->recv_addr = NULL;
+ 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
+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);
+}
+
+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);
+}
+
+// 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;
+ int data_count;
+ 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_ws_type type)
+{
+ (void)ws;
+ 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;
+ 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;
+ calls->data_type = type;
+ 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 = {.return_on_open = 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_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.data_type == TH_WS_TEXT);
+ 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_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_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};
+
+ 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 == 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);
+ }
+ 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_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);
+
+ 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_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_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
+}
+TH_TEST_END
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.