Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ignore:
- "examples"
- ".github"
- "src/*_test.c"
- "src/*_test.h"
- "src/*_bench.c"
- "src/th_bench.h"
58 changes: 58 additions & 0 deletions examples/websocket.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <th.h>

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

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;
}
56 changes: 55 additions & 1 deletion include/th.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
48 changes: 48 additions & 0 deletions src/th_base64.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "th_base64.h"

#include <stdint.h>

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;
}
14 changes: 14 additions & 0 deletions src/th_base64.h
Original file line number Diff line number Diff line change
@@ -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
68 changes: 68 additions & 0 deletions src/th_base64_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include "th_base64.h"
#include "th_test.h"

#include <string.h>

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
12 changes: 12 additions & 0 deletions src/th_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
3 changes: 2 additions & 1 deletion src/th_conn.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Loading
Loading