From 2fad7e469139fe6e82b791e23552b1617de20207 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Mon, 21 Sep 2026 17:50:09 +0500 Subject: [PATCH 01/48] Merge pull request #53 from netcorelink/feat/fixed-worker-pool feat: add non-blocking event-driven server runtime --- .github/workflows/ci.yml | 2 +- ChangeLog | 2 + docs/server/README.md | 8 + docs/server/README_RU.md | 8 + include/libchttpx.h | 1 + src/cHTTPX_event.c | 637 +++++++++++++++++++ src/cHTTPX_event.h | 32 + src/cHTTPX_response.c | 278 +++++++-- src/cHTTPX_runtime.c | 1269 ++++++++++++++++++++++++++++++++++++++ src/cHTTPX_runtime.h | 11 + src/cHTTPX_serv.c | 113 +--- src/cHTTPX_serv.h | 1 + src/cHTTPX_tls.c | 131 ++++ src/cHTTPX_tls.h | 6 + tests/test_server.c | 33 + 15 files changed, 2385 insertions(+), 147 deletions(-) create mode 100644 src/cHTTPX_event.c create mode 100644 src/cHTTPX_event.h create mode 100644 src/cHTTPX_runtime.c create mode 100644 src/cHTTPX_runtime.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d519a8e..0c50387 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [main, master] pull_request: - branches: [main, master, audit/core-safety-hardening] + branches: [main, master, develop, audit/core-safety-hardening] permissions: contents: read diff --git a/ChangeLog b/ChangeLog index bda2c4e..acb88b0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,7 @@ - Unreleased +Replaced per-connection blocking socket handling with a non-blocking event-driven runtime. Linux uses epoll, macOS/BSD uses kqueue and Windows uses WSAPoll; complete requests are dispatched to a fixed internal 32-thread worker pool and responses return to the event loop for non-blocking writes. Worker count is intentionally not configurable through the public server config. + Added request-scoped memory and contexts, response ownership, config-based initialization, typed values, JSON binding and builder APIs, route groups and scoped middleware, multipart/form parsing, upload policies, request IDs, Accept-Language selection, callback logging, chunked request decoding, body limits, graceful shutdown, and expanded tests/documentation. Fixed multipart uploads buffering the complete request in RAM, truncated fixed-length body acceptance, partial response sends, incorrect status reason phrases, OPTIONS fallthrough, temporary upload cleanup, unsafe query/cookie tokenization, current-client races and busy looping, router prefix leaks, fatal library exit calls, and unsafe signal/longjmp recovery. diff --git a/docs/server/README.md b/docs/server/README.md index 9514311..60b3d5c 100644 --- a/docs/server/README.md +++ b/docs/server/README.md @@ -134,6 +134,14 @@ cHTTPX_AppShutdown(&app); Trigger shutdown from an appropriate control thread for SIGINT/SIGTERM. Avoid complex work directly in an async-signal handler. +## Event-driven runtime and worker pool + +Sockets are non-blocking and stay on the server event loop instead of occupying worker threads while waiting for network I/O. Linux uses `epoll`, macOS/BSD uses `kqueue`, and Windows uses the non-blocking `WSAPoll` backend. + +A connection is submitted to the internal fixed pool of **32 worker threads** only after the complete HTTP request has been received. Workers execute middleware, routing and the application handler; they do not call `recv()` or `send()`. The serialized response is returned to the event loop and written when the socket is ready. + +The worker count is intentionally not part of `chttpx_config_t` and cannot be changed by application code. `max_clients` limits accepted/in-flight connections, not the number of OS threads. Large multipart/chunked upload bodies remain disk-backed while they are received. + ## Thread-safety model - client count is updated atomically; diff --git a/docs/server/README_RU.md b/docs/server/README_RU.md index ce6c7ac..f254846 100644 --- a/docs/server/README_RU.md +++ b/docs/server/README_RU.md @@ -123,6 +123,14 @@ cHTTPX_AppShutdown(&app); `cHTTPX_AppShutdown()` прекращает accept, закрывает listeners, ждёт активные requests, освобождает routes, CORS, middleware state, server objects и remote registrations. +## Event-driven runtime и worker pool + +Sockets работают в non-blocking режиме и остаются на server event loop, поэтому worker thread больше не занят ожиданием сетевого I/O. На Linux используется `epoll`, на macOS/BSD — `kqueue`, на Windows — non-blocking backend на `WSAPoll`. + +Connection попадает во внутренний фиксированный пул из **32 worker threads** только после того, как HTTP request полностью принят. Worker выполняет middleware, routing и application handler и не вызывает `recv()` или `send()`. Готовый сериализованный response возвращается в event loop и отправляется только когда socket готов к записи. + +Количество workers намеренно не добавлено в `chttpx_config_t` и не может изменяться кодом приложения. `max_clients` ограничивает число accepted/in-flight connections, а не количество OS threads. Большие multipart/chunked uploads во время приёма остаются disk-backed. + ## Thread safety Routes/middleware настраивайте до `AppStart/AppRun`. Во время работы они должны рассматриваться как read-only. diff --git a/include/libchttpx.h b/include/libchttpx.h index fd239c7..7586102 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -1313,6 +1313,7 @@ extern "C" void* rate_limiter_state; void* compression_state; void* metrics_state; + void* runtime_state; chttpx_cors_t cors; } chttpx_serv_t; diff --git a/src/cHTTPX_event.c b/src/cHTTPX_event.c new file mode 100644 index 0000000..940eeb5 --- /dev/null +++ b/src/cHTTPX_event.c @@ -0,0 +1,637 @@ +#include "cHTTPX_event.h" + +#include +#include +#include + +#ifdef CHTTPX_PLATFORM_WINDOWS + +struct chttpx_event_loop +{ + WSAPOLLFD* pollfds; + void** data; + size_t count; + size_t capacity; + volatile LONG wake_pending; +}; + +static short native_events(uint32_t events) +{ + short value = 0; + if (events & CHTTPX_EVENT_READ) + value |= POLLRDNORM; + if (events & CHTTPX_EVENT_WRITE) + value |= POLLWRNORM; + return value; +} + +static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) +{ + if (capacity <= loop->capacity) + return 1; + size_t next = loop->capacity ? loop->capacity * 2 : 64; + while (next < capacity) + next *= 2; + WSAPOLLFD* pollfds = realloc(loop->pollfds, next * sizeof(*pollfds)); + if (!pollfds) + return 0; + void** data = realloc(loop->data, next * sizeof(*data)); + if (!data) + { + loop->pollfds = pollfds; + return 0; + } + loop->pollfds = pollfds; + loop->data = data; + loop->capacity = next; + return 1; +} + +static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + for (size_t i = 0; i < loop->count; i++) + if (loop->pollfds[i].fd == fd) + return (ptrdiff_t)i; + return -1; +} + +chttpx_event_loop_t* _chttpx_event_create(void) +{ + return calloc(1, sizeof(chttpx_event_loop_t)); +} + +void _chttpx_event_destroy(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + free(loop->pollfds); + free(loop->data); + free(loop); +} + +int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop || find_fd(loop, fd) >= 0 || !ensure_capacity(loop, loop->count + 1)) + return -1; + loop->pollfds[loop->count].fd = fd; + loop->pollfds[loop->count].events = native_events(events); + loop->pollfds[loop->count].revents = 0; + loop->data[loop->count] = data; + loop->count++; + return 0; +} + +int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + ptrdiff_t index = find_fd(loop, fd); + if (index < 0) + return -1; + loop->pollfds[index].events = native_events(events); + loop->data[index] = data; + return 0; +} + +void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + if (!loop) + return; + ptrdiff_t index = find_fd(loop, fd); + if (index < 0) + return; + size_t i = (size_t)index; + size_t last = loop->count - 1; + if (i != last) + { + loop->pollfds[i] = loop->pollfds[last]; + loop->data[i] = loop->data[last]; + } + loop->count--; +} + +int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) +{ + if (!loop || !events || max_events == 0) + return -1; + + size_t output = 0; + if (InterlockedExchange(&loop->wake_pending, 0) != 0) + events[output++] = (chttpx_event_t){.fd = INVALID_SOCKET, .events = CHTTPX_EVENT_WAKE, .data = NULL}; + + if (output == max_events) + return (int)output; + + int wait_ms = timeout_ms; + if (wait_ms < 0 || wait_ms > 10) + wait_ms = 10; + + int ready = WSAPoll(loop->pollfds, (ULONG)loop->count, wait_ms); + if (ready == SOCKET_ERROR) + return -1; + + for (size_t i = 0; i < loop->count && output < max_events; i++) + { + short revents = loop->pollfds[i].revents; + if (!revents) + continue; + uint32_t value = 0; + if (revents & (POLLRDNORM | POLLRDBAND | POLLIN)) + value |= CHTTPX_EVENT_READ; + if (revents & (POLLWRNORM | POLLOUT)) + value |= CHTTPX_EVENT_WRITE; + if (revents & (POLLERR | POLLHUP | POLLNVAL)) + value |= CHTTPX_EVENT_ERROR; + events[output++] = (chttpx_event_t){.fd = loop->pollfds[i].fd, .events = value, .data = loop->data[i]}; + } + + if (output < max_events && InterlockedExchange(&loop->wake_pending, 0) != 0) + events[output++] = (chttpx_event_t){.fd = INVALID_SOCKET, .events = CHTTPX_EVENT_WAKE, .data = NULL}; + + return (int)output; +} + +void _chttpx_event_wake(chttpx_event_loop_t* loop) +{ + if (loop) + InterlockedExchange(&loop->wake_pending, 1); +} + +int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) +{ + u_long mode = 1; + return ioctlsocket(fd, FIONBIO, &mode) == 0 ? 0 : -1; +} + +#elif defined(__linux__) + +#include +#include +#include +#include +#include + +struct chttpx_event_loop +{ + int fd; + int wake_fd; +}; + +static uint32_t native_events(uint32_t events) +{ + uint32_t value = EPOLLRDHUP; + if (events & CHTTPX_EVENT_READ) + value |= EPOLLIN; + if (events & CHTTPX_EVENT_WRITE) + value |= EPOLLOUT; + return value; +} + +chttpx_event_loop_t* _chttpx_event_create(void) +{ + chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); + if (!loop) + return NULL; + loop->fd = epoll_create1(EPOLL_CLOEXEC); + loop->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (loop->fd < 0 || loop->wake_fd < 0) + goto error; + struct epoll_event event = {.events = EPOLLIN, .data.ptr = NULL}; + if (epoll_ctl(loop->fd, EPOLL_CTL_ADD, loop->wake_fd, &event) != 0) + goto error; + return loop; +error: + if (loop->wake_fd >= 0) + close(loop->wake_fd); + if (loop->fd >= 0) + close(loop->fd); + free(loop); + return NULL; +} + +void _chttpx_event_destroy(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + close(loop->wake_fd); + close(loop->fd); + free(loop); +} + +int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + struct epoll_event event = {.events = native_events(events), .data.ptr = data}; + return epoll_ctl(loop->fd, EPOLL_CTL_ADD, fd, &event); +} + +int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + struct epoll_event event = {.events = native_events(events), .data.ptr = data}; + return epoll_ctl(loop->fd, EPOLL_CTL_MOD, fd, &event); +} + +void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + if (loop) + epoll_ctl(loop->fd, EPOLL_CTL_DEL, fd, NULL); +} + +int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) +{ + if (!loop || !events || max_events == 0) + return -1; + if (max_events > 256) + max_events = 256; + struct epoll_event native[256]; + int count; + do + { + count = epoll_wait(loop->fd, native, (int)max_events, timeout_ms); + } while (count < 0 && errno == EINTR); + if (count < 0) + return -1; + + int output = 0; + for (int i = 0; i < count; i++) + { + if (!native[i].data.ptr) + { + uint64_t value; + while (read(loop->wake_fd, &value, sizeof(value)) > 0) + { + } + events[output++] = (chttpx_event_t){.fd = loop->wake_fd, .events = CHTTPX_EVENT_WAKE, .data = NULL}; + continue; + } + uint32_t value = 0; + if (native[i].events & EPOLLIN) + value |= CHTTPX_EVENT_READ; + if (native[i].events & EPOLLOUT) + value |= CHTTPX_EVENT_WRITE; + if (native[i].events & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) + value |= CHTTPX_EVENT_ERROR; + events[output++] = (chttpx_event_t){.fd = -1, .events = value, .data = native[i].data.ptr}; + } + return output; +} + +void _chttpx_event_wake(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + uint64_t value = 1; + ssize_t ignored = write(loop->wake_fd, &value, sizeof(value)); + (void)ignored; +} + +int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + +#include +#include +#include +#include +#include + +struct chttpx_event_loop +{ + int fd; +}; + +static int change_filter(chttpx_event_loop_t* loop, chttpx_socket_t fd, int16_t filter, bool enabled, void* data) +{ + struct kevent change; + EV_SET(&change, (uintptr_t)fd, filter, EV_ADD | (enabled ? EV_ENABLE : EV_DISABLE), 0, 0, data); + return kevent(loop->fd, &change, 1, NULL, 0, NULL); +} + +chttpx_event_loop_t* _chttpx_event_create(void) +{ + chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); + if (!loop) + return NULL; + loop->fd = kqueue(); + if (loop->fd < 0) + { + free(loop); + return NULL; + } + struct kevent wake; + EV_SET(&wake, 1, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, NULL); + if (kevent(loop->fd, &wake, 1, NULL, 0, NULL) != 0) + { + close(loop->fd); + free(loop); + return NULL; + } + return loop; +} + +void _chttpx_event_destroy(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + close(loop->fd); + free(loop); +} + +int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + if (change_filter(loop, fd, EVFILT_READ, (events & CHTTPX_EVENT_READ) != 0, data) != 0) + return -1; + if (change_filter(loop, fd, EVFILT_WRITE, (events & CHTTPX_EVENT_WRITE) != 0, data) != 0) + { + _chttpx_event_del(loop, fd); + return -1; + } + return 0; +} + +int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + if (change_filter(loop, fd, EVFILT_READ, (events & CHTTPX_EVENT_READ) != 0, data) != 0) + return -1; + return change_filter(loop, fd, EVFILT_WRITE, (events & CHTTPX_EVENT_WRITE) != 0, data); +} + +void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + if (!loop) + return; + struct kevent changes[2]; + EV_SET(&changes[0], (uintptr_t)fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + EV_SET(&changes[1], (uintptr_t)fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + kevent(loop->fd, changes, 2, NULL, 0, NULL); +} + +int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) +{ + if (!loop || !events || max_events == 0) + return -1; + if (max_events > 256) + max_events = 256; + struct kevent native[256]; + struct timespec timeout; + struct timespec* timeout_ptr = NULL; + if (timeout_ms >= 0) + { + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_nsec = (long)(timeout_ms % 1000) * 1000000L; + timeout_ptr = &timeout; + } + int count; + do + { + count = kevent(loop->fd, NULL, 0, native, (int)max_events, timeout_ptr); + } while (count < 0 && errno == EINTR); + if (count < 0) + return -1; + + size_t output = 0; + for (int i = 0; i < count && output < max_events; i++) + { + if (native[i].filter == EVFILT_USER) + { + events[output++] = (chttpx_event_t){.fd = -1, .events = CHTTPX_EVENT_WAKE, .data = NULL}; + continue; + } + void* data = native[i].udata; + uint32_t value = native[i].filter == EVFILT_READ ? CHTTPX_EVENT_READ : CHTTPX_EVENT_WRITE; + if (native[i].flags & (EV_ERROR | EV_EOF)) + value |= CHTTPX_EVENT_ERROR; + + size_t existing = output; + for (size_t j = 0; j < output; j++) + { + if (events[j].data == data) + { + existing = j; + break; + } + } + if (existing < output) + events[existing].events |= value; + else + events[output++] = (chttpx_event_t){.fd = (chttpx_socket_t)native[i].ident, .events = value, .data = data}; + } + return (int)output; +} + +void _chttpx_event_wake(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + struct kevent wake; + EV_SET(&wake, 1, EVFILT_USER, 0, NOTE_TRIGGER, 0, NULL); + kevent(loop->fd, &wake, 1, NULL, 0, NULL); +} + +int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +#else + +#include +#include +#include +#include + +struct chttpx_event_loop +{ + struct pollfd* pollfds; + void** data; + size_t count; + size_t capacity; + int wake_pipe[2]; +}; + +static short native_events(uint32_t events) +{ + short value = 0; + if (events & CHTTPX_EVENT_READ) + value |= POLLIN; + if (events & CHTTPX_EVENT_WRITE) + value |= POLLOUT; + return value; +} + +static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) +{ + if (capacity <= loop->capacity) + return 1; + size_t next = loop->capacity ? loop->capacity * 2 : 64; + while (next < capacity) + next *= 2; + struct pollfd* pollfds = realloc(loop->pollfds, next * sizeof(*pollfds)); + if (!pollfds) + return 0; + void** data = realloc(loop->data, next * sizeof(*data)); + if (!data) + { + loop->pollfds = pollfds; + return 0; + } + loop->pollfds = pollfds; + loop->data = data; + loop->capacity = next; + return 1; +} + +static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + for (size_t i = 0; i < loop->count; i++) + if (loop->pollfds[i].fd == fd) + return (ptrdiff_t)i; + return -1; +} + +chttpx_event_loop_t* _chttpx_event_create(void) +{ + chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); + if (!loop) + return NULL; + if (pipe(loop->wake_pipe) != 0 || _chttpx_socket_set_nonblocking(loop->wake_pipe[0]) != 0 || _chttpx_socket_set_nonblocking(loop->wake_pipe[1]) != 0) + { + free(loop); + return NULL; + } + if (!ensure_capacity(loop, 1)) + { + close(loop->wake_pipe[0]); + close(loop->wake_pipe[1]); + free(loop); + return NULL; + } + loop->pollfds[0] = (struct pollfd){.fd = loop->wake_pipe[0], .events = POLLIN}; + loop->data[0] = NULL; + loop->count = 1; + return loop; +} + +void _chttpx_event_destroy(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + close(loop->wake_pipe[0]); + close(loop->wake_pipe[1]); + free(loop->pollfds); + free(loop->data); + free(loop); +} + +int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop || find_fd(loop, fd) >= 0 || !ensure_capacity(loop, loop->count + 1)) + return -1; + loop->pollfds[loop->count] = (struct pollfd){.fd = fd, .events = native_events(events)}; + loop->data[loop->count] = data; + loop->count++; + return 0; +} + +int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) +{ + if (!loop) + return -1; + ptrdiff_t index = find_fd(loop, fd); + if (index < 0) + return -1; + loop->pollfds[index].events = native_events(events); + loop->data[index] = data; + return 0; +} + +void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) +{ + if (!loop) + return; + ptrdiff_t index = find_fd(loop, fd); + if (index <= 0) + return; + size_t i = (size_t)index; + size_t last = loop->count - 1; + if (i != last) + { + loop->pollfds[i] = loop->pollfds[last]; + loop->data[i] = loop->data[last]; + } + loop->count--; +} + +int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) +{ + if (!loop || !events || max_events == 0) + return -1; + int ready; + do + { + ready = poll(loop->pollfds, loop->count, timeout_ms); + } while (ready < 0 && errno == EINTR); + if (ready < 0) + return -1; + + size_t output = 0; + for (size_t i = 0; i < loop->count && output < max_events; i++) + { + short revents = loop->pollfds[i].revents; + if (!revents) + continue; + if (i == 0) + { + char drain[64]; + while (read(loop->wake_pipe[0], drain, sizeof(drain)) > 0) + { + } + events[output++] = (chttpx_event_t){.fd = loop->wake_pipe[0], .events = CHTTPX_EVENT_WAKE, .data = NULL}; + continue; + } + uint32_t value = 0; + if (revents & POLLIN) + value |= CHTTPX_EVENT_READ; + if (revents & POLLOUT) + value |= CHTTPX_EVENT_WRITE; + if (revents & (POLLERR | POLLHUP | POLLNVAL)) + value |= CHTTPX_EVENT_ERROR; + events[output++] = (chttpx_event_t){.fd = loop->pollfds[i].fd, .events = value, .data = loop->data[i]}; + } + return (int)output; +} + +void _chttpx_event_wake(chttpx_event_loop_t* loop) +{ + if (!loop) + return; + const char value = 1; + ssize_t ignored = write(loop->wake_pipe[1], &value, 1); + (void)ignored; +} + +int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +#endif diff --git a/src/cHTTPX_event.h b/src/cHTTPX_event.h new file mode 100644 index 0000000..2415c7f --- /dev/null +++ b/src/cHTTPX_event.h @@ -0,0 +1,32 @@ +#ifndef CHTTPX_EVENT_H +#define CHTTPX_EVENT_H + +#include "cHTTPX_crosspltm.h" + +#include +#include + +#define CHTTPX_EVENT_READ 0x01u +#define CHTTPX_EVENT_WRITE 0x02u +#define CHTTPX_EVENT_ERROR 0x04u +#define CHTTPX_EVENT_WAKE 0x08u + +typedef struct chttpx_event_loop chttpx_event_loop_t; + +typedef struct +{ + chttpx_socket_t fd; + uint32_t events; + void* data; +} chttpx_event_t; + +chttpx_event_loop_t* _chttpx_event_create(void); +void _chttpx_event_destroy(chttpx_event_loop_t* loop); +int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data); +int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data); +void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd); +int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms); +void _chttpx_event_wake(chttpx_event_loop_t* loop); +int _chttpx_socket_set_nonblocking(chttpx_socket_t fd); + +#endif diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index 6e299e8..e880bcf 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -402,95 +402,122 @@ static int append_response_header(char* buffer, size_t capacity, size_t* length, * * This function formats the HTTP response headers and body according to HTTP/1.1. */ -static void send_response(chttpx_request_t* req, chttpx_response_t res) +static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, char** output, size_t* output_size) { - chttpx_serv_t* server = req ? req->_server : NULL; + if (!req || !output || !output_size) + return CHTTPX_ERR_INVALID_ARGUMENT; + + *output = NULL; + *output_size = 0; + chttpx_serv_t* server = req->_server; size_t capacity = 1024; for (size_t i = 0; i < res.headers_count; i++) - capacity += strlen(res.headers[i].name) + strlen(res.headers[i].value) + 4; + { + size_t name_size = strlen(res.headers[i].name); + size_t value_size = strlen(res.headers[i].value); + if (capacity > SIZE_MAX - name_size - value_size - 4) + return CHTTPX_ERR_LIMIT; + capacity += name_size + value_size + 4; + } if (server && server->cors.enabled) - capacity += strlen(server->cors.methods) + strlen(server->cors.headers) + MAX_HEADER_VALUE + 512; - char* buffer = malloc(capacity); - if (!buffer) - return; - size_t length = 0; + { + size_t methods_size = server->cors.methods ? strlen(server->cors.methods) : 0; + size_t headers_size = server->cors.headers ? strlen(server->cors.headers) : 0; + if (capacity > SIZE_MAX - methods_size - headers_size - MAX_HEADER_VALUE - 512) + return CHTTPX_ERR_LIMIT; + capacity += methods_size + headers_size + MAX_HEADER_VALUE + 512; + } - /* Cors */ - const char* allowed_origin = req ? allowed_origin_cors(server, cHTTPX_HeaderGet(req, "Origin")) : NULL; + char* header = malloc(capacity); + if (!header) + return CHTTPX_ERR_MEMORY; + size_t length = 0; + const char* allowed_origin = server && server->cors.enabled ? allowed_origin_cors(server, cHTTPX_HeaderGet(req, "Origin")) : NULL; - if (!append_response_header(buffer, capacity, &length, - "HTTP/1.1 %d %s\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "Connection: close\r\n", - res.status, cHTTPX_StatusReason((uint16_t)res.status), res.content_type ? res.content_type : cHTTPX_CTYPE_OCTET, - res.body_size)) - goto done; + if (!append_response_header(header, capacity, &length, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\n", res.status, cHTTPX_StatusReason((uint16_t)res.status), res.content_type ? res.content_type : cHTTPX_CTYPE_OCTET, res.body_size)) + goto limit_error; - /* Etag */ const char* etag = generate_etag(res.body, res.body_size); if (etag) { - append_response_header(buffer, capacity, &length, "Etag: %s\r\n", etag); + if (!append_response_header(header, capacity, &length, "Etag: %s\r\n", etag)) + { + free((void*)etag); + goto limit_error; + } free((void*)etag); } if (allowed_origin) { - if (!append_response_header(buffer, capacity, &length, - "Access-Control-Allow-Origin: %s\r\n" - "Access-Control-Allow-Methods: %s\r\n" - "Access-Control-Allow-Headers: %s\r\n" - "Access-Control-Allow-Credentials: true\r\n", - allowed_origin, server->cors.methods, server->cors.headers)) - goto done; + if (!append_response_header(header, capacity, &length, "Access-Control-Allow-Origin: %s\r\nAccess-Control-Allow-Methods: %s\r\nAccess-Control-Allow-Headers: %s\r\nAccess-Control-Allow-Credentials: true\r\n", allowed_origin, server->cors.methods, server->cors.headers)) + goto limit_error; } - if (req && req->request_id[0]) - if (!append_response_header(buffer, capacity, &length, "X-Request-ID: %s\r\n", req->request_id)) - goto done; + if (req->request_id[0] && !append_response_header(header, capacity, &length, "X-Request-ID: %s\r\n", req->request_id)) + goto limit_error; - /* Add all request headers */ for (size_t i = 0; i < res.headers_count; i++) - { - if (!append_response_header(buffer, capacity, &length, "%s: %s\r\n", res.headers[i].name, res.headers[i].value)) - goto done; - } + if (!append_response_header(header, capacity, &length, "%s: %s\r\n", res.headers[i].name, res.headers[i].value)) + goto limit_error; - if (!append_response_header(buffer, capacity, &length, "\r\n")) - goto done; + if (!append_response_header(header, capacity, &length, "\r\n")) + goto limit_error; + if (res.body_size > SIZE_MAX - length) + goto limit_error; - int write_result = _chttpx_io_send_all(req->client_fd, req->_tls_session, buffer, length); - if (write_result != CHTTPX_OK) + size_t total = length + res.body_size; + char* response = malloc(total ? total : 1); + if (!response) { - if (write_result == CHTTPX_ERR_TLS) - _chttpx_tls_log_error(server, req->request_id, "TLS response-header write failed"); - goto done; + free(header); + return CHTTPX_ERR_MEMORY; } + memcpy(response, header, length); + if (res.body && res.body_size) + memcpy(response + length, res.body, res.body_size); + free(header); + *output = response; + *output_size = total; + return CHTTPX_OK; - if (res.body && res.body_size > 0) - { - write_result = _chttpx_io_send_all(req->client_fd, req->_tls_session, res.body, res.body_size); - if (write_result == CHTTPX_ERR_TLS) - _chttpx_tls_log_error(server, req->request_id, "TLS response-body write failed"); - } +limit_error: + free(header); + return CHTTPX_ERR_LIMIT; +} -done: - free(buffer); +static void send_response(chttpx_request_t* req, chttpx_response_t res) +{ + char* response = NULL; + size_t response_size = 0; + int build_result = build_response_buffer(req, res, &response, &response_size); + if (build_result != CHTTPX_OK) + return; + int write_result = _chttpx_io_send_all(req->client_fd, req->_tls_session, response, response_size); + if (write_result == CHTTPX_ERR_TLS) + _chttpx_tls_log_error(req->_server, req->request_id, "TLS response write failed"); + free(response); } /* Handle browser CORS preflight without hijacking ordinary OPTIONS routes. */ -static int is_cors_preflight(chttpx_request_t* req) +static int build_cors_preflight(chttpx_request_t* req, chttpx_response_t* res) { chttpx_serv_t* server = req ? req->_server : NULL; - if (!req || !server || !server->cors.enabled || strcasecmp(req->method, cHTTPX_MethodOptions) != 0) + if (!req || !res || !server || !server->cors.enabled || strcasecmp(req->method, cHTTPX_MethodOptions) != 0) return 0; - if (!cHTTPX_HeaderGet(req, "Origin") || !cHTTPX_HeaderGet(req, "Access-Control-Request-Method")) return 0; + *res = (chttpx_response_t){.status = cHTTPX_StatusNoContent, .content_type = cHTTPX_CTYPE_TEXT, .body = NULL, .body_size = 0}; + return 1; +} - chttpx_response_t res = {.status = cHTTPX_StatusNoContent, .content_type = cHTTPX_CTYPE_TEXT, .body = NULL, .body_size = 0}; +static int is_cors_preflight(chttpx_request_t* req) +{ + chttpx_response_t res = {0}; + if (!build_cors_preflight(req, &res)) + return 0; send_response(req, res); + cHTTPX_ResponseCleanup(&res); return 1; } @@ -677,6 +704,113 @@ static chttpx_request_t* parse_req_buffer(chttpx_serv_t* server, chttpx_socket_t return req; } +static void close_prefetched_stream(void* resource) +{ + if (resource) + fclose((FILE*)resource); +} + +static void free_request_object(chttpx_request_t* req) +{ + if (!req) + return; + cHTTPX_RequestCleanup(req); + chttpx_free_req_cookie(req); + free(req->method); + free(req->path); + free(req->body); + for (size_t i = 0; i < req->query_count; i++) + { + free(req->query[i].name); + free(req->query[i].value); + } + free(req->query); + free(req); +} + +static chttpx_request_t* parse_prefetched_request(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length) +{ + chttpx_request_t* req = calloc(1, sizeof(*req)); + if (!req) + { + free(body); + if (body_stream) + fclose(body_stream); + return NULL; + } + + req->body = body; + req->body_size = body_size; + req->content_length = content_length; + req->client_fd = client_fd; + req->_server = server; + req->_tls_session = tls_session; + + char method[16]; + char path[CHTTPX_MAX_PATH]; + char protocol[16]; + if (!headers || sscanf(headers, "%15s %4095s %15s", method, path, protocol) != 3) + { + req->_parse_status = cHTTPX_StatusBadRequest; + return req; + } + + req->method = strdup(method); + req->path = strdup(path); + if (!req->method || !req->path) + { + req->_parse_status = cHTTPX_StatusInternalServerError; + return req; + } + + const char* client_ip = cHTTPX_ClientInetIP(client_fd); + if (client_ip) + snprintf(req->client_ip, sizeof(req->client_ip), "%s", client_ip); + + _parse_req_headers(req, headers, header_size); + if (req->_parse_status) + return req; + + _parse_req_cookies(req); + if (req->_parse_status) + return req; + + const char* content_type = cHTTPX_HeaderGet(req, "Content-Type"); + if (content_type) + snprintf(req->content_type, sizeof(req->content_type), "%s", content_type); + + const char* user_agent = cHTTPX_HeaderGet(req, "User-Agent"); + if (user_agent) + snprintf(req->user_agent, sizeof(req->user_agent), "%s", user_agent); + + snprintf(req->protocol, sizeof(req->protocol), "%s", protocol); + set_request_id(req); + set_request_language(req); + + char* query = strchr(req->path, '?'); + if (query) + { + *query = '\0'; + _parse_req_query(req, query + 1); + if (req->_parse_status) + return req; + } + + if (body_stream) + { + if (cHTTPX_Defer(req, body_stream, close_prefetched_stream) != 0) + { + fclose(body_stream); + req->_parse_status = cHTTPX_StatusInternalServerError; + return req; + } + req->_multipart_stream = body_stream; + } + + _parse_media(req, headers, header_size); + return req; +} + int _chttpx_dispatch(chttpx_serv_t* server, chttpx_request_t* req, chttpx_response_t* res) { if (!server || !server->initialized || !req || !res || !req->method || !req->path) @@ -779,6 +913,42 @@ int _chttpx_dispatch(chttpx_serv_t* server, chttpx_request_t* req, chttpx_respon return CHTTPX_OK; } +int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size) +{ + if (!server || !headers || !output || !output_size) + { + free(body); + if (body_stream) + fclose(body_stream); + return CHTTPX_ERR_INVALID_ARGUMENT; + } + + *output = NULL; + *output_size = 0; + chttpx_request_t* req = parse_prefetched_request(server, client_fd, tls_session, headers, header_size, body, body_size, body_stream, content_length); + if (!req) + return CHTTPX_ERR_MEMORY; + + chttpx_response_t res = {0}; + if (req->_parse_status) + { + _chttpx_metrics_parser_failure(server); + const char* message = req->_parse_status == cHTTPX_StatusPayloadTooLarge ? "payload too large" : (req->_parse_status == cHTTPX_StatusInternalServerError ? "internal server error" : "invalid request"); + res = cHTTPX_ResError((uint16_t)req->_parse_status, message); + } + else if (!build_cors_preflight(req, &res)) + { + int dispatch_result = _chttpx_dispatch(server, req, &res); + if (dispatch_result != CHTTPX_OK) + res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "request dispatch failed"); + } + + int result = build_response_buffer(req, res, output, output_size); + cHTTPX_ResponseCleanup(&res); + free_request_object(req); + return result; +} + /** * Handle one accepted socket. The argument is a chttpx_client_ctx_t carrying * both the socket and the exact App-managed server that accepted it. diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c new file mode 100644 index 0000000..943b2dc --- /dev/null +++ b/src/cHTTPX_runtime.c @@ -0,0 +1,1269 @@ +#include "cHTTPX_runtime.h" + +#include "cHTTPX_event.h" +#include "cHTTPX_http.h" +#include "cHTTPX_metrics.h" +#include "cHTTPX_tls.h" +#include "cHTTPX_utils.h" + +#include +#include +#include +#include +#include +#include + +#define CHTTPX_WORKER_THREADS 32 +#define CHTTPX_RUNTIME_EVENTS 256 +#define CHTTPX_CHUNK_LINE_MAX 128 + +typedef enum +{ + CHTTPX_CONN_TLS, + CHTTPX_CONN_READING_HEADERS, + CHTTPX_CONN_READING_BODY, + CHTTPX_CONN_PROCESSING, + CHTTPX_CONN_WRITING +} chttpx_connection_state_t; + +typedef enum +{ + CHTTPX_CHUNK_SIZE, + CHTTPX_CHUNK_SIZE_LF, + CHTTPX_CHUNK_DATA, + CHTTPX_CHUNK_DATA_CR, + CHTTPX_CHUNK_DATA_LF, + CHTTPX_CHUNK_TRAILER, + CHTTPX_CHUNK_TRAILER_LF, + CHTTPX_CHUNK_DONE +} chttpx_chunk_state_t; + +struct chttpx_runtime; + +typedef struct chttpx_connection +{ + struct chttpx_runtime* runtime; + chttpx_serv_t* server; + chttpx_socket_t fd; + void* tls_session; + chttpx_connection_state_t state; + char* headers; + size_t header_size; + size_t header_capacity; + size_t header_end; + unsigned char* body; + size_t body_size; + size_t body_capacity; + FILE* body_stream; + size_t content_length; + size_t body_received; + size_t body_limit; + bool chunked; + bool memory_body; + bool multipart_body; + bool discard_body; + chttpx_chunk_state_t chunk_state; + char chunk_line[CHTTPX_CHUNK_LINE_MAX]; + size_t chunk_line_size; + size_t chunk_remaining; + size_t chunk_decoded; + char* write_buffer; + size_t write_size; + size_t write_offset; + int worker_result; + uint64_t last_activity_ms; + struct chttpx_connection* next; + struct chttpx_connection* completion_next; +} chttpx_connection_t; + +typedef struct chttpx_runtime +{ + chttpx_serv_t* server; + chttpx_event_loop_t* event_loop; + thread_t workers[CHTTPX_WORKER_THREADS]; + size_t workers_started; + chttpx_connection_t** jobs; + size_t job_capacity; + size_t job_head; + size_t job_tail; + size_t job_count; + bool workers_stopping; + bool stopping; + bool shutdown_started; + bool listener_registered; +#ifdef CHTTPX_PLATFORM_WINDOWS + CRITICAL_SECTION job_mutex; + CONDITION_VARIABLE job_ready; + CRITICAL_SECTION completion_mutex; +#else + pthread_mutex_t job_mutex; + pthread_cond_t job_ready; + pthread_mutex_t completion_mutex; +#endif + chttpx_connection_t* completions; + chttpx_connection_t* connections; +} chttpx_runtime_t; + +int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size); + +static bool runtime_is_stopping(chttpx_runtime_t* runtime) +{ + return __atomic_load_n(&runtime->stopping, __ATOMIC_ACQUIRE); +} + +static bool socket_valid(chttpx_socket_t fd) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + return fd != INVALID_SOCKET; +#else + return fd >= 0; +#endif +} + +static uint64_t monotonic_ms(void) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + return GetTickCount64(); +#else + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint64_t)now.tv_sec * 1000ULL + (uint64_t)now.tv_nsec / 1000000ULL; +#endif +} + +static void job_lock(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + EnterCriticalSection(&runtime->job_mutex); +#else + pthread_mutex_lock(&runtime->job_mutex); +#endif +} + +static void job_unlock(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + LeaveCriticalSection(&runtime->job_mutex); +#else + pthread_mutex_unlock(&runtime->job_mutex); +#endif +} + +static void job_wait(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + SleepConditionVariableCS(&runtime->job_ready, &runtime->job_mutex, INFINITE); +#else + pthread_cond_wait(&runtime->job_ready, &runtime->job_mutex); +#endif +} + +static void job_signal(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + WakeConditionVariable(&runtime->job_ready); +#else + pthread_cond_signal(&runtime->job_ready); +#endif +} + +static void job_broadcast(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + WakeAllConditionVariable(&runtime->job_ready); +#else + pthread_cond_broadcast(&runtime->job_ready); +#endif +} + +static void completion_lock(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + EnterCriticalSection(&runtime->completion_mutex); +#else + pthread_mutex_lock(&runtime->completion_mutex); +#endif +} + +static void completion_unlock(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + LeaveCriticalSection(&runtime->completion_mutex); +#else + pthread_mutex_unlock(&runtime->completion_mutex); +#endif +} + +static int sync_init(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + InitializeCriticalSection(&runtime->job_mutex); + InitializeConditionVariable(&runtime->job_ready); + InitializeCriticalSection(&runtime->completion_mutex); + return 0; +#else + if (pthread_mutex_init(&runtime->job_mutex, NULL) != 0) + return -1; + if (pthread_cond_init(&runtime->job_ready, NULL) != 0) + { + pthread_mutex_destroy(&runtime->job_mutex); + return -1; + } + if (pthread_mutex_init(&runtime->completion_mutex, NULL) != 0) + { + pthread_cond_destroy(&runtime->job_ready); + pthread_mutex_destroy(&runtime->job_mutex); + return -1; + } + return 0; +#endif +} + +static void sync_destroy(chttpx_runtime_t* runtime) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + DeleteCriticalSection(&runtime->completion_mutex); + DeleteCriticalSection(&runtime->job_mutex); +#else + pthread_mutex_destroy(&runtime->completion_mutex); + pthread_cond_destroy(&runtime->job_ready); + pthread_mutex_destroy(&runtime->job_mutex); +#endif +} + +static void completion_push(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + completion_lock(runtime); + connection->completion_next = runtime->completions; + runtime->completions = connection; + completion_unlock(runtime); + _chttpx_event_wake(runtime->event_loop); +} + +static chttpx_connection_t* completion_take_all(chttpx_runtime_t* runtime) +{ + completion_lock(runtime); + chttpx_connection_t* list = runtime->completions; + runtime->completions = NULL; + completion_unlock(runtime); + return list; +} + +static bool worker_submit(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + job_lock(runtime); + if (runtime->workers_stopping || runtime->job_count >= runtime->job_capacity) + { + job_unlock(runtime); + return false; + } + runtime->jobs[runtime->job_tail] = connection; + runtime->job_tail = (runtime->job_tail + 1) % runtime->job_capacity; + runtime->job_count++; + job_signal(runtime); + job_unlock(runtime); + return true; +} + +static chttpx_connection_t* worker_take(chttpx_runtime_t* runtime) +{ + job_lock(runtime); + while (runtime->job_count == 0 && !runtime->workers_stopping) + job_wait(runtime); + if (runtime->job_count == 0 && runtime->workers_stopping) + { + job_unlock(runtime); + return NULL; + } + chttpx_connection_t* connection = runtime->jobs[runtime->job_head]; + runtime->jobs[runtime->job_head] = NULL; + runtime->job_head = (runtime->job_head + 1) % runtime->job_capacity; + runtime->job_count--; + job_unlock(runtime); + return connection; +} + +static void* runtime_worker(void* argument) +{ + chttpx_runtime_t* runtime = argument; + for (;;) + { + chttpx_connection_t* connection = worker_take(runtime); + if (!connection) + break; + + unsigned char* body = connection->body; + FILE* stream = connection->body_stream; + connection->body = NULL; + connection->body_stream = NULL; + + if (runtime_is_stopping(runtime)) + { + free(body); + if (stream) + fclose(stream); + connection->worker_result = CHTTPX_ERR_STATE; + } + else + { + connection->worker_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session, connection->headers, connection->header_end, body, connection->body_size, stream, connection->content_length, &connection->write_buffer, &connection->write_size); + } + + free(connection->headers); + connection->headers = NULL; + connection->header_size = 0; + connection->header_capacity = 0; + completion_push(runtime, connection); + } + return NULL; +} + +static void connection_unlink(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + chttpx_connection_t** current = &runtime->connections; + while (*current) + { + if (*current == connection) + { + *current = connection->next; + return; + } + current = &(*current)->next; + } +} + +static void connection_close(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + if (!runtime || !connection) + return; + _chttpx_event_del(runtime->event_loop, connection->fd); + connection_unlink(runtime, connection); + _chttpx_tls_session_close(connection->tls_session); + connection->tls_session = NULL; + if (socket_valid(connection->fd)) + chttpx_close(connection->fd); + free(connection->headers); + free(connection->body); + if (connection->body_stream) + fclose(connection->body_stream); + free(connection->write_buffer); + _chttpx_metrics_connection_closed(connection->server); + __atomic_fetch_sub(&connection->server->current_clients, 1, __ATOMIC_SEQ_CST); + free(connection); +} + +static void connection_touch(chttpx_connection_t* connection) +{ + connection->last_activity_ms = monotonic_ms(); +} + +static bool span_equal_ci(const char* value, size_t value_size, const char* expected) +{ + size_t expected_size = strlen(expected); + return value_size == expected_size && strncasecmp(value, expected, value_size) == 0; +} + +static bool content_type_matches(const char* value, const char* expected) +{ + if (!value || !expected) + return false; + size_t expected_size = strlen(expected); + if (strncasecmp(value, expected, expected_size) != 0) + return false; + char suffix = value[expected_size]; + return suffix == '\0' || suffix == ';' || suffix == ' ' || suffix == '\t'; +} + +static int parse_size_value(const char* value, size_t value_size, size_t* output) +{ + if (!value || !value_size || !output || value_size >= 32) + return 0; + char buffer[32]; + memcpy(buffer, value, value_size); + buffer[value_size] = '\0'; + if (buffer[0] == '-') + return 0; + errno = 0; + char* end = NULL; + unsigned long long parsed = strtoull(buffer, &end, 10); + if (errno == ERANGE || !end || *end || parsed > SIZE_MAX) + return 0; + *output = (size_t)parsed; + return 1; +} + +static int connection_prepare_body(chttpx_connection_t* connection) +{ + const char* start = connection->headers; + const char* end = connection->headers + connection->header_end; + const char* line = chttpx_memmem(start, connection->header_end, "\r\n", 2); + if (!line) + return cHTTPX_StatusBadRequest; + line += 2; + + bool has_content_length = false; + bool has_transfer_encoding = false; + size_t content_length = 0; + char content_type[512] = {0}; + + while (line < end) + { + const char* line_end = chttpx_memmem(line, (size_t)(end - line), "\r\n", 2); + if (!line_end) + return cHTTPX_StatusBadRequest; + if (line_end == line) + break; + + const char* colon = memchr(line, ':', (size_t)(line_end - line)); + if (!colon) + return cHTTPX_StatusBadRequest; + + const char* name_start = line; + const char* name_end = colon; + while (name_end > name_start && (name_end[-1] == ' ' || name_end[-1] == '\t')) + name_end--; + + const char* value_start = colon + 1; + while (value_start < line_end && (*value_start == ' ' || *value_start == '\t')) + value_start++; + const char* value_end = line_end; + while (value_end > value_start && (value_end[-1] == ' ' || value_end[-1] == '\t')) + value_end--; + + size_t name_size = (size_t)(name_end - name_start); + size_t value_size = (size_t)(value_end - value_start); + + if (span_equal_ci(name_start, name_size, "Content-Length")) + { + size_t parsed = 0; + if (!parse_size_value(value_start, value_size, &parsed)) + return cHTTPX_StatusBadRequest; + if (has_content_length && parsed != content_length) + return cHTTPX_StatusBadRequest; + has_content_length = true; + content_length = parsed; + } + else if (span_equal_ci(name_start, name_size, "Transfer-Encoding")) + { + if (has_transfer_encoding || !span_equal_ci(value_start, value_size, "chunked")) + return cHTTPX_StatusBadRequest; + has_transfer_encoding = true; + } + else if (span_equal_ci(name_start, name_size, "Content-Type")) + { + size_t copy = value_size < sizeof(content_type) - 1 ? value_size : sizeof(content_type) - 1; + memcpy(content_type, value_start, copy); + content_type[copy] = '\0'; + } + + line = line_end + 2; + } + + if (has_content_length && has_transfer_encoding) + return cHTTPX_StatusBadRequest; + + connection->memory_body = content_type_matches(content_type, cHTTPX_CTYPE_JSON) || content_type_matches(content_type, cHTTPX_CTYPE_FORM) || strncasecmp(content_type, "text/", 5) == 0; + connection->multipart_body = content_type_matches(content_type, cHTTPX_CTYPE_MULTI); + connection->body_limit = connection->memory_body ? connection->server->max_body_size : connection->server->max_upload_size; + connection->chunked = has_transfer_encoding; + connection->content_length = has_content_length ? content_length : 0; + + if (connection->chunked) + { + connection->chunk_state = CHTTPX_CHUNK_SIZE; + if (!connection->memory_body) + { + connection->body_stream = tmpfile(); + if (!connection->body_stream) + return cHTTPX_StatusInternalServerError; + } + connection->state = CHTTPX_CONN_READING_BODY; + return 0; + } + + if (connection->content_length > connection->body_limit) + return cHTTPX_StatusPayloadTooLarge; + if (connection->content_length == 0) + return 0; + + if (connection->memory_body) + { + if (connection->content_length == SIZE_MAX) + return cHTTPX_StatusPayloadTooLarge; + connection->body = malloc(connection->content_length + 1); + if (!connection->body) + return cHTTPX_StatusInternalServerError; + connection->body_capacity = connection->content_length + 1; + } + else if (connection->multipart_body) + { + connection->body_stream = tmpfile(); + if (!connection->body_stream) + return cHTTPX_StatusInternalServerError; + } + else + connection->discard_body = true; + + connection->state = CHTTPX_CONN_READING_BODY; + return 0; +} + +static int connection_store_decoded(chttpx_connection_t* connection, const unsigned char* data, size_t size) +{ + if (size == 0) + return 1; + if (connection->chunk_decoded > connection->body_limit || size > connection->body_limit - connection->chunk_decoded) + return 0; + + if (connection->memory_body) + { + size_t required = connection->chunk_decoded + size + 1; + if (required > connection->body_capacity) + { + size_t capacity = connection->body_capacity ? connection->body_capacity : 4096; + while (capacity < required) + { + if (capacity > (connection->body_limit + 1) / 2) + { + capacity = connection->body_limit + 1; + break; + } + capacity *= 2; + } + if (capacity < required) + return 0; + unsigned char* resized = realloc(connection->body, capacity); + if (!resized) + return 0; + connection->body = resized; + connection->body_capacity = capacity; + } + memcpy(connection->body + connection->chunk_decoded, data, size); + connection->body[connection->chunk_decoded + size] = '\0'; + } + else if (connection->body_stream) + { + if (fwrite(data, 1, size, connection->body_stream) != size) + return 0; + } + + connection->chunk_decoded += size; + return 1; +} + +static int connection_feed_chunked(chttpx_connection_t* connection, const unsigned char* data, size_t size) +{ + size_t offset = 0; + while (offset < size && connection->chunk_state != CHTTPX_CHUNK_DONE) + { + switch (connection->chunk_state) + { + case CHTTPX_CHUNK_SIZE: + if (data[offset] == '\r') + connection->chunk_state = CHTTPX_CHUNK_SIZE_LF; + else + { + if (connection->chunk_line_size + 1 >= sizeof(connection->chunk_line)) + return cHTTPX_StatusBadRequest; + connection->chunk_line[connection->chunk_line_size++] = (char)data[offset]; + } + offset++; + break; + + case CHTTPX_CHUNK_SIZE_LF: + { + if (data[offset++] != '\n') + return cHTTPX_StatusBadRequest; + connection->chunk_line[connection->chunk_line_size] = '\0'; + char* extension = strchr(connection->chunk_line, ';'); + if (extension) + *extension = '\0'; + if (!connection->chunk_line[0]) + return cHTTPX_StatusBadRequest; + errno = 0; + char* end = NULL; + unsigned long long parsed = strtoull(connection->chunk_line, &end, 16); + if (errno == ERANGE || !end || *end || parsed > SIZE_MAX) + return cHTTPX_StatusBadRequest; + if ((size_t)parsed > connection->body_limit - connection->chunk_decoded) + return cHTTPX_StatusPayloadTooLarge; + connection->chunk_line_size = 0; + connection->chunk_remaining = (size_t)parsed; + connection->chunk_state = connection->chunk_remaining ? CHTTPX_CHUNK_DATA : CHTTPX_CHUNK_TRAILER; + break; + } + + case CHTTPX_CHUNK_DATA: + { + size_t available = size - offset; + size_t take = connection->chunk_remaining < available ? connection->chunk_remaining : available; + if (!connection_store_decoded(connection, data + offset, take)) + return cHTTPX_StatusInternalServerError; + offset += take; + connection->chunk_remaining -= take; + if (connection->chunk_remaining == 0) + connection->chunk_state = CHTTPX_CHUNK_DATA_CR; + break; + } + + case CHTTPX_CHUNK_DATA_CR: + if (data[offset++] != '\r') + return cHTTPX_StatusBadRequest; + connection->chunk_state = CHTTPX_CHUNK_DATA_LF; + break; + + case CHTTPX_CHUNK_DATA_LF: + if (data[offset++] != '\n') + return cHTTPX_StatusBadRequest; + connection->chunk_state = CHTTPX_CHUNK_SIZE; + break; + + case CHTTPX_CHUNK_TRAILER: + if (data[offset] == '\r') + connection->chunk_state = CHTTPX_CHUNK_TRAILER_LF; + else + { + if (connection->chunk_line_size + 1 >= sizeof(connection->chunk_line)) + return cHTTPX_StatusBadRequest; + connection->chunk_line[connection->chunk_line_size++] = (char)data[offset]; + } + offset++; + break; + + case CHTTPX_CHUNK_TRAILER_LF: + if (data[offset++] != '\n') + return cHTTPX_StatusBadRequest; + if (connection->chunk_line_size == 0) + connection->chunk_state = CHTTPX_CHUNK_DONE; + else + { + connection->chunk_line_size = 0; + connection->chunk_state = CHTTPX_CHUNK_TRAILER; + } + break; + + case CHTTPX_CHUNK_DONE: + break; + } + } + + if (connection->chunk_state == CHTTPX_CHUNK_DONE) + { + connection->content_length = connection->chunk_decoded; + connection->body_size = connection->memory_body ? connection->chunk_decoded : 0; + } + return 0; +} + +static int connection_feed_body(chttpx_connection_t* connection, const unsigned char* data, size_t size) +{ + if (connection->chunked) + return connection_feed_chunked(connection, data, size); + + if (connection->body_received >= connection->content_length) + return 0; + + size_t remaining = connection->content_length - connection->body_received; + size_t take = size < remaining ? size : remaining; + + if (connection->memory_body && take) + memcpy(connection->body + connection->body_received, data, take); + else if (connection->body_stream && take && fwrite(data, 1, take, connection->body_stream) != take) + return cHTTPX_StatusInternalServerError; + + connection->body_received += take; + if (connection->memory_body && connection->body_received == connection->content_length) + { + connection->body[connection->body_received] = '\0'; + connection->body_size = connection->body_received; + } + return 0; +} + +static bool connection_body_complete(chttpx_connection_t* connection) +{ + return connection->chunked ? connection->chunk_state == CHTTPX_CHUNK_DONE : connection->body_received >= connection->content_length; +} + +static int connection_error_response(chttpx_runtime_t* runtime, chttpx_connection_t* connection, int status) +{ + const char* reason = cHTTPX_StatusReason((uint16_t)status); + char buffer[256]; + int size = snprintf(buffer, sizeof(buffer), "HTTP/1.1 %d %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", status, reason); + if (size <= 0 || (size_t)size >= sizeof(buffer)) + { + connection_close(runtime, connection); + return 0; + } + connection->write_buffer = malloc((size_t)size); + if (!connection->write_buffer) + { + connection_close(runtime, connection); + return 0; + } + memcpy(connection->write_buffer, buffer, (size_t)size); + connection->write_size = (size_t)size; + connection->write_offset = 0; + connection->state = CHTTPX_CONN_WRITING; + connection_touch(connection); + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; +} + +static int connection_submit(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + if (connection->body_stream) + { + if (fflush(connection->body_stream) != 0 || fseek(connection->body_stream, 0, SEEK_SET) != 0) + { + connection_error_response(runtime, connection, cHTTPX_StatusInternalServerError); + return 0; + } + } + _chttpx_event_del(runtime->event_loop, connection->fd); + connection->state = CHTTPX_CONN_PROCESSING; + if (!worker_submit(runtime, connection)) + { + connection_close(runtime, connection); + return 0; + } + return 1; +} + +static int connection_process_bytes(chttpx_runtime_t* runtime, chttpx_connection_t* connection, const unsigned char* data, size_t size) +{ + if (connection->state == CHTTPX_CONN_READING_HEADERS) + { + size_t header_limit = connection->server->max_header_size ? connection->server->max_header_size : BUFFER_SIZE - 1; + if (header_limit > SIZE_MAX - BUFFER_SIZE - 1) + { + connection_close(runtime, connection); + return 0; + } + size_t maximum = header_limit + BUFFER_SIZE + 1; + if (size > maximum - connection->header_size) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, cHTTPX_StatusRequestHeaderFieldsTooLarge); + } + + size_t required = connection->header_size + size + 1; + if (required > connection->header_capacity) + { + size_t capacity = connection->header_capacity ? connection->header_capacity : 4096; + while (capacity < required) + { + size_t next = capacity * 2; + if (next < capacity || next > maximum) + next = maximum; + capacity = next; + if (capacity < required && capacity == maximum) + break; + } + if (capacity < required) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, cHTTPX_StatusRequestHeaderFieldsTooLarge); + } + char* resized = realloc(connection->headers, capacity); + if (!resized) + return connection_error_response(runtime, connection, cHTTPX_StatusInternalServerError); + connection->headers = resized; + connection->header_capacity = capacity; + } + + memcpy(connection->headers + connection->header_size, data, size); + connection->header_size += size; + connection->headers[connection->header_size] = '\0'; + + char* delimiter = chttpx_memmem(connection->headers, connection->header_size, "\r\n\r\n", 4); + if (!delimiter) + { + if (connection->header_size > header_limit) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, cHTTPX_StatusRequestHeaderFieldsTooLarge); + } + return 1; + } + + connection->header_end = (size_t)(delimiter - connection->headers) + 4; + if (connection->header_end > header_limit) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, cHTTPX_StatusRequestHeaderFieldsTooLarge); + } + + int body_status = connection_prepare_body(connection); + if (body_status) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, body_status); + } + + size_t initial_size = connection->header_size - connection->header_end; + if (initial_size) + { + int body_result = connection_feed_body(connection, (unsigned char*)connection->headers + connection->header_end, initial_size); + if (body_result) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, body_result); + } + } + + connection->header_size = connection->header_end; + connection->headers[connection->header_end] = '\0'; + + if (connection_body_complete(connection)) + return connection_submit(runtime, connection); + return 1; + } + + if (connection->state == CHTTPX_CONN_READING_BODY) + { + int body_result = connection_feed_body(connection, data, size); + if (body_result) + { + _chttpx_metrics_parser_failure(connection->server); + return connection_error_response(runtime, connection, body_result); + } + if (connection_body_complete(connection)) + return connection_submit(runtime, connection); + } + return 1; +} + +static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + int result = _chttpx_tls_accept_step(connection->tls_session); + if (result == CHTTPX_OK) + { + connection->state = CHTTPX_CONN_READING_HEADERS; + connection_touch(connection); + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_IO_WANT_READ) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_IO_WANT_WRITE) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + _chttpx_metrics_connection_rejected(connection->server); + _chttpx_tls_log_error(connection->server, "-", "TLS handshake failed"); + connection_close(runtime, connection); + return 0; +} + +static int connection_read_ready(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + unsigned char buffer[BUFFER_SIZE]; + for (;;) + { + int result = _chttpx_io_recv_nonblocking(connection->fd, connection->tls_session, buffer, sizeof(buffer)); + if (result > 0) + { + connection_touch(connection); + if (!connection_process_bytes(runtime, connection, buffer, (size_t)result)) + return 0; + if (connection->state == CHTTPX_CONN_PROCESSING || connection->state == CHTTPX_CONN_WRITING) + return 1; + continue; + } + if (result == 0) + { + connection_close(runtime, connection); + return 0; + } + if (result == CHTTPX_IO_WANT_READ) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_IO_WANT_WRITE) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_ERR_TLS) + _chttpx_tls_log_error(connection->server, "-", "TLS request read failed"); + connection_close(runtime, connection); + return 0; + } +} + +static int connection_write_ready(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +{ + while (connection->write_offset < connection->write_size) + { + int result = _chttpx_io_send_nonblocking(connection->fd, connection->tls_session, connection->write_buffer + connection->write_offset, connection->write_size - connection->write_offset); + if (result > 0) + { + connection->write_offset += (size_t)result; + connection_touch(connection); + continue; + } + if (result == CHTTPX_IO_WANT_WRITE) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_IO_WANT_READ) + { + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } + if (result == CHTTPX_ERR_TLS) + _chttpx_tls_log_error(connection->server, "-", "TLS response write failed"); + connection_close(runtime, connection); + return 0; + } + connection_close(runtime, connection); + return 0; +} + +static void drain_completions(chttpx_runtime_t* runtime) +{ + chttpx_connection_t* connection = completion_take_all(runtime); + while (connection) + { + chttpx_connection_t* next = connection->completion_next; + connection->completion_next = NULL; + if (runtime_is_stopping(runtime) || connection->worker_result != CHTTPX_OK || !connection->write_buffer) + connection_close(runtime, connection); + else + { + connection->state = CHTTPX_CONN_WRITING; + connection->write_offset = 0; + connection_touch(connection); + if (_chttpx_event_add(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) + connection_close(runtime, connection); + } + connection = next; + } +} + +static void accept_connections(chttpx_runtime_t* runtime) +{ + chttpx_serv_t* server = runtime->server; + for (;;) + { + chttpx_socket_t fd = accept(server->server_fd, NULL, NULL); + if (!socket_valid(fd)) + { +#ifdef CHTTPX_PLATFORM_WINDOWS + int error = WSAGetLastError(); + if (error == WSAEWOULDBLOCK || error == WSAEINTR) + return; +#else + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return; +#endif + return; + } + + _chttpx_metrics_connection_accepted(server); + + if (runtime_is_stopping(runtime) || __atomic_load_n(&server->current_clients, __ATOMIC_SEQ_CST) >= server->max_clients) + { + if (!server->tls.enabled) + { + static const char busy[] = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + send(fd, busy, (int)(sizeof(busy) - 1), 0); + } + _chttpx_metrics_connection_rejected(server); + chttpx_close(fd); + continue; + } + + if (_chttpx_socket_set_nonblocking(fd) != 0) + { + _chttpx_metrics_connection_rejected(server); + chttpx_close(fd); + continue; + } + + chttpx_connection_t* connection = calloc(1, sizeof(*connection)); + if (!connection) + { + _chttpx_metrics_connection_rejected(server); + chttpx_close(fd); + continue; + } + + connection->runtime = runtime; + connection->server = server; + connection->fd = fd; + connection->state = CHTTPX_CONN_TLS; + connection_touch(connection); + connection->next = runtime->connections; + runtime->connections = connection; + __atomic_fetch_add(&server->current_clients, 1, __ATOMIC_SEQ_CST); + _chttpx_metrics_connection_opened(server); + + int tls_result = _chttpx_tls_accept_begin(server, fd, &connection->tls_session); + if (tls_result != CHTTPX_OK) + { + _chttpx_metrics_connection_rejected(server); + connection_close(runtime, connection); + continue; + } + + uint32_t interest = CHTTPX_EVENT_READ; + if (server->tls.enabled) + { + int step = _chttpx_tls_accept_step(connection->tls_session); + if (step == CHTTPX_OK) + connection->state = CHTTPX_CONN_READING_HEADERS; + else if (step == CHTTPX_IO_WANT_READ) + interest = CHTTPX_EVENT_READ; + else if (step == CHTTPX_IO_WANT_WRITE) + interest = CHTTPX_EVENT_WRITE; + else + { + _chttpx_metrics_connection_rejected(server); + _chttpx_tls_log_error(server, "-", "TLS handshake failed"); + connection_close(runtime, connection); + continue; + } + } + else + connection->state = CHTTPX_CONN_READING_HEADERS; + + if (_chttpx_event_add(runtime->event_loop, fd, interest, connection) != 0) + { + connection_close(runtime, connection); + continue; + } + } +} + +static void scan_timeouts(chttpx_runtime_t* runtime) +{ + uint64_t now = monotonic_ms(); + chttpx_connection_t* connection = runtime->connections; + while (connection) + { + chttpx_connection_t* next = connection->next; + if (connection->state != CHTTPX_CONN_PROCESSING) + { + uint16_t timeout = connection->state == CHTTPX_CONN_WRITING ? connection->server->write_timeout_sec : connection->server->read_timeout_sec; + if (connection->server->idle_timeout_sec && (!timeout || connection->server->idle_timeout_sec < timeout)) + timeout = connection->server->idle_timeout_sec; + if (timeout && now - connection->last_activity_ms >= (uint64_t)timeout * 1000ULL) + { + _chttpx_metrics_timeout_failure(connection->server); + connection_close(runtime, connection); + } + } + connection = next; + } +} + +static void begin_shutdown(chttpx_runtime_t* runtime) +{ + if (runtime->shutdown_started) + return; + runtime->shutdown_started = true; + if (runtime->listener_registered) + { + _chttpx_event_del(runtime->event_loop, runtime->server->server_fd); + runtime->listener_registered = false; + } + + chttpx_connection_t* connection = runtime->connections; + while (connection) + { + chttpx_connection_t* next = connection->next; + if (connection->state != CHTTPX_CONN_PROCESSING) + connection_close(runtime, connection); + connection = next; + } +} + +int _chttpx_runtime_init(chttpx_serv_t* server) +{ + if (!server || !socket_valid(server->server_fd) || server->max_clients == 0) + return CHTTPX_ERR_INVALID_ARGUMENT; + + chttpx_runtime_t* runtime = calloc(1, sizeof(*runtime)); + if (!runtime) + return CHTTPX_ERR_MEMORY; + runtime->server = server; + runtime->job_capacity = server->max_clients; + runtime->jobs = calloc(runtime->job_capacity, sizeof(*runtime->jobs)); + if (!runtime->jobs) + { + free(runtime); + return CHTTPX_ERR_MEMORY; + } + if (sync_init(runtime) != 0) + { + free(runtime->jobs); + free(runtime); + return CHTTPX_ERR_IO; + } + + runtime->event_loop = _chttpx_event_create(); + if (!runtime->event_loop || _chttpx_socket_set_nonblocking(server->server_fd) != 0 || _chttpx_event_add(runtime->event_loop, server->server_fd, CHTTPX_EVENT_READ, runtime) != 0) + goto error; + runtime->listener_registered = true; + + for (size_t i = 0; i < CHTTPX_WORKER_THREADS; i++) + { + if (_thread_create(&runtime->workers[i], runtime_worker, runtime) != 0) + goto error; + runtime->workers_started++; + } + + server->runtime_state = runtime; + return CHTTPX_OK; + +error: + job_lock(runtime); + runtime->workers_stopping = true; + job_broadcast(runtime); + job_unlock(runtime); + for (size_t i = 0; i < runtime->workers_started; i++) + _thread_join(runtime->workers[i]); + if (runtime->event_loop) + _chttpx_event_destroy(runtime->event_loop); + sync_destroy(runtime); + free(runtime->jobs); + free(runtime); + return CHTTPX_ERR_IO; +} + +void _chttpx_runtime_listen(chttpx_serv_t* server) +{ + chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; + if (!runtime) + return; + + chttpx_event_t events[CHTTPX_RUNTIME_EVENTS]; + while (true) + { + if (runtime_is_stopping(runtime) || __atomic_load_n(&server->shutdown_requested, __ATOMIC_ACQUIRE)) + begin_shutdown(runtime); + + drain_completions(runtime); + + if (runtime->shutdown_started && __atomic_load_n(&server->current_clients, __ATOMIC_SEQ_CST) == 0) + break; + + int count = _chttpx_event_wait(runtime->event_loop, events, CHTTPX_RUNTIME_EVENTS, 100); + if (count < 0) + continue; + + for (int i = 0; i < count; i++) + { + if (events[i].events & CHTTPX_EVENT_WAKE) + { + drain_completions(runtime); + continue; + } + + if (events[i].data == runtime) + { + if (!runtime_is_stopping(runtime)) + accept_connections(runtime); + continue; + } + + chttpx_connection_t* connection = events[i].data; + if (!connection || connection->state == CHTTPX_CONN_PROCESSING) + continue; + + int alive = 1; + if (connection->state == CHTTPX_CONN_TLS) + alive = connection_tls_step(runtime, connection); + else if (connection->state == CHTTPX_CONN_READING_HEADERS || connection->state == CHTTPX_CONN_READING_BODY) + alive = connection_read_ready(runtime, connection); + else if (connection->state == CHTTPX_CONN_WRITING) + alive = connection_write_ready(runtime, connection); + + if (alive && (events[i].events & CHTTPX_EVENT_ERROR) && connection->state != CHTTPX_CONN_PROCESSING && connection->state != CHTTPX_CONN_WRITING) + connection_close(runtime, connection); + } + + if (!runtime->shutdown_started) + scan_timeouts(runtime); + } + + drain_completions(runtime); +} + +void _chttpx_runtime_request_stop(chttpx_serv_t* server) +{ + chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; + if (!runtime) + return; + __atomic_store_n(&runtime->stopping, true, __ATOMIC_RELEASE); + _chttpx_event_wake(runtime->event_loop); +} + +void _chttpx_runtime_cleanup(chttpx_serv_t* server) +{ + chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; + if (!runtime) + return; + + __atomic_store_n(&runtime->stopping, true, __ATOMIC_RELEASE); + begin_shutdown(runtime); + + job_lock(runtime); + runtime->workers_stopping = true; + job_broadcast(runtime); + job_unlock(runtime); + + for (size_t i = 0; i < runtime->workers_started; i++) + _thread_join(runtime->workers[i]); + + drain_completions(runtime); + + while (runtime->connections) + connection_close(runtime, runtime->connections); + + _chttpx_event_destroy(runtime->event_loop); + sync_destroy(runtime); + free(runtime->jobs); + free(runtime); + server->runtime_state = NULL; +} diff --git a/src/cHTTPX_runtime.h b/src/cHTTPX_runtime.h new file mode 100644 index 0000000..313d629 --- /dev/null +++ b/src/cHTTPX_runtime.h @@ -0,0 +1,11 @@ +#ifndef CHTTPX_RUNTIME_H +#define CHTTPX_RUNTIME_H + +#include "cHTTPX_serv.h" + +int _chttpx_runtime_init(chttpx_serv_t* server); +void _chttpx_runtime_listen(chttpx_serv_t* server); +void _chttpx_runtime_request_stop(chttpx_serv_t* server); +void _chttpx_runtime_cleanup(chttpx_serv_t* server); + +#endif diff --git a/src/cHTTPX_serv.c b/src/cHTTPX_serv.c index bd57dde..d6fdc63 100644 --- a/src/cHTTPX_serv.c +++ b/src/cHTTPX_serv.c @@ -17,6 +17,7 @@ #include "cHTTPX_tls.h" #include "cHTTPX_compression.h" #include "cHTTPX_metrics.h" +#include "cHTTPX_runtime.h" #include @@ -305,6 +306,19 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha return metrics_result; } + int runtime_result = _chttpx_runtime_init(server); + if (runtime_result != CHTTPX_OK) + { + _chttpx_metrics_server_cleanup(server); + _chttpx_tls_server_cleanup(server); + chttpx_close(server->server_fd); + invalidate_socket(server); + free_server_languages(server); + free(server->name); + server->name = NULL; + return runtime_result; + } + server->initialized = true; return CHTTPX_OK; @@ -507,22 +521,6 @@ void cHTTPX_SetLogger(chttpx_serv_t* server, chttpx_logger_fn logger, void* user server->log_level = level; } -static void* handle_client_wrapper(void* arg) -{ - chttpx_client_ctx_t* context = arg; - chttpx_serv_t* server = context ? context->server : NULL; - if (!context || !server) - { - free(context); - return NULL; - } - - chttpx_handle(context); - _chttpx_metrics_connection_closed(server); - __atomic_fetch_sub(&server->current_clients, 1, __ATOMIC_SEQ_CST); - return NULL; -} - void _chttpx_server_listen(chttpx_serv_t* server) { if (!server || !server->initialized || !socket_valid(server->server_fd)) @@ -532,76 +530,7 @@ void _chttpx_server_listen(chttpx_serv_t* server) if (!__atomic_compare_exchange_n(&server->listening, &expected, true, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return; - while (!__atomic_load_n(&server->shutdown_requested, __ATOMIC_ACQUIRE)) - { - chttpx_socket_t client_fd = accept(server->server_fd, NULL, NULL); - if (!socket_valid(client_fd)) - { - if (__atomic_load_n(&server->shutdown_requested, __ATOMIC_ACQUIRE)) - break; -#ifdef CHTTPX_PLATFORM_POSIX - if (errno == EINTR) - continue; -#endif - server_sleep_ms(10); - continue; - } - - _chttpx_metrics_connection_accepted(server); - - if (__atomic_load_n(&server->shutdown_requested, __ATOMIC_ACQUIRE)) - { - chttpx_close(client_fd); - break; - } - - if (__atomic_load_n(&server->current_clients, __ATOMIC_SEQ_CST) >= server->max_clients) - { - /* - * A TLS connection has not completed its handshake yet, so raw - * HTTP bytes must never be written to it here. - */ - if (!server->tls.enabled) - { - static const char busy[] = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - cHTTPX_SendAll(client_fd, busy, sizeof(busy) - 1); - } - _chttpx_metrics_connection_rejected(server); - chttpx_close(client_fd); - continue; - } - - chttpx_client_ctx_t* context = malloc(sizeof(*context)); - if (!context) - { - _chttpx_metrics_connection_rejected(server); - chttpx_close(client_fd); - continue; - } - - context->server = server; - context->client_fd = client_fd; - __atomic_fetch_add(&server->current_clients, 1, __ATOMIC_SEQ_CST); - _chttpx_metrics_connection_opened(server); - - thread_t thread_id; - if (_thread_create(&thread_id, handle_client_wrapper, context) != 0) - { - free(context); - chttpx_close(client_fd); - _chttpx_metrics_connection_closed(server); - _chttpx_metrics_connection_rejected(server); - __atomic_fetch_sub(&server->current_clients, 1, __ATOMIC_SEQ_CST); - continue; - } - -#ifdef CHTTPX_PLATFORM_WINDOWS - CloseHandle(thread_id); -#else - pthread_detach(thread_id); -#endif - } - + _chttpx_runtime_listen(server); __atomic_store_n(&server->listening, false, __ATOMIC_RELEASE); } @@ -611,6 +540,12 @@ void _chttpx_server_shutdown(chttpx_serv_t* server) return; __atomic_store_n(&server->shutdown_requested, true, __ATOMIC_RELEASE); + _chttpx_runtime_request_stop(server); + + while (__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)) + server_sleep_ms(10); + + _chttpx_runtime_cleanup(server); if (socket_valid(server->server_fd)) { @@ -623,12 +558,6 @@ void _chttpx_server_shutdown(chttpx_serv_t* server) invalidate_socket(server); } - while (__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)) - server_sleep_ms(10); - - while (__atomic_load_n(&server->current_clients, __ATOMIC_SEQ_CST) > 0) - server_sleep_ms(10); - _chttpx_tls_server_cleanup(server); _chttpx_metrics_server_cleanup(server); diff --git a/src/cHTTPX_serv.h b/src/cHTTPX_serv.h index fdffadf..b1c8438 100644 --- a/src/cHTTPX_serv.h +++ b/src/cHTTPX_serv.h @@ -166,6 +166,7 @@ extern "C" void* rate_limiter_state; void* compression_state; void* metrics_state; + void* runtime_state; chttpx_cors_t cors; } chttpx_serv_t; diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index e8388a8..b024d99 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -168,6 +168,7 @@ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** return CHTTPX_OK; #ifndef CHTTPX_ENABLE_TLS + (void)client_fd; return CHTTPX_ERR_UNAVAILABLE; #else SSL* ssl = SSL_new((SSL_CTX*)server->_tls_ctx); @@ -189,6 +190,50 @@ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** #endif } +int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session) +{ + if (!server || !session) + return CHTTPX_ERR_INVALID_ARGUMENT; + *session = NULL; + if (!server->tls.enabled) + return CHTTPX_OK; +#ifndef CHTTPX_ENABLE_TLS + (void)client_fd; + return CHTTPX_ERR_UNAVAILABLE; +#else + SSL* ssl = SSL_new((SSL_CTX*)server->_tls_ctx); + if (!ssl) + return CHTTPX_ERR_TLS; + if (SSL_set_fd(ssl, (int)client_fd) != 1) + { + SSL_free(ssl); + return CHTTPX_ERR_TLS; + } + SSL_set_accept_state(ssl); + *session = ssl; + return CHTTPX_OK; +#endif +} + +int _chttpx_tls_accept_step(void* session) +{ + if (!session) + return CHTTPX_OK; +#ifndef CHTTPX_ENABLE_TLS + return CHTTPX_ERR_UNAVAILABLE; +#else + int result = SSL_accept((SSL*)session); + if (result == 1) + return CHTTPX_OK; + int error = SSL_get_error((SSL*)session, result); + if (error == SSL_ERROR_WANT_READ) + return CHTTPX_IO_WANT_READ; + if (error == SSL_ERROR_WANT_WRITE) + return CHTTPX_IO_WANT_WRITE; + return CHTTPX_ERR_TLS; +#endif +} + void _chttpx_tls_session_close(void* session) { #ifdef CHTTPX_ENABLE_TLS @@ -359,6 +404,92 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t } } +int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size) +{ + if (!buffer || size == 0) + return CHTTPX_ERR_INVALID_ARGUMENT; + size_t wanted = size > INT_MAX ? INT_MAX : size; +#ifdef CHTTPX_ENABLE_TLS + if (tls_session) + { + int result = SSL_read((SSL*)tls_session, buffer, (int)wanted); + if (result > 0) + return result; + int error = SSL_get_error((SSL*)tls_session, result); + if (error == SSL_ERROR_WANT_READ) + return CHTTPX_IO_WANT_READ; + if (error == SSL_ERROR_WANT_WRITE) + return CHTTPX_IO_WANT_WRITE; + if (error == SSL_ERROR_ZERO_RETURN) + return 0; + return CHTTPX_ERR_TLS; + } +#else + (void)tls_session; +#endif + int result = recv(fd, (char*)buffer, (int)wanted, 0); + if (result >= 0) + return result; +#ifdef CHTTPX_PLATFORM_WINDOWS + int error = WSAGetLastError(); + if (error == WSAEWOULDBLOCK) + return CHTTPX_IO_WANT_READ; + if (error == WSAEINTR) + return CHTTPX_IO_WANT_READ; +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) + return CHTTPX_IO_WANT_READ; + if (errno == EINTR) + return CHTTPX_IO_WANT_READ; +#endif + return CHTTPX_ERR_IO; +} + +int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) +{ + if (size > 0 && !data) + return CHTTPX_ERR_INVALID_ARGUMENT; + if (size == 0) + return 0; + size_t wanted = size > INT_MAX ? INT_MAX : size; +#ifdef CHTTPX_ENABLE_TLS + if (tls_session) + { + int result = SSL_write((SSL*)tls_session, data, (int)wanted); + if (result > 0) + return result; + int error = SSL_get_error((SSL*)tls_session, result); + if (error == SSL_ERROR_WANT_READ) + return CHTTPX_IO_WANT_READ; + if (error == SSL_ERROR_WANT_WRITE) + return CHTTPX_IO_WANT_WRITE; + return CHTTPX_ERR_TLS; + } +#else + (void)tls_session; +#endif + int flags = 0; +#ifdef MSG_NOSIGNAL + flags |= MSG_NOSIGNAL; +#endif + int result = send(fd, (const char*)data, (int)wanted, flags); + if (result >= 0) + return result; +#ifdef CHTTPX_PLATFORM_WINDOWS + int error = WSAGetLastError(); + if (error == WSAEWOULDBLOCK) + return CHTTPX_IO_WANT_WRITE; + if (error == WSAEINTR) + return CHTTPX_IO_WANT_WRITE; +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) + return CHTTPX_IO_WANT_WRITE; + if (errno == EINTR) + return CHTTPX_IO_WANT_WRITE; +#endif + return CHTTPX_ERR_IO; +} + int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) { if (size > 0 && !data) diff --git a/src/cHTTPX_tls.h b/src/cHTTPX_tls.h index 60c2ad5..7648e3c 100644 --- a/src/cHTTPX_tls.h +++ b/src/cHTTPX_tls.h @@ -13,6 +13,10 @@ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* config); void _chttpx_tls_server_cleanup(chttpx_serv_t* server); int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session); +#define CHTTPX_IO_WANT_READ -1001 +#define CHTTPX_IO_WANT_WRITE -1002 +int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session); +int _chttpx_tls_accept_step(void* session); void _chttpx_tls_session_close(void* session); int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, @@ -21,6 +25,8 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, void _chttpx_tls_client_close(void* context, void* session); int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size); +int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size); +int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const void* data, size_t size); int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, size_t size); #ifdef __cplusplus diff --git a/tests/test_server.c b/tests/test_server.c index 59e5c19..f39039e 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -157,6 +157,23 @@ static void exchange(uint16_t port, const char* request, char* response, size_t exchange_family(port, AF_INET, request, response, response_size); } +static chttpx_socket_t open_idle_connection(uint16_t port) +{ + chttpx_socket_t socket_fd = socket(AF_INET, SOCK_STREAM, 0); +#ifdef CHTTPX_PLATFORM_WINDOWS + assert(socket_fd != INVALID_SOCKET); +#else + assert(socket_fd >= 0); +#endif + struct sockaddr_in address = {0}; + address.sin_family = AF_INET; + address.sin_port = htons(port); + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + assert(connect(socket_fd, (struct sockaddr*)&address, sizeof(address)) == 0); + return socket_fd; +} + + static void exchange_ipv6(uint16_t port, const char* request, char* response, size_t response_size) { exchange_family(port, AF_INET6, request, response, response_size); @@ -252,6 +269,22 @@ int main(void) char response[4096]; + chttpx_socket_t idle_connections[40]; + for (size_t i = 0; i < CHTTPX_ARRAY_LEN(idle_connections); i++) + idle_connections[i] = open_idle_connection(public_port); + + struct timespec concurrency_start; + struct timespec concurrency_end; + clock_gettime(CLOCK_MONOTONIC, &concurrency_start); + exchange(public_port, "GET /ip HTTP/1.1\r\nHost: localhost\r\n\r\n", response, sizeof(response)); + clock_gettime(CLOCK_MONOTONIC, &concurrency_end); + double concurrency_seconds = (double)(concurrency_end.tv_sec - concurrency_start.tv_sec) + (double)(concurrency_end.tv_nsec - concurrency_start.tv_nsec) / 1000000000.0; + assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); + assert(concurrency_seconds < 1.0); + + for (size_t i = 0; i < CHTTPX_ARRAY_LEN(idle_connections); i++) + chttpx_close(idle_connections[i]); + exchange_ipv6(public_port, "GET /ip HTTP/1.1\r\n" "Host: localhost\r\n" From ea4f90fb09b422f1ce5467c37a7c3e70184387c7 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Tue, 22 Sep 2026 05:20:30 +0500 Subject: [PATCH 02/48] feat: establish maintainer runtime quality baseline (#57) * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: normalize public error result names * refactor: extract bounded worker pool * refactor: extract bounded worker pool * refactor: separate runtime worker pool * feat: expose internal worker runtime stats * feat: expose internal worker runtime stats * feat: expose worker runtime metrics * feat: export worker runtime metrics * feat: expose worker runtime metrics * feat: publish worker runtime metrics API * docs: define maintainer merge baseline * docs: document runtime architecture and contracts * docs: link maintainer architecture baseline * ci: add warnings sanitizers fuzz and format gates * test: add HTTP header parser fuzz target * ci: enforce maintainer quality gates * docs: document worker runtime metrics * test: cover worker runtime metrics * style: improve runtime code spacing * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * refactor: normalize public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * docs: use canonical public enum names * refactor: keep legacy server enum aliases * refactor: keep legacy server enum aliases * refactor: keep legacy normalizer aliases * refactor: keep legacy normalizer aliases * refactor: keep legacy body ownership aliases * refactor: keep legacy body ownership aliases * style: improve vertical spacing without forced wrapping * test: reflect runtime metrics lifecycle * fix: initialize runtime metrics snapshot * fix: make compression token bounds explicit * fix: make package validation pipefail-safe * docs: align PR checklist with maintainer baseline * fix: restore valid CI workflow --- .clang-format | 6 +- .github/pull_request_template.md | 7 +- .github/workflows/ci.yml | 136 +++++++++++--- CONTRIBUTING.md | 90 +++++++++ Makefile | 24 ++- docs/README.md | 5 + docs/app/README.md | 22 +-- docs/app/README_RU.md | 22 +-- docs/compression/README.md | 6 +- docs/compression/README_RU.md | 6 +- docs/json/README.md | 10 +- docs/json/README_RU.md | 10 +- docs/logging/README.md | 16 +- docs/logging/README_RU.md | 16 +- docs/maintainers/architecture.md | 129 +++++++++++++ docs/metrics/README.md | 8 +- docs/metrics/README_RU.md | 4 +- docs/responses/README.md | 4 +- docs/responses/README_RU.md | 4 +- docs/server/README.md | 40 ++-- docs/server/README_RU.md | 40 ++-- docs/site/docs.html | 4 +- docs/site/examples.html | 6 +- docs/tls/README.md | 4 +- docs/tls/README_RU.md | 4 +- example/basic.c | 4 +- example/compression.c | 6 +- example/json.c | 8 +- example/local_call.c | 6 +- example/metrics.c | 6 +- example/middleware.c | 4 +- example/multiple_servers.c | 4 +- example/remote_call.c | 12 +- example/tls.c | 4 +- example/upload.c | 4 +- include/libchttpx.h | 139 ++++++++++---- src/cHTTPX_app.c | 174 +++++++++--------- src/cHTTPX_body.c | 6 +- src/cHTTPX_compression.c | 93 +++++----- src/cHTTPX_i18n.c | 2 +- src/cHTTPX_i18n.h | 2 +- src/cHTTPX_media.c | 2 +- src/cHTTPX_metrics.c | 72 ++++++-- src/cHTTPX_metrics.h | 28 ++- src/cHTTPX_middlewares.c | 4 +- src/cHTTPX_request.c | 6 +- src/cHTTPX_request.h | 21 ++- src/cHTTPX_response.c | 52 +++--- src/cHTTPX_response.h | 11 +- src/cHTTPX_runtime.c | 255 ++++++++----------------- src/cHTTPX_runtime.h | 2 + src/cHTTPX_serv.c | 74 ++++---- src/cHTTPX_serv.h | 80 +++++--- src/cHTTPX_tls.c | 98 +++++----- src/cHTTPX_worker.c | 307 +++++++++++++++++++++++++++++++ src/cHTTPX_worker.h | 31 ++++ tests/fuzz_headers.c | 26 +++ tests/test_compression.c | 14 +- tests/test_core.c | 10 +- tests/test_metrics.c | 33 ++-- tests/test_server.c | 32 ++-- tests/test_tls.c | 12 +- 62 files changed, 1535 insertions(+), 732 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/maintainers/architecture.md create mode 100644 src/cHTTPX_worker.c create mode 100644 src/cHTTPX_worker.h create mode 100644 tests/fuzz_headers.c diff --git a/.clang-format b/.clang-format index 5dee993..4b2b501 100644 --- a/.clang-format +++ b/.clang-format @@ -9,9 +9,11 @@ AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AllowShortFunctionsOnASingleLine: None -ColumnLimit: 150 +ColumnLimit: 0 PointerAlignment: Left SpaceBeforeParens: ControlStatements -SortIncludes: false \ No newline at end of file +SortIncludes: false +MaxEmptyLinesToKeep: 1 +SeparateDefinitionBlocks: Always diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2825d54..b1a32ed 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,8 +20,11 @@ Use a **conventional commit** title — it becomes the release version on merge: - [ ] PR title follows the table above - [ ] Code builds on Linux (`make lin` and `make libchttpx.so`) - [ ] Code builds on Windows (`make win`) if applicable -- [ ] Source is formatted (`make lin-format` or `clang-format`) -- [ ] README updated if public API or usage changed +- [ ] Source follows `.clang-format` and keeps logical blocks readable +- [ ] ASan/UBSan pass; TSan passes for concurrency-sensitive changes +- [ ] Parser/network changes include negative-path tests and fuzz coverage where applicable +- [ ] Public API changes document ownership, lifetime, thread-safety, and compatibility impact +- [ ] Documentation is updated when public API or observable behavior changes ## Related issues diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c50387..1d20e93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, master] + branches: [main, master, develop] pull_request: branches: [main, master, develop, audit/core-safety-hardening] @@ -10,8 +10,55 @@ permissions: contents: read jobs: + style: + name: Format and public naming + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install clang-format + run: sudo apt update && sudo apt install -y clang-format + + - name: Check newly maintained runtime sources + run: clang-format --dry-run --Werror src/cHTTPX_worker.c src/cHTTPX_worker.h tests/fuzz_headers.c + + - name: Reject legacy result names outside compatibility aliases + shell: bash + run: | + set -euo pipefail + matches="$(grep -R -nE '\\bCHTTPX_(OK|ERR_[A-Z0-9_]+)\\b' src include example tests 2>/dev/null || true)" + matches="$(printf '%s\n' "$matches" | grep -vE '^(src/cHTTPX_serv\.h|include/libchttpx\.h):[0-9]+:#define CHTTPX_(OK|ERR_[A-Z0-9_]+) ' || true)" + if [ -n "$matches" ]; then + echo "Use canonical cHTTPX_OK / cHTTPX_ERR_* result names:" + echo "$matches" + exit 1 + fi + + static-analysis: + name: Static analysis (cppcheck) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install cppcheck + run: sudo apt update && sudo apt install -y cppcheck + + - name: Run cppcheck + run: | + cppcheck \ + --enable=warning,performance,portability \ + --suppress=missingIncludeSystem \ + --inline-suppr \ + -I include \ + -I src \ + --error-exitcode=1 \ + src/ include/ example/ + build-linux: - name: Build (Linux) + name: Build and test (Linux) + needs: [style, static-analysis] runs-on: ubuntu-latest steps: - name: Checkout @@ -20,7 +67,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev clang-format + sudo apt install -y gcc make libcjson-dev zlib1g-dev - name: Build binary run: make lin @@ -34,11 +81,69 @@ jobs: - name: Run tests run: make test - - name: Run ASan and UBSan tests + sanitizers: + name: ASan, UBSan and TSan + needs: build-linux + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y gcc make libcjson-dev zlib1g-dev + + - name: Run ASan and UBSan run: make test-sanitize + - name: Run ThreadSanitizer + run: make test-tsan + + fuzz-smoke: + name: HTTP parser fuzz smoke + needs: build-linux + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y clang make libcjson-dev zlib1g-dev + + - name: Run fuzz smoke test + run: make test-fuzz + + package-smoke: + name: Package validation + needs: build-linux + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y gcc make libcjson-dev zlib1g-dev + + - name: Build release archive + run: make lin-lib + + - name: Validate archive contents + shell: bash + run: | + set -euo pipefail + test -f libchttpx-dev.tar.gz + tar -tzf libchttpx-dev.tar.gz > /tmp/libchttpx-package-files.txt + grep -q '^libchttpx-dev/include/libchttpx.h$' /tmp/libchttpx-package-files.txt + grep -q '^libchttpx-dev/libchttpx.so$' /tmp/libchttpx-package-files.txt + build-linux-tls: name: Build + HTTPS integration (Linux, OpenSSL) + needs: style runs-on: ubuntu-latest steps: - name: Checkout @@ -60,6 +165,7 @@ jobs: build-linux-compression: name: Response compression integration (Linux, zlib) + needs: style runs-on: ubuntu-latest steps: - name: Checkout @@ -86,6 +192,7 @@ jobs: build-windows: name: Build (Windows) + needs: style runs-on: windows-latest defaults: run: @@ -109,24 +216,3 @@ jobs: - name: Run tests run: mingw32-make test-win SHELL=cmd - - static-analysis: - name: Static analysis (cppcheck) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install cppcheck - run: sudo apt update && sudo apt install -y cppcheck - - - name: Run cppcheck - run: | - cppcheck \ - --enable=warning,performance,portability \ - --suppress=missingIncludeSystem \ - --inline-suppr \ - -I include \ - -I src \ - --error-exitcode=1 \ - src/ include/ example/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e8bfdb7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing to libchttpx + +Thank you for contributing to libchttpx. Changes to the HTTP parser, network runtime, TLS, memory ownership, public API, and concurrency model are treated as security- or stability-sensitive. + +## Pull request baseline + +A pull request must not be merged when it: + +- adds or changes public API without tests and documentation; +- changes ownership or lifetime rules without documenting who owns the resource and when it becomes invalid; +- changes public structure layout or function signatures without an API/ABI compatibility note; +- introduces blocking socket I/O into the application worker pool; +- adds unbounded queues, buffers, or client-controlled allocations; +- ignores a system or library error that can affect correctness; +- adds library-side `exit()` or `abort()`; +- introduces mutable global state without an explicit synchronization and ownership model; +- mixes unrelated refactoring with a feature or bug fix; +- changes parser/network/TLS behavior without negative-path tests. + +## Runtime model + +The server runtime owns sockets and network readiness. Application workers execute middleware, routing, handlers, validation, and application code only. + +The worker pool has a fixed size of 32 threads. It is intentionally not exposed as a public configuration option. Work is submitted through a bounded queue and overload must fail predictably instead of growing memory without a limit. + +Routes, middleware, and server configuration are expected to be registered before the server starts. After startup they are treated as immutable unless an API explicitly documents otherwise. + +## Public naming + +Public functions and public result values use the `cHTTPX_` prefix. + +Canonical result values are: + +`cHTTPX_OK`, `cHTTPX_ERR_MEMORY`, `cHTTPX_ERR_SOCKET`, `cHTTPX_ERR_BIND`, `cHTTPX_ERR_LISTEN`, `cHTTPX_ERR_INVALID_ARGUMENT`, `cHTTPX_ERR_LIMIT`, `cHTTPX_ERR_IO`, `cHTTPX_ERR_NOT_FOUND`, `cHTTPX_ERR_PROTOCOL`, `cHTTPX_ERR_STATE`, `cHTTPX_ERR_UNAVAILABLE`, `cHTTPX_ERR_TIMEOUT`, `cHTTPX_ERR_TLS`, and `cHTTPX_ERR_COMPRESSION`. + +Legacy `CHTTPX_*` result names remain compatibility aliases for existing applications. New code and documentation must use `cHTTPX_*`. + +## Ownership and cleanup + +Every public pointer-bearing API must document whether a pointer is borrowed or owned, who frees it, and how long it remains valid. + +Request-scoped allocations and deferred cleanups are released when the request is cleaned up unless explicitly detached. Response helpers that allocate response body data transfer ownership to the response object, which must be released with `cHTTPX_ResponseCleanup()`. + +Cleanup functions must be safe after partial initialization when the API documents that cleanup is allowed. + +## Error handling + +Functions returning `chttpx_error_t` use `cHTTPX_OK` on success and a negative `cHTTPX_ERR_*` value on failure. + +Pointer-returning functions use `NULL` for failure unless the API explicitly documents another contract. Avoid logging the same error on multiple internal layers; the layer with enough context to make the message useful should log it. + +## Formatting + +The repository uses `.clang-format`. Keep logical sections readable with blank lines between unrelated operations and between definition blocks. Do not compress multiple independent operations into visually dense blocks. + +Do not wrap function declarations or calls merely to reduce line length when the existing style keeps them readable. + +## Required checks + +Changes must pass: + +- formatting check; +- Linux build and unit/integration tests; +- Windows build/tests when the change is cross-platform; +- ASan/UBSan; +- TSan for concurrency-sensitive code; +- parser fuzz smoke tests; +- static analysis. + +Compiler warnings are treated as defects. Library code is built with `-Wall -Wextra -Wpedantic` where supported. + +## Parser and network changes + +Parser inputs are untrusted. Tests should cover malformed framing, duplicate/conflicting Content-Length, Transfer-Encoding conflicts, oversized headers/body, embedded control bytes, truncated input, partial I/O, timeout, disconnect, and chunked edge cases. + +Fuzz targets are part of the normal maintenance workflow and should be extended when new parsing surfaces are added. + +## Versioning + +libchttpx follows Semantic Versioning: + +- backward-compatible feature: MINOR; +- backward-compatible fix: PATCH; +- incompatible public API/ABI change: MAJOR. + +Use conventional PR titles so release automation can select the correct bump. + +## Performance claims + +Any published performance claim must include CPU, RAM, OS, compiler and flags, concurrency, payload size, TLS mode, keep-alive mode, requests/second, p50/p95/p99 latency, CPU utilization, and memory-per-connection where applicable. diff --git a/Makefile b/Makefile index 217d8fa..189c517 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ RELEASE_DIR = libchttpx-dev TAR = $(RELEASE_DIR).tar.gz CC = gcc -CFLAGS = -Wall -Wextra -O2 -Iinclude -Isrc +CFLAGS = -Wall -Wextra -Wpedantic -O2 -Iinclude -Isrc # Native TLS is opt-in so plain HTTP builds keep zero OpenSSL dependency. TLS ?= 0 @@ -19,6 +19,7 @@ endif CFLAGS += $(TLS_CFLAGS) TARGET_DLL = libchttpx.dll CLANG_FORMAT = clang-format +FUZZ_CC ?= clang OBJDIR = .out BINDIR = .build @@ -35,6 +36,8 @@ TEST_TARGET = $(BINDIR)/test_core TEST_SERVER_TARGET = $(BINDIR)/test_server TEST_SANITIZE_TARGET = $(BINDIR)/test_core_sanitize TEST_SERVER_SANITIZE_TARGET = $(BINDIR)/test_server_sanitize +TEST_TSAN_TARGET = $(BINDIR)/test_server_tsan +FUZZ_HEADERS_TARGET = $(BINDIR)/fuzz_headers TEST_TLS_TARGET = $(BINDIR)/test_tls TEST_COMPRESSION_TARGET = $(BINDIR)/test_compression TEST_METRICS_TARGET = $(BINDIR)/test_metrics @@ -45,6 +48,7 @@ EXAMPLE_NAMES = basic multiple_servers local_call remote_call middleware json up EXAMPLE_TARGETS = $(addprefix $(BINDIR)/example-,$(EXAMPLE_NAMES)) LIN_SRCS = $(wildcard src/*.c) +FORMAT_SRCS = $(wildcard src/*.c src/*.h include/*.h example/*.c tests/*.c) WIN_SRCS = $(wildcard src/*.c) lib/cjson/cJSON.c LIN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(LIN_SRCS)) @@ -154,6 +158,16 @@ test-sanitize: $(CC) $(CFLAGS) -std=gnu11 -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined tests/test_server.c $(LIN_SRCS) -o $(TEST_SERVER_SANITIZE_TARGET) $(LIN_LDFLAGS) -pthread ASAN_OPTIONS=detect_leaks=1 $(TEST_SERVER_SANITIZE_TARGET) +test-tsan: + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -std=gnu11 -O1 -g -fno-omit-frame-pointer -fsanitize=thread tests/test_server.c $(LIN_SRCS) -o $(TEST_TSAN_TARGET) $(LIN_LDFLAGS) -pthread + TSAN_OPTIONS=halt_on_error=1 $(TEST_TSAN_TARGET) + +test-fuzz: + @mkdir -p $(BINDIR) + $(FUZZ_CC) $(CFLAGS) -std=gnu11 -O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer,address,undefined tests/fuzz_headers.c $(LIN_SRCS) -o $(FUZZ_HEADERS_TARGET) $(LIN_LDFLAGS) -pthread + $(FUZZ_HEADERS_TARGET) -runs=2000 -max_len=32768 + test-tls: @mkdir -p $(BINDIR)/tls openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ @@ -221,8 +235,12 @@ win-run: win # - lin-format: - @echo ">> Formatting clang source files" - $(CLANG_FORMAT) -i $(LIN_SRCS) + @echo ">> Formatting C source and header files" + $(CLANG_FORMAT) -i $(FORMAT_SRCS) + +format-check: + @echo ">> Checking clang-format" + $(CLANG_FORMAT) --dry-run --Werror $(FORMAT_SRCS) # WINdows format # - diff --git a/docs/README.md b/docs/README.md index cfbb8af..ace7ea3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,3 +39,8 @@ Examples assume: ``` unless a module explicitly states otherwise. + +## Maintainer documentation + +- [Maintainer architecture baseline](maintainers/architecture.md) — runtime ownership, worker model, API/ABI policy, error model, CI gates, and benchmark protocol. +- Repository contribution and merge requirements are defined in [`CONTRIBUTING.md`](../CONTRIBUTING.md). diff --git a/docs/app/README.md b/docs/app/README.md index aaaae9c..0694d29 100644 --- a/docs/app/README.md +++ b/docs/app/README.md @@ -7,7 +7,7 @@ ```c chttpx_app_t app; -if (cHTTPX_AppInit(&app) != CHTTPX_OK) +if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; ``` @@ -40,7 +40,7 @@ Names must be unique across local and remote registrations. ## Start, wait, run ```c -if (cHTTPX_AppStart(&app) != CHTTPX_OK) +if (cHTTPX_AppStart(&app) != cHTTPX_OK) { cHTTPX_AppShutdown(&app); return 1; @@ -117,16 +117,16 @@ Connect/send/receive use a fixed 30-second timeout. | Result | Meaning | | --- | --- | -| `CHTTPX_OK` | valid HTTP response received, including HTTP 4xx/5xx | -| `CHTTPX_ERR_UNAVAILABLE` | DNS/connect failure or connection lost before a valid response | -| `CHTTPX_ERR_TIMEOUT` | connect/send/receive exceeded 30 seconds | -| `CHTTPX_ERR_TLS` | TLS handshake, certificate verification, encrypted read/write failure | -| `CHTTPX_ERR_PROTOCOL` | peer returned invalid HTTP | +| `cHTTPX_OK` | valid HTTP response received, including HTTP 4xx/5xx | +| `cHTTPX_ERR_UNAVAILABLE` | DNS/connect failure or connection lost before a valid response | +| `cHTTPX_ERR_TIMEOUT` | connect/send/receive exceeded 30 seconds | +| `cHTTPX_ERR_TLS` | TLS handshake, certificate verification, encrypted read/write failure | +| `cHTTPX_ERR_PROTOCOL` | peer returned invalid HTTP | A 400/403/500 response means the server responded successfully at the transport layer: ```c -if (result == CHTTPX_OK) +if (result == cHTTPX_OK) { if (res->status >= 400) { @@ -150,7 +150,7 @@ static void create_payment( res ); - if (result == CHTTPX_ERR_UNAVAILABLE) + if (result == cHTTPX_ERR_UNAVAILABLE) { *res = cHTTPX_ResError( cHTTPX_StatusServiceUnavailable, @@ -159,7 +159,7 @@ static void create_payment( return; } - if (result == CHTTPX_ERR_TIMEOUT) + if (result == cHTTPX_ERR_TIMEOUT) { *res = cHTTPX_ResError( cHTTPX_StatusGatewayTimeout, @@ -168,7 +168,7 @@ static void create_payment( return; } - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) { *res = cHTTPX_ResError( cHTTPX_StatusBadGateway, diff --git a/docs/app/README_RU.md b/docs/app/README_RU.md index be390bf..c795ba4 100644 --- a/docs/app/README_RU.md +++ b/docs/app/README_RU.md @@ -7,7 +7,7 @@ ```c chttpx_app_t app; -if (cHTTPX_AppInit(&app) != CHTTPX_OK) +if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; ``` @@ -36,7 +36,7 @@ chttpx_serv_t* admin_server = ## Запуск ```c -if (cHTTPX_AppStart(&app) != CHTTPX_OK) +if (cHTTPX_AppStart(&app) != cHTTPX_OK) { cHTTPX_AppShutdown(&app); return 1; @@ -114,16 +114,16 @@ Timeout через `CallEx` менять нельзя. | Результат | Значение | | --- | --- | -| `CHTTPX_OK` | получен корректный HTTP response, даже если status 400/403/500 | -| `CHTTPX_ERR_UNAVAILABLE` | DNS/connect failed или connection оборвался до HTTP response | -| `CHTTPX_ERR_TIMEOUT` | connect/send/receive превысил 30 секунд | -| `CHTTPX_ERR_TLS` | ошибка TLS handshake, certificate verification или encrypted read/write | -| `CHTTPX_ERR_PROTOCOL` | remote сторона вернула некорректный HTTP response | +| `cHTTPX_OK` | получен корректный HTTP response, даже если status 400/403/500 | +| `cHTTPX_ERR_UNAVAILABLE` | DNS/connect failed или connection оборвался до HTTP response | +| `cHTTPX_ERR_TIMEOUT` | connect/send/receive превысил 30 секунд | +| `cHTTPX_ERR_TLS` | ошибка TLS handshake, certificate verification или encrypted read/write | +| `cHTTPX_ERR_PROTOCOL` | remote сторона вернула некорректный HTTP response | HTTP 500 — не transport error: ```c -if (result == CHTTPX_OK && res->status == 500) +if (result == cHTTPX_OK && res->status == 500) { /* server жив, но его handler вернул ошибку */ } @@ -144,7 +144,7 @@ static void create_payment( res ); - if (result == CHTTPX_ERR_UNAVAILABLE) + if (result == cHTTPX_ERR_UNAVAILABLE) { *res = cHTTPX_ResError( cHTTPX_StatusServiceUnavailable, @@ -153,7 +153,7 @@ static void create_payment( return; } - if (result == CHTTPX_ERR_TIMEOUT) + if (result == cHTTPX_ERR_TIMEOUT) { *res = cHTTPX_ResError( cHTTPX_StatusGatewayTimeout, @@ -162,7 +162,7 @@ static void create_payment( return; } - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) { *res = cHTTPX_ResError( cHTTPX_StatusBadGateway, diff --git a/docs/compression/README.md b/docs/compression/README.md index f400bcd..9d0036a 100644 --- a/docs/compression/README.md +++ b/docs/compression/README.md @@ -30,7 +30,7 @@ compression.min_size = 1024; compression.level = 5; int result = cHTTPX_CompressionUse(server, &compression); -if (result != CHTTPX_OK) { +if (result != cHTTPX_OK) { /* handle configuration / provider error */ } ``` @@ -146,7 +146,7 @@ static int encode_zstd( void *user_data) { /* allocate *output and encode the complete input */ - return CHTTPX_OK; + return cHTTPX_OK; } chttpx_compression_provider_t providers[] = { @@ -168,7 +168,7 @@ Providers are evaluated using the client's quality values. When qualities tie, p ## Errors and logging -Invalid configuration returns `CHTTPX_ERR_INVALID_ARGUMENT`; allocation failures return `CHTTPX_ERR_MEMORY`; gzip/provider failures use `CHTTPX_ERR_COMPRESSION` internally. +Invalid configuration returns `cHTTPX_ERR_INVALID_ARGUMENT`; allocation failures return `cHTTPX_ERR_MEMORY`; gzip/provider failures use `cHTTPX_ERR_COMPRESSION` internally. If encoding fails and identity is acceptable, libchttpx logs a warning and sends the original response. If identity is forbidden, it sends an empty `500 Internal Server Error` rather than silently violating `Accept-Encoding`. diff --git a/docs/compression/README_RU.md b/docs/compression/README_RU.md index 7d84056..71d69e7 100644 --- a/docs/compression/README_RU.md +++ b/docs/compression/README_RU.md @@ -30,7 +30,7 @@ compression.min_size = 1024; compression.level = 5; int result = cHTTPX_CompressionUse(server, &compression); -if (result != CHTTPX_OK) { +if (result != cHTTPX_OK) { /* обработка ошибки */ } ``` @@ -146,7 +146,7 @@ static int encode_zstd( void *user_data) { /* выделить *output и закодировать input */ - return CHTTPX_OK; + return cHTTPX_OK; } chttpx_compression_provider_t providers[] = { @@ -168,7 +168,7 @@ Provider выбирается по quality клиента. При одинако ## Ошибки и logging -Некорректная конфигурация возвращает `CHTTPX_ERR_INVALID_ARGUMENT`, ошибка памяти — `CHTTPX_ERR_MEMORY`, а ошибка compression provider — `CHTTPX_ERR_COMPRESSION` внутри middleware. +Некорректная конфигурация возвращает `cHTTPX_ERR_INVALID_ARGUMENT`, ошибка памяти — `cHTTPX_ERR_MEMORY`, а ошибка compression provider — `cHTTPX_ERR_COMPRESSION` внутри middleware. Если provider не смог сжать body, но клиент принимает identity, libchttpx пишет warning в logger и отправляет исходный ответ. Если identity запрещён, возвращается пустой `500 Internal Server Error`, а не ответ с неподдерживаемым encoding. diff --git a/docs/json/README.md b/docs/json/README.md index d63dd4e..2fc5840 100644 --- a/docs/json/README.md +++ b/docs/json/README.md @@ -38,7 +38,7 @@ static void create_user( true, 3, 254, - CHTTPX_TRIM | CHTTPX_LOWERCASE, + cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL ), cHTTPX_StringField( @@ -47,7 +47,7 @@ static void create_user( true, 3, 32, - CHTTPX_TRIM, + cHTTPX_TRIM, validate_username ), }; @@ -70,9 +70,9 @@ On failure, `BindJSON` returns 0 and builds a safe HTTP 400 response. ## Normalizers -- `CHTTPX_TRIM` -- `CHTTPX_LOWERCASE` -- `CHTTPX_UPPERCASE` +- `cHTTPX_TRIM` +- `cHTTPX_LOWERCASE` +- `cHTTPX_UPPERCASE` They can be OR-combined. diff --git a/docs/json/README_RU.md b/docs/json/README_RU.md index e38791a..338bf90 100644 --- a/docs/json/README_RU.md +++ b/docs/json/README_RU.md @@ -24,7 +24,7 @@ static void create_user( true, 3, 254, - CHTTPX_TRIM | CHTTPX_LOWERCASE, + cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL ), cHTTPX_StringField( @@ -33,7 +33,7 @@ static void create_user( true, 3, 32, - CHTTPX_TRIM, + cHTTPX_TRIM, NULL ), }; @@ -56,9 +56,9 @@ static void create_user( ## Normalizers -- `CHTTPX_TRIM` -- `CHTTPX_LOWERCASE` -- `CHTTPX_UPPERCASE` +- `cHTTPX_TRIM` +- `cHTTPX_LOWERCASE` +- `cHTTPX_UPPERCASE` Можно комбинировать через OR. diff --git a/docs/logging/README.md b/docs/logging/README.md index 5fdcf86..d5dbdd0 100644 --- a/docs/logging/README.md +++ b/docs/logging/README.md @@ -30,17 +30,17 @@ cHTTPX_SetLogger( server, logger, NULL, - CHTTPX_LOG_INFO + cHTTPX_LOG_INFO ); ``` Levels: -- `CHTTPX_LOG_DEBUG` -- `CHTTPX_LOG_INFO` -- `CHTTPX_LOG_WARN` -- `CHTTPX_LOG_ERROR` -- `CHTTPX_LOG_OFF` +- `cHTTPX_LOG_DEBUG` +- `cHTTPX_LOG_INFO` +- `cHTTPX_LOG_WARN` +- `cHTTPX_LOG_ERROR` +- `cHTTPX_LOG_OFF` `user_data` is passed back to the callback and can point to an application logging sink. @@ -59,14 +59,14 @@ cHTTPX_SetLogger( public_server, public_logger, public_sink, - CHTTPX_LOG_INFO + cHTTPX_LOG_INFO ); cHTTPX_SetLogger( admin_server, audit_logger, audit_sink, - CHTTPX_LOG_DEBUG + cHTTPX_LOG_DEBUG ); ``` diff --git a/docs/logging/README_RU.md b/docs/logging/README_RU.md index 9696519..bc5f1c8 100644 --- a/docs/logging/README_RU.md +++ b/docs/logging/README_RU.md @@ -30,17 +30,17 @@ cHTTPX_SetLogger( server, logger, NULL, - CHTTPX_LOG_INFO + cHTTPX_LOG_INFO ); ``` Levels: -- `CHTTPX_LOG_DEBUG` -- `CHTTPX_LOG_INFO` -- `CHTTPX_LOG_WARN` -- `CHTTPX_LOG_ERROR` -- `CHTTPX_LOG_OFF` +- `cHTTPX_LOG_DEBUG` +- `cHTTPX_LOG_INFO` +- `cHTTPX_LOG_WARN` +- `cHTTPX_LOG_ERROR` +- `cHTTPX_LOG_OFF` `user_data` можно использовать для передачи собственного logger/sink. @@ -57,14 +57,14 @@ cHTTPX_SetLogger( public_server, public_logger, public_sink, - CHTTPX_LOG_INFO + cHTTPX_LOG_INFO ); cHTTPX_SetLogger( admin_server, audit_logger, audit_sink, - CHTTPX_LOG_DEBUG + cHTTPX_LOG_DEBUG ); ``` diff --git a/docs/maintainers/architecture.md b/docs/maintainers/architecture.md new file mode 100644 index 0000000..2666439 --- /dev/null +++ b/docs/maintainers/architecture.md @@ -0,0 +1,129 @@ +# Maintainer architecture baseline + +This document defines the architecture and maintenance constraints for libchttpx. + +## Server responsibilities + +`cHTTPX_serv.c` owns public server configuration, lifecycle, route registration, and shutdown coordination. + +`cHTTPX_runtime.c` owns connection lifecycle and the state machine. The runtime never exposes the event backend or scheduler through the public synchronous handler API. + +`cHTTPX_event.c` owns readiness notification and non-blocking socket registration. + +`cHTTPX_worker.c` owns the bounded application job queue and the fixed 32-thread worker pool. + +The intended flow is: + +```text +listener / event loop + | + +-- non-blocking connection I/O + | + +-- HTTP framing + | + v + bounded job queue + | + v + 32 application workers + | + v + completed response queue + | + v + event loop + | + v + non-blocking socket write +``` + +Socket read/write ownership remains in the runtime/event loop. Workers execute request processing and application code and return completed response buffers. + +## Connection state machine + +Connections move through explicit states: + +- TLS handshake; +- reading headers; +- reading body; +- processing; +- writing; +- closing. + +A connection is removed from readiness polling while application code is processing it. Completed responses are handed back to the event loop before socket writes resume. + +## Backpressure + +The application queue is bounded by the server connection capacity. Submitting to a full or stopping pool is rejected instead of allocating an unbounded linked list or growing queue. + +Runtime metrics expose queue depth, active workers, rejected jobs, completed jobs, and total queue wait time. + +Other request limits remain enforced independently, including maximum clients, headers, body, and upload size. + +## Ownership and thread safety + +The event loop owns live connection objects except while a connection is executing inside the worker pool. The worker returns ownership through the completion queue. + +Routes, middleware, and server configuration are configured before startup and treated as immutable while the server is running. + +Metrics and completion queues use explicit synchronization. Runtime stop state is atomic. + +Public request-owned memory is valid until request cleanup unless it is detached. Public response-owned memory is released by `cHTTPX_ResponseCleanup()`. + +## Public API and ABI policy + +Backward-compatible additions are allowed in MINOR releases. Breaking signature, semantic, or public layout changes require a MAJOR release. + +Opaque/internal state stays under `src/` and must not be promoted to the public header without a compatibility reason. + +Canonical public result names use `cHTTPX_*`. Legacy `CHTTPX_*` result macros are compatibility aliases and may be disabled with `CHTTPX_DISABLE_LEGACY_ERROR_NAMES`. + +## Error model + +`chttpx_error_t` APIs return `cHTTPX_OK` or a negative `cHTTPX_ERR_*` value. + +Pointer APIs use `NULL` for failure. APIs that intentionally use another convention must document it. + +The library should log an error once at the layer that has actionable context and still return the error to the application when the application needs to decide what to do. + +## Security boundary + +The HTTP parser and framing code process attacker-controlled bytes. Changes must be tested with malformed, fragmented, oversized, conflicting, truncated, and ambiguous input. + +The fuzz smoke target continuously exercises header parsing. Parser regression tests remain the source for exact expected behavior around framing rules. + +## CI merge gates + +The expected order is: + +```text +format/static analysis + -> build matrix + -> unit/integration tests + -> ASan/UBSan/TSan + -> parser fuzz smoke +``` + +A sanitizer or fuzz failure blocks merge even when normal unit tests pass. + +## Documentation source of truth + +Module documentation lives under `docs/`. The website should consume or link to that content rather than maintaining separate hand-written copies of the same API description. + +## Benchmark protocol + +Performance comparisons must record: + +- CPU and RAM; +- operating system; +- compiler and flags; +- connection concurrency; +- request/response payload; +- HTTP/TLS mode; +- keep-alive mode; +- requests per second; +- p50/p95/p99 latency; +- CPU utilization; +- memory per connection. + +Benchmarks are tracked separately from correctness CI because noisy timing variance must not block unrelated pull requests. diff --git a/docs/metrics/README.md b/docs/metrics/README.md index c9e4403..fa437a0 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -23,7 +23,7 @@ The configuration must be set before the server is created. ```c chttpx_metrics_t metrics; -if (cHTTPX_ServerMetrics(server, &metrics) == CHTTPX_OK) { +if (cHTTPX_ServerMetrics(server, &metrics) == cHTTPX_OK) { printf("requests: %llu\n", (unsigned long long)metrics.requests_total); printf("in flight: %llu\n", @@ -42,6 +42,10 @@ The snapshot includes: - parser, timeout and rate-limit failure counters; - request-duration count, sum and cumulative histogram buckets. +Worker-runtime metrics are available through `cHTTPX_ServerRuntimeMetrics()`. They include the bounded application queue depth, active workers, rejected jobs, completed jobs, and total queue-wait time. The worker pool is fixed at 32 threads and is intentionally not a public configuration knob. + +The Prometheus exporter also publishes these values as `libchttpx_worker_queue_depth`, `libchttpx_workers_active`, `libchttpx_worker_jobs_rejected_total`, `libchttpx_worker_jobs_completed_total`, and `libchttpx_worker_queue_wait_seconds_total`. + The duration buckets are: ```text @@ -65,7 +69,7 @@ chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); cHTTPX_Get(&router, "/health", health_handler); -if (cHTTPX_MetricsRoute(&router, "/metrics") != CHTTPX_OK) { +if (cHTTPX_MetricsRoute(&router, "/metrics") != cHTTPX_OK) { /* metrics are disabled or the route could not be registered */ } ``` diff --git a/docs/metrics/README_RU.md b/docs/metrics/README_RU.md index 8f55fae..97b7131 100644 --- a/docs/metrics/README_RU.md +++ b/docs/metrics/README_RU.md @@ -23,7 +23,7 @@ chttpx_serv_t *server = cHTTPX_AppServer(&app, "api", &config); ```c chttpx_metrics_t metrics; -if (cHTTPX_ServerMetrics(server, &metrics) == CHTTPX_OK) { +if (cHTTPX_ServerMetrics(server, &metrics) == cHTTPX_OK) { printf("requests: %llu\n", (unsigned long long)metrics.requests_total); printf("in flight: %llu\n", @@ -65,7 +65,7 @@ chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); cHTTPX_Get(&router, "/health", health_handler); -if (cHTTPX_MetricsRoute(&router, "/metrics") != CHTTPX_OK) { +if (cHTTPX_MetricsRoute(&router, "/metrics") != cHTTPX_OK) { /* metrics выключены или route не удалось зарегистрировать */ } ``` diff --git a/docs/responses/README.md b/docs/responses/README.md index 5056843..54665d1 100644 --- a/docs/responses/README.md +++ b/docs/responses/README.md @@ -77,8 +77,8 @@ Use `cHTTPX_Status*` and `cHTTPX_CTYPE_*` constants from `http.h`. `cHTTPX_Statu ## Ownership -- `CHTTPX_BODY_OWNED` → response cleanup frees the body; -- `CHTTPX_BODY_BORROWED` → library does not free the body. +- `cHTTPX_BODY_OWNED` → response cleanup frees the body; +- `cHTTPX_BODY_BORROWED` → library does not free the body. Response helpers that allocate content create owned responses. Normal handler code should not manually free helper-produced bodies; the server cleans them after send. diff --git a/docs/responses/README_RU.md b/docs/responses/README_RU.md index 6b1de00..4a5feab 100644 --- a/docs/responses/README_RU.md +++ b/docs/responses/README_RU.md @@ -79,8 +79,8 @@ cHTTPX_HeaderAdd( ## Ownership -- `CHTTPX_BODY_OWNED` — cleanup освобождает body; -- `CHTTPX_BODY_BORROWED` — библиотека body не освобождает. +- `cHTTPX_BODY_OWNED` — cleanup освобождает body; +- `cHTTPX_BODY_BORROWED` — библиотека body не освобождает. Response helpers, которые выделяют память, создают owned response. В обычном handler не освобождайте их body вручную. diff --git a/docs/server/README.md b/docs/server/README.md index 60b3d5c..34becd8 100644 --- a/docs/server/README.md +++ b/docs/server/README.md @@ -17,7 +17,7 @@ Current defaults: | Field | Default | | --- | ---: | | `port` | 8080 | -| `network_mode` | `CHTTPX_NETWORK_DUAL` | +| `network_mode` | `cHTTPX_NETWORK_DUAL` | | `max_clients` | 255 | | `read_timeout_sec` | 30 | | `write_timeout_sec` | 30 | @@ -28,7 +28,7 @@ Current defaults: | `request_id_enabled` | true | | `metrics_enabled` | false | | `default_language` | `"en"` | -| `log_level` | `CHTTPX_LOG_INFO` | +| `log_level` | `cHTTPX_LOG_INFO` | ## Network mode @@ -38,16 +38,16 @@ Servers use dual-stack networking by default. A single IPv6 listener bound to `: chttpx_config_t config = cHTTPX_DefaultConfig(); /* Default: accept IPv4 and IPv6. */ -config.network_mode = CHTTPX_NETWORK_DUAL; +config.network_mode = cHTTPX_NETWORK_DUAL; /* IPv4 only. */ -// config.network_mode = CHTTPX_NETWORK_IPV4; +// config.network_mode = cHTTPX_NETWORK_IPV4; /* IPv6 only. */ -// config.network_mode = CHTTPX_NETWORK_IPV6; +// config.network_mode = cHTTPX_NETWORK_IPV6; ``` -With `CHTTPX_NETWORK_DUAL`, both `http://127.0.0.1:8080` and `http://[::1]:8080` reach the same server. IPv4-mapped peer addresses are normalized before they are exposed through `req->client_ip`, so an IPv4 client is reported as `127.0.0.1` rather than `::ffff:127.0.0.1`. +With `cHTTPX_NETWORK_DUAL`, both `http://127.0.0.1:8080` and `http://[::1]:8080` reach the same server. IPv4-mapped peer addresses are normalized before they are exposed through `req->client_ip`, so an IPv4 client is reported as `127.0.0.1` rather than `::ffff:127.0.0.1`. ## Custom limits @@ -87,26 +87,26 @@ See [Metrics and Prometheus](../metrics/README.md) for snapshots and the `/metri | Code | Meaning | | --- | --- | -| `CHTTPX_OK` | success | -| `CHTTPX_ERR_MEMORY` | allocation failure | -| `CHTTPX_ERR_SOCKET` | socket setup failure | -| `CHTTPX_ERR_BIND` | bind failed | -| `CHTTPX_ERR_LISTEN` | listen failed | -| `CHTTPX_ERR_INVALID_ARGUMENT` | invalid input | -| `CHTTPX_ERR_LIMIT` | limit exceeded | -| `CHTTPX_ERR_IO` | I/O failure | -| `CHTTPX_ERR_NOT_FOUND` | resource/target not found | -| `CHTTPX_ERR_PROTOCOL` | invalid protocol data | -| `CHTTPX_ERR_STATE` | invalid lifecycle state | -| `CHTTPX_ERR_UNAVAILABLE` | remote call unavailable | -| `CHTTPX_ERR_TIMEOUT` | remote call timed out | +| `cHTTPX_OK` | success | +| `cHTTPX_ERR_MEMORY` | allocation failure | +| `cHTTPX_ERR_SOCKET` | socket setup failure | +| `cHTTPX_ERR_BIND` | bind failed | +| `cHTTPX_ERR_LISTEN` | listen failed | +| `cHTTPX_ERR_INVALID_ARGUMENT` | invalid input | +| `cHTTPX_ERR_LIMIT` | limit exceeded | +| `cHTTPX_ERR_IO` | I/O failure | +| `cHTTPX_ERR_NOT_FOUND` | resource/target not found | +| `cHTTPX_ERR_PROTOCOL` | invalid protocol data | +| `cHTTPX_ERR_STATE` | invalid lifecycle state | +| `cHTTPX_ERR_UNAVAILABLE` | remote call unavailable | +| `cHTTPX_ERR_TIMEOUT` | remote call timed out | ## Lifecycle ```c chttpx_app_t app; -if (cHTTPX_AppInit(&app) != CHTTPX_OK) +if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); diff --git a/docs/server/README_RU.md b/docs/server/README_RU.md index f254846..8da1e50 100644 --- a/docs/server/README_RU.md +++ b/docs/server/README_RU.md @@ -17,7 +17,7 @@ chttpx_serv_t* server = | Поле | Default | | --- | ---: | | `port` | 8080 | -| `network_mode` | `CHTTPX_NETWORK_DUAL` | +| `network_mode` | `cHTTPX_NETWORK_DUAL` | | `max_clients` | 255 | | `read_timeout_sec` | 30 | | `write_timeout_sec` | 30 | @@ -28,7 +28,7 @@ chttpx_serv_t* server = | `request_id_enabled` | true | | `metrics_enabled` | false | | `default_language` | `"en"` | -| `log_level` | `CHTTPX_LOG_INFO` | +| `log_level` | `cHTTPX_LOG_INFO` | ## Сетевой режим @@ -38,16 +38,16 @@ chttpx_serv_t* server = chttpx_config_t config = cHTTPX_DefaultConfig(); /* Default: IPv4 + IPv6. */ -config.network_mode = CHTTPX_NETWORK_DUAL; +config.network_mode = cHTTPX_NETWORK_DUAL; /* Только IPv4. */ -// config.network_mode = CHTTPX_NETWORK_IPV4; +// config.network_mode = cHTTPX_NETWORK_IPV4; /* Только IPv6. */ -// config.network_mode = CHTTPX_NETWORK_IPV6; +// config.network_mode = cHTTPX_NETWORK_IPV6; ``` -При `CHTTPX_NETWORK_DUAL` один server доступен и через `http://127.0.0.1:8080`, и через `http://[::1]:8080`. IPv4-mapped адреса нормализуются перед записью в `req->client_ip`, поэтому IPv4 client будет иметь адрес `127.0.0.1`, а не `::ffff:127.0.0.1`. +При `cHTTPX_NETWORK_DUAL` один server доступен и через `http://127.0.0.1:8080`, и через `http://[::1]:8080`. IPv4-mapped адреса нормализуются перед записью в `req->client_ip`, поэтому IPv4 client будет иметь адрес `127.0.0.1`, а не `::ffff:127.0.0.1`. ## Собственные лимиты @@ -82,19 +82,19 @@ Snapshot API и Prometheus endpoint описаны в [Metrics и Prometheus](.. | Код | Значение | | --- | --- | -| `CHTTPX_OK` | успех | -| `CHTTPX_ERR_MEMORY` | allocation failure | -| `CHTTPX_ERR_SOCKET` | socket error | -| `CHTTPX_ERR_BIND` | bind failed | -| `CHTTPX_ERR_LISTEN` | listen failed | -| `CHTTPX_ERR_INVALID_ARGUMENT` | неверные аргументы | -| `CHTTPX_ERR_LIMIT` | превышен limit | -| `CHTTPX_ERR_IO` | I/O error | -| `CHTTPX_ERR_NOT_FOUND` | target/resource не найден | -| `CHTTPX_ERR_PROTOCOL` | protocol error | -| `CHTTPX_ERR_STATE` | неверное состояние lifecycle | -| `CHTTPX_ERR_UNAVAILABLE` | remote server недоступен | -| `CHTTPX_ERR_TIMEOUT` | remote Call timeout | +| `cHTTPX_OK` | успех | +| `cHTTPX_ERR_MEMORY` | allocation failure | +| `cHTTPX_ERR_SOCKET` | socket error | +| `cHTTPX_ERR_BIND` | bind failed | +| `cHTTPX_ERR_LISTEN` | listen failed | +| `cHTTPX_ERR_INVALID_ARGUMENT` | неверные аргументы | +| `cHTTPX_ERR_LIMIT` | превышен limit | +| `cHTTPX_ERR_IO` | I/O error | +| `cHTTPX_ERR_NOT_FOUND` | target/resource не найден | +| `cHTTPX_ERR_PROTOCOL` | protocol error | +| `cHTTPX_ERR_STATE` | неверное состояние lifecycle | +| `cHTTPX_ERR_UNAVAILABLE` | remote server недоступен | +| `cHTTPX_ERR_TIMEOUT` | remote Call timeout | `cHTTPX_AppServer()` возвращает pointer или `NULL`. Библиотека не вызывает `exit()` при обычной ошибке инициализации. @@ -103,7 +103,7 @@ Snapshot API и Prometheus endpoint описаны в [Metrics и Prometheus](.. ```c chttpx_app_t app; -if (cHTTPX_AppInit(&app) != CHTTPX_OK) +if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); diff --git a/docs/site/docs.html b/docs/site/docs.html index 46c8b34..6ed990e 100644 --- a/docs/site/docs.html +++ b/docs/site/docs.html @@ -124,7 +124,7 @@

Your first server

int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -140,7 +140,7 @@

Your first server

int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/docs/site/examples.html b/docs/site/examples.html index e9ac45c..ff45600 100644 --- a/docs/site/examples.html +++ b/docs/site/examples.html @@ -55,7 +55,7 @@

Health endpoint

int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -67,7 +67,7 @@

Health endpoint

int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } Open source example ↗ @@ -117,7 +117,7 @@

Bind and validate JSON

true, 3, 254, - CHTTPX_TRIM | CHTTPX_LOWERCASE, + cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL ) }; diff --git a/docs/tls/README.md b/docs/tls/README.md index e08c9ee..3198d2d 100644 --- a/docs/tls/README.md +++ b/docs/tls/README.md @@ -33,7 +33,7 @@ chttpx_serv_t* server = cHTTPX_AppServer(&app, "https", &config); ``` -Certificate and private-key files are required when `tls.enabled` is true. A plain build returns `CHTTPX_ERR_UNAVAILABLE` when TLS is requested. +Certificate and private-key files are required when `tls.enabled` is true. A plain build returns `cHTTPX_ERR_UNAVAILABLE` when TLS is requested. The runnable example is `example/tls.c`: @@ -100,7 +100,7 @@ config.tls.require_client_cert = true; ## Errors and logging -TLS initialization, handshake, certificate verification, and encrypted I/O failures use the normal libchttpx error path. TLS-specific failures return `CHTTPX_ERR_TLS`; a build without TLS support returns `CHTTPX_ERR_UNAVAILABLE` for HTTPS/TLS configuration. +TLS initialization, handshake, certificate verification, and encrypted I/O failures use the normal libchttpx error path. TLS-specific failures return `cHTTPX_ERR_TLS`; a build without TLS support returns `cHTTPX_ERR_UNAVAILABLE` for HTTPS/TLS configuration. TLS server failures are reported through the configured server logger. Remote TLS failures also use the source server logger. diff --git a/docs/tls/README_RU.md b/docs/tls/README_RU.md index 9d511a6..8b7e11d 100644 --- a/docs/tls/README_RU.md +++ b/docs/tls/README_RU.md @@ -33,7 +33,7 @@ chttpx_serv_t* server = cHTTPX_AppServer(&app, "https", &config); ``` -Когда `tls.enabled = true`, необходимо указать certificate и private key. Если библиотека собрана без TLS, попытка включить TLS вернёт `CHTTPX_ERR_UNAVAILABLE`. +Когда `tls.enabled = true`, необходимо указать certificate и private key. Если библиотека собрана без TLS, попытка включить TLS вернёт `cHTTPX_ERR_UNAVAILABLE`. Готовый пример находится в `example/tls.c`: @@ -93,7 +93,7 @@ config.tls.require_client_cert = true; ## Ошибки и logging -Ошибки инициализации TLS, handshake, certificate verification и encrypted I/O проходят через обычный error path библиотеки. TLS-ошибки возвращают `CHTTPX_ERR_TLS`; если TLS вообще не собран, HTTPS/TLS-конфигурация возвращает `CHTTPX_ERR_UNAVAILABLE`. +Ошибки инициализации TLS, handshake, certificate verification и encrypted I/O проходят через обычный error path библиотеки. TLS-ошибки возвращают `cHTTPX_ERR_TLS`; если TLS вообще не собран, HTTPS/TLS-конфигурация возвращает `cHTTPX_ERR_UNAVAILABLE`. Ошибки TLS server пишутся через настроенный logger. Ошибки исходящих HTTPS-вызовов также проходят через logger исходного server. diff --git a/example/basic.c b/example/basic.c index e01885b..373f042 100644 --- a/example/basic.c +++ b/example/basic.c @@ -9,7 +9,7 @@ static void health(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -28,5 +28,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/compression.c b/example/compression.c index 6b1eb99..2ad3049 100644 --- a/example/compression.c +++ b/example/compression.c @@ -31,7 +31,7 @@ static void health_handler(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -49,7 +49,7 @@ int main(void) compression.level = 5; int compression_result = cHTTPX_CompressionUse(server, &compression); - if (compression_result != CHTTPX_OK) + if (compression_result != cHTTPX_OK) { fprintf(stderr, "compression configuration failed: %d\n", compression_result); cHTTPX_AppShutdown(&app); @@ -66,5 +66,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/json.c b/example/json.c index a7a5801..f7533d3 100644 --- a/example/json.c +++ b/example/json.c @@ -17,7 +17,7 @@ static void create_user(chttpx_request_t* req, chttpx_response_t* res) true, 3, 254, - CHTTPX_TRIM | CHTTPX_LOWERCASE, + cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL ), cHTTPX_StringField( @@ -26,7 +26,7 @@ static void create_user(chttpx_request_t* req, chttpx_response_t* res) true, 1, 64, - CHTTPX_TRIM, + cHTTPX_TRIM, NULL ), }; @@ -44,7 +44,7 @@ static void create_user(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -63,5 +63,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/local_call.c b/example/local_call.c index dfc4c03..5e23ae4 100644 --- a/example/local_call.c +++ b/example/local_call.c @@ -16,14 +16,14 @@ static void public_proxy(chttpx_request_t* req, chttpx_response_t* res) res ); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "internal call failed"); } int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t public_config = cHTTPX_DefaultConfig(); @@ -50,5 +50,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/metrics.c b/example/metrics.c index 2b67ea9..84c55ba 100644 --- a/example/metrics.c +++ b/example/metrics.c @@ -11,7 +11,7 @@ static void hello(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -27,7 +27,7 @@ int main(void) chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); if (!cHTTPX_Get(&router, "/hello", hello) || - cHTTPX_MetricsRoute(&router, "/metrics") != CHTTPX_OK) + cHTTPX_MetricsRoute(&router, "/metrics") != cHTTPX_OK) { cHTTPX_AppShutdown(&app); return 1; @@ -38,5 +38,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/middleware.c b/example/middleware.c index 20ef617..d65a033 100644 --- a/example/middleware.c +++ b/example/middleware.c @@ -20,7 +20,7 @@ static void me(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -42,5 +42,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/multiple_servers.c b/example/multiple_servers.c index b0b51ad..956e1f8 100644 --- a/example/multiple_servers.c +++ b/example/multiple_servers.c @@ -15,7 +15,7 @@ static void admin_health(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t public_config = cHTTPX_DefaultConfig(); @@ -42,5 +42,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/remote_call.c b/example/remote_call.c index 6a325f0..ca0fe59 100644 --- a/example/remote_call.c +++ b/example/remote_call.c @@ -10,26 +10,26 @@ static void create_payment(chttpx_request_t* req, chttpx_response_t* res) res ); - if (result == CHTTPX_ERR_UNAVAILABLE) + if (result == cHTTPX_ERR_UNAVAILABLE) { *res = cHTTPX_ResError(cHTTPX_StatusServiceUnavailable, "payments unavailable"); return; } - if (result == CHTTPX_ERR_TIMEOUT) + if (result == cHTTPX_ERR_TIMEOUT) { *res = cHTTPX_ResError(cHTTPX_StatusGatewayTimeout, "payments timeout"); return; } - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "payments call failed"); } int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -42,7 +42,7 @@ int main(void) return 1; } - if (cHTTPX_AppRemote(&app, "payments", "http://payment-server:8090") != CHTTPX_OK) + if (cHTTPX_AppRemote(&app, "payments", "http://payment-server:8090") != cHTTPX_OK) { cHTTPX_AppShutdown(&app); return 1; @@ -54,5 +54,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/tls.c b/example/tls.c index a1a4845..c5f002f 100644 --- a/example/tls.c +++ b/example/tls.c @@ -17,7 +17,7 @@ int main(int argc, char** argv) } chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -39,5 +39,5 @@ int main(int argc, char** argv) printf("HTTPS server listening on https://localhost:%u\n", server->port); int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/example/upload.c b/example/upload.c index d9ef771..4f936ce 100644 --- a/example/upload.c +++ b/example/upload.c @@ -20,7 +20,7 @@ static void upload_avatar(chttpx_request_t* req, chttpx_response_t* res) int main(void) { chttpx_app_t app; - if (cHTTPX_AppInit(&app) != CHTTPX_OK) + if (cHTTPX_AppInit(&app) != cHTTPX_OK) return 1; chttpx_config_t config = cHTTPX_DefaultConfig(); @@ -50,5 +50,5 @@ int main(void) int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); - return result == CHTTPX_OK ? 0 : 1; + return result == cHTTPX_OK ? 0 : 1; } diff --git a/include/libchttpx.h b/include/libchttpx.h index 7586102..38be397 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -433,12 +433,19 @@ extern "C" typedef enum { - CHTTPX_NORMALIZE_NONE = 0, - CHTTPX_TRIM = 1 << 0, - CHTTPX_LOWERCASE = 1 << 1, - CHTTPX_UPPERCASE = 1 << 2 + cHTTPX_NORMALIZE_NONE = 0, + cHTTPX_TRIM = 1 << 0, + cHTTPX_LOWERCASE = 1 << 1, + cHTTPX_UPPERCASE = 1 << 2 } chttpx_normalizer_t; +#ifndef CHTTPX_DISABLE_LEGACY_NORMALIZER_NAMES +#define CHTTPX_NORMALIZE_NONE cHTTPX_NORMALIZE_NONE +#define CHTTPX_TRIM cHTTPX_TRIM +#define CHTTPX_LOWERCASE cHTTPX_LOWERCASE +#define CHTTPX_UPPERCASE cHTTPX_UPPERCASE +#endif + typedef struct { const char* name; @@ -742,7 +749,7 @@ extern "C" #define chttpx_validation_string(name, ptr, required, min_length, max_length, validator) \ (chttpx_validation_t) \ { \ - name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, cHTTPX_NORMALIZE_NONE, NULL \ } /** @@ -758,7 +765,7 @@ extern "C" #define chttpx_validation_integer(name, ptr, required) \ (chttpx_validation_t) \ { \ - name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ } /** @@ -774,7 +781,7 @@ extern "C" #define chttpx_validation_boolean(name, ptr, required) \ (chttpx_validation_t) \ { \ - name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ } #define cHTTPX_StringField(name, ptr, required, min_length, max_length, normalizers, validator) \ @@ -817,10 +824,15 @@ extern "C" // RESponse typedef enum { - CHTTPX_BODY_BORROWED = 0, - CHTTPX_BODY_OWNED = 1 + cHTTPX_BODY_BORROWED = 0, + cHTTPX_BODY_OWNED = 1 } chttpx_body_ownership_t; +#ifndef CHTTPX_DISABLE_LEGACY_BODY_OWNERSHIP_NAMES +#define CHTTPX_BODY_BORROWED cHTTPX_BODY_BORROWED +#define CHTTPX_BODY_OWNED cHTTPX_BODY_OWNED +#endif + typedef struct chttpx_response { /* Response status code */ @@ -935,7 +947,7 @@ extern "C" * @param data Buffer * to send. * @param size Buffer size in bytes. - * @return CHTTPX_OK on success, otherwise a negative error code. + * @return cHTTPX_OK on success, otherwise a negative error code. */ int cHTTPX_SendAll(chttpx_socket_t fd, const void* data, size_t size); @@ -1175,41 +1187,75 @@ extern "C" typedef enum { - CHTTPX_OK = 0, - CHTTPX_ERR_MEMORY = -1, - CHTTPX_ERR_SOCKET = -2, - CHTTPX_ERR_BIND = -3, - CHTTPX_ERR_LISTEN = -4, - CHTTPX_ERR_INVALID_ARGUMENT = -5, - CHTTPX_ERR_LIMIT = -6, - CHTTPX_ERR_IO = -7, - CHTTPX_ERR_NOT_FOUND = -8, - CHTTPX_ERR_PROTOCOL = -9, - CHTTPX_ERR_STATE = -10, - CHTTPX_ERR_UNAVAILABLE = -11, - CHTTPX_ERR_TIMEOUT = -12, - CHTTPX_ERR_TLS = -13, - CHTTPX_ERR_COMPRESSION = -14 + cHTTPX_OK = 0, + cHTTPX_ERR_MEMORY = -1, + cHTTPX_ERR_SOCKET = -2, + cHTTPX_ERR_BIND = -3, + cHTTPX_ERR_LISTEN = -4, + cHTTPX_ERR_INVALID_ARGUMENT = -5, + cHTTPX_ERR_LIMIT = -6, + cHTTPX_ERR_IO = -7, + cHTTPX_ERR_NOT_FOUND = -8, + cHTTPX_ERR_PROTOCOL = -9, + cHTTPX_ERR_STATE = -10, + cHTTPX_ERR_UNAVAILABLE = -11, + cHTTPX_ERR_TIMEOUT = -12, + cHTTPX_ERR_TLS = -13, + cHTTPX_ERR_COMPRESSION = -14 } chttpx_error_t; + /* Backward compatibility for the legacy all-uppercase result names. */ +#ifndef CHTTPX_DISABLE_LEGACY_ERROR_NAMES +#define CHTTPX_OK cHTTPX_OK +#define CHTTPX_ERR_MEMORY cHTTPX_ERR_MEMORY +#define CHTTPX_ERR_SOCKET cHTTPX_ERR_SOCKET +#define CHTTPX_ERR_BIND cHTTPX_ERR_BIND +#define CHTTPX_ERR_LISTEN cHTTPX_ERR_LISTEN +#define CHTTPX_ERR_INVALID_ARGUMENT cHTTPX_ERR_INVALID_ARGUMENT +#define CHTTPX_ERR_LIMIT cHTTPX_ERR_LIMIT +#define CHTTPX_ERR_IO cHTTPX_ERR_IO +#define CHTTPX_ERR_NOT_FOUND cHTTPX_ERR_NOT_FOUND +#define CHTTPX_ERR_PROTOCOL cHTTPX_ERR_PROTOCOL +#define CHTTPX_ERR_STATE cHTTPX_ERR_STATE +#define CHTTPX_ERR_UNAVAILABLE cHTTPX_ERR_UNAVAILABLE +#define CHTTPX_ERR_TIMEOUT cHTTPX_ERR_TIMEOUT +#define CHTTPX_ERR_TLS cHTTPX_ERR_TLS +#define CHTTPX_ERR_COMPRESSION cHTTPX_ERR_COMPRESSION +#endif + + typedef enum { - CHTTPX_LOG_DEBUG, - CHTTPX_LOG_INFO, - CHTTPX_LOG_WARN, - CHTTPX_LOG_ERROR, - CHTTPX_LOG_OFF + cHTTPX_LOG_DEBUG, + cHTTPX_LOG_INFO, + cHTTPX_LOG_WARN, + cHTTPX_LOG_ERROR, + cHTTPX_LOG_OFF } chttpx_log_level_t; +#ifndef CHTTPX_DISABLE_LEGACY_SERVER_ENUM_NAMES +#define CHTTPX_LOG_DEBUG cHTTPX_LOG_DEBUG +#define CHTTPX_LOG_INFO cHTTPX_LOG_INFO +#define CHTTPX_LOG_WARN cHTTPX_LOG_WARN +#define CHTTPX_LOG_ERROR cHTTPX_LOG_ERROR +#define CHTTPX_LOG_OFF cHTTPX_LOG_OFF +#endif + typedef void (*chttpx_logger_fn)(chttpx_log_level_t level, const char* request_id, const char* message, void* user_data); typedef enum { - CHTTPX_NETWORK_IPV4 = 0, - CHTTPX_NETWORK_IPV6, - CHTTPX_NETWORK_DUAL + cHTTPX_NETWORK_IPV4 = 0, + cHTTPX_NETWORK_IPV6, + cHTTPX_NETWORK_DUAL } chttpx_network_mode_t; +#ifndef CHTTPX_DISABLE_LEGACY_SERVER_ENUM_NAMES +#define CHTTPX_NETWORK_IPV4 cHTTPX_NETWORK_IPV4 +#define CHTTPX_NETWORK_IPV6 cHTTPX_NETWORK_IPV6 +#define CHTTPX_NETWORK_DUAL cHTTPX_NETWORK_DUAL +#endif + typedef struct { bool enabled; @@ -1459,9 +1505,34 @@ extern "C" uint64_t request_duration_buckets[CHTTPX_METRICS_DURATION_BUCKETS]; } chttpx_metrics_t; + /** + * Snapshot of the bounded application worker pool. + * + * The pool is internal to the server runtime and uses a fixed 32 workers. + * Queue depth and active_workers are gauges; the remaining fields are + * monotonic counters for the lifetime of the server runtime. + */ + typedef struct + { + uint64_t worker_queue_depth; + uint64_t active_workers; + uint64_t rejected_jobs_total; + uint64_t completed_jobs_total; + uint64_t queue_wait_nanoseconds_total; + } chttpx_runtime_metrics_t; + + /** Copy a consistent per-server metrics snapshot. */ int cHTTPX_ServerMetrics(chttpx_serv_t* server, chttpx_metrics_t* metrics); + /** + * Copy a worker-runtime metrics snapshot. + * + * Returns cHTTPX_OK while the server runtime is active, otherwise + * cHTTPX_ERR_UNAVAILABLE. + */ + int cHTTPX_ServerRuntimeMetrics(chttpx_serv_t* server, chttpx_runtime_metrics_t* metrics); + /** Register a Prometheus text exposition route. */ int cHTTPX_MetricsRoute(chttpx_router_t* router, const char* path); @@ -1978,7 +2049,7 @@ extern "C" * codes. * @param count Number of elements in languages. * @param fallback Fallback language code. - * @return CHTTPX_OK on + * @return cHTTPX_OK on * success, otherwise a negative error code. */ int cHTTPX_i18n_languages(struct chttpx_serv* server, const char** languages, size_t count, const char* fallback); diff --git a/src/cHTTPX_app.c b/src/cHTTPX_app.c index 1226d27..877c3da 100644 --- a/src/cHTTPX_app.c +++ b/src/cHTTPX_app.c @@ -82,59 +82,59 @@ static chttpx_app_remote_t* find_remote(chttpx_app_t* app, const char* name) static int ensure_server_capacity(chttpx_app_t* app) { if (app->_servers_count < app->_servers_capacity) - return CHTTPX_OK; + return cHTTPX_OK; size_t new_capacity = app->_servers_capacity ? app->_servers_capacity * 2 : 4; if (new_capacity < app->_servers_capacity || new_capacity > SIZE_MAX / sizeof(chttpx_app_server_t)) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; void* resized = realloc(app->_servers, new_capacity * sizeof(chttpx_app_server_t)); if (!resized) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; app->_servers = resized; memset(app_servers(app) + app->_servers_capacity, 0, (new_capacity - app->_servers_capacity) * sizeof(chttpx_app_server_t)); app->_servers_capacity = new_capacity; - return CHTTPX_OK; + return cHTTPX_OK; } static int ensure_remote_capacity(chttpx_app_t* app) { if (app->_remotes_count < app->_remotes_capacity) - return CHTTPX_OK; + return cHTTPX_OK; size_t new_capacity = app->_remotes_capacity ? app->_remotes_capacity * 2 : 4; if (new_capacity < app->_remotes_capacity || new_capacity > SIZE_MAX / sizeof(chttpx_app_remote_t)) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; void* resized = realloc(app->_remotes, new_capacity * sizeof(chttpx_app_remote_t)); if (!resized) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; app->_remotes = resized; memset(app_remotes(app) + app->_remotes_capacity, 0, (new_capacity - app->_remotes_capacity) * sizeof(chttpx_app_remote_t)); app->_remotes_capacity = new_capacity; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_AppInit(chttpx_app_t* app) { if (!app) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; memset(app, 0, sizeof(*app)); #ifdef CHTTPX_PLATFORM_WINDOWS WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) - return CHTTPX_ERR_SOCKET; + return cHTTPX_ERR_SOCKET; #endif app->_network_initialized = true; app->_initialized = true; - return CHTTPX_OK; + return cHTTPX_OK; } chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttpx_config_t* config) @@ -145,7 +145,7 @@ chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttp if (find_server(app, name) || find_remote(app, name)) return NULL; - if (ensure_server_capacity(app) != CHTTPX_OK) + if (ensure_server_capacity(app) != cHTTPX_OK) return NULL; chttpx_serv_t* server = calloc(1, sizeof(*server)); @@ -153,7 +153,7 @@ chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttp return NULL; int result = _chttpx_server_init(server, app, name, config); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) { free(server); return NULL; @@ -190,21 +190,21 @@ int cHTTPX_AppRemoteEx(chttpx_app_t* app, const char* name, const char* base_url bool http = base_url && strncmp(base_url, "http://", 7) == 0; if (!app || !app->_initialized || app->_started || !name || !*name || (!http && !https)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (https && !_chttpx_tls_available()) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; chttpx_tls_client_config_t selected = tls_config ? *tls_config : cHTTPX_DefaultTLSClientConfig(); if ((selected.client_cert_file && !selected.client_key_file) || (!selected.client_cert_file && selected.client_key_file)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (find_server(app, name) || find_remote(app, name)) - return CHTTPX_ERR_STATE; + return cHTTPX_ERR_STATE; int result = ensure_remote_capacity(app); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) return result; chttpx_app_remote_t* item = &app_remotes(app)[app->_remotes_count]; @@ -224,11 +224,11 @@ int cHTTPX_AppRemoteEx(chttpx_app_t* app, const char* name, const char* base_url free(item->base_url); free_remote_tls_config(&item->tls); memset(item, 0, sizeof(*item)); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } app->_remotes_count++; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_AppRemote(chttpx_app_t* app, const char* name, const char* base_url) @@ -246,7 +246,7 @@ static void* app_listener(void* arg) int cHTTPX_AppStart(chttpx_app_t* app) { if (!app || !app->_initialized || app->_started) - return CHTTPX_ERR_STATE; + return cHTTPX_ERR_STATE; chttpx_app_server_t* items = app_servers(app); @@ -269,20 +269,20 @@ int cHTTPX_AppStart(chttpx_app_t* app) items[j].thread_started = false; } - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } items[i].thread_started = true; } app->_started = true; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_AppWait(chttpx_app_t* app) { if (!app || !app->_initialized || !app->_started) - return CHTTPX_ERR_STATE; + return cHTTPX_ERR_STATE; chttpx_app_server_t* items = app_servers(app); @@ -296,13 +296,13 @@ int cHTTPX_AppWait(chttpx_app_t* app) } app->_started = false; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_AppRun(chttpx_app_t* app) { int result = cHTTPX_AppStart(app); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) return result; return cHTTPX_AppWait(app); @@ -375,27 +375,27 @@ static void free_internal_request(chttpx_request_t* req) static int stabilize_response(chttpx_response_t* res) { - if (!res || !res->body || res->body_size == 0 || res->body_ownership == CHTTPX_BODY_OWNED) - return CHTTPX_OK; + if (!res || !res->body || res->body_size == 0 || res->body_ownership == cHTTPX_BODY_OWNED) + return cHTTPX_OK; unsigned char* copy = malloc(res->body_size); if (!copy) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; memcpy(copy, res->body, res->body_size); res->body = copy; - res->body_ownership = CHTTPX_BODY_OWNED; - return CHTTPX_OK; + res->body_ownership = cHTTPX_BODY_OWNED; + return cHTTPX_OK; } static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const char* method, const char* path, const void* body, size_t body_size, const char* content_type, chttpx_response_t* res) { if (!source || !target || !target->initialized || !method || !path || !res || (body_size && !body)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (__atomic_load_n(&target->current_clients, __ATOMIC_SEQ_CST) >= target->max_clients) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; chttpx_request_t internal = {0}; internal._server = target; @@ -410,7 +410,7 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha if (!internal.method || !internal.path) { free_internal_request(&internal); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } if (body_size) @@ -419,7 +419,7 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha if (!internal.body) { free_internal_request(&internal); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } memcpy(internal.body, body, body_size); @@ -453,7 +453,7 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha if (internal._parse_status) { free_internal_request(&internal); - return CHTTPX_ERR_PROTOCOL; + return cHTTPX_ERR_PROTOCOL; } } @@ -461,14 +461,14 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha if (internal._parse_status) { free_internal_request(&internal); - return CHTTPX_ERR_PROTOCOL; + return cHTTPX_ERR_PROTOCOL; } __atomic_fetch_add(&target->current_clients, 1, __ATOMIC_SEQ_CST); int result = _chttpx_dispatch(target, &internal, res); __atomic_fetch_sub(&target->current_clients, 1, __ATOMIC_SEQ_CST); - if (result == CHTTPX_OK) + if (result == cHTTPX_OK) result = stabilize_response(res); free_internal_request(&internal); @@ -592,18 +592,18 @@ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) { #ifdef CHTTPX_PLATFORM_WINDOWS u_long mode = enabled ? 1UL : 0UL; - return ioctlsocket(socket_fd, FIONBIO, &mode) == 0 ? CHTTPX_OK : CHTTPX_ERR_UNAVAILABLE; + return ioctlsocket(socket_fd, FIONBIO, &mode) == 0 ? cHTTPX_OK : cHTTPX_ERR_UNAVAILABLE; #else int flags = fcntl(socket_fd, F_GETFL, 0); if (flags < 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; if (enabled) flags |= O_NONBLOCK; else flags &= ~O_NONBLOCK; - return fcntl(socket_fd, F_SETFL, flags) == 0 ? CHTTPX_OK : CHTTPX_ERR_UNAVAILABLE; + return fcntl(socket_fd, F_SETFL, flags) == 0 ? cHTTPX_OK : cHTTPX_ERR_UNAVAILABLE; #endif } @@ -612,17 +612,17 @@ static int socket_set_call_timeouts(chttpx_socket_t socket_fd) #ifdef CHTTPX_PLATFORM_WINDOWS DWORD timeout_ms = CHTTPX_CALL_TIMEOUT_SEC * 1000U; if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; if (setsockopt(socket_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else struct timeval timeout = {.tv_sec = CHTTPX_CALL_TIMEOUT_SEC, .tv_usec = 0}; if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; if (setsockopt(socket_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #endif - return CHTTPX_OK; + return cHTTPX_OK; } static int wait_for_connect(chttpx_socket_t socket_fd) @@ -643,10 +643,10 @@ static int wait_for_connect(chttpx_socket_t socket_fd) #endif if (ready == 0) - return CHTTPX_ERR_TIMEOUT; + return cHTTPX_ERR_TIMEOUT; if (ready < 0) - return socket_error_is_timeout(socket_last_error()) ? CHTTPX_ERR_TIMEOUT : CHTTPX_ERR_UNAVAILABLE; + return socket_error_is_timeout(socket_last_error()) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; int socket_error = 0; #ifdef CHTTPX_PLATFORM_WINDOWS @@ -662,18 +662,18 @@ static int wait_for_connect(chttpx_socket_t socket_fd) &socket_error, #endif &error_size) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; if (socket_error == 0) - return CHTTPX_OK; + return cHTTPX_OK; - return socket_error_is_timeout(socket_error) ? CHTTPX_ERR_TIMEOUT : CHTTPX_ERR_UNAVAILABLE; + return socket_error_is_timeout(socket_error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; } static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* connected) { if (!remote || !connected) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; #ifdef CHTTPX_PLATFORM_WINDOWS *connected = INVALID_SOCKET; @@ -689,9 +689,9 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(remote->host, remote->port, &hints, &result) != 0) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; - int final_result = CHTTPX_ERR_UNAVAILABLE; + int final_result = cHTTPX_ERR_UNAVAILABLE; for (struct addrinfo* current = result; current; current = current->ai_next) { @@ -703,7 +703,7 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co #endif continue; - if (socket_set_nonblocking(socket_fd, true) != CHTTPX_OK) + if (socket_set_nonblocking(socket_fd, true) != cHTTPX_OK) { chttpx_close(socket_fd); continue; @@ -720,25 +720,25 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co } int wait_result = wait_for_connect(socket_fd); - if (wait_result != CHTTPX_OK) + if (wait_result != cHTTPX_OK) { chttpx_close(socket_fd); final_result = wait_result; - if (wait_result == CHTTPX_ERR_TIMEOUT) + if (wait_result == cHTTPX_ERR_TIMEOUT) break; continue; } } - if (socket_set_nonblocking(socket_fd, false) != CHTTPX_OK || - socket_set_call_timeouts(socket_fd) != CHTTPX_OK) + if (socket_set_nonblocking(socket_fd, false) != cHTTPX_OK || + socket_set_call_timeouts(socket_fd) != cHTTPX_OK) { chttpx_close(socket_fd); continue; } *connected = socket_fd; - final_result = CHTTPX_OK; + final_result = cHTTPX_OK; break; } @@ -791,19 +791,19 @@ static const char* find_remote_content_type(char* headers) static void log_remote_tls_error(chttpx_request_t* source, const char* message) { chttpx_serv_t* server = source ? source->_server : NULL; - if (server && server->logger && server->log_level <= CHTTPX_LOG_ERROR) - server->logger(CHTTPX_LOG_ERROR, source->request_id, message, server->logger_data); + if (server && server->logger && server->log_level <= cHTTPX_LOG_ERROR) + server->logger(cHTTPX_LOG_ERROR, source->request_id, message, server->logger_data); } static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* target, const char* method, const char* path, const void* body, size_t body_size, const char* content_type, chttpx_response_t* res) { if (!source || !target) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_remote_url_t remote; if (!parse_remote_url(target->base_url, &remote)) - return CHTTPX_ERR_PROTOCOL; + return cHTTPX_ERR_PROTOCOL; #ifdef CHTTPX_PLATFORM_WINDOWS chttpx_socket_t socket_fd = INVALID_SOCKET; @@ -812,7 +812,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ #endif int result = connect_remote(&remote, &socket_fd); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) return result; void* tls_ctx = NULL; @@ -822,7 +822,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ if (remote.tls) { result = _chttpx_tls_client_connect(socket_fd, remote.host, &target->tls, &tls_ctx, &tls_session); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) { log_remote_tls_error(source, "remote TLS handshake or certificate verification failed"); goto done; @@ -832,7 +832,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ char full_path[CHTTPX_MAX_PATH]; if (snprintf(full_path, sizeof(full_path), "%s%s", remote.base_path, path) >= (int)sizeof(full_path)) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } @@ -842,7 +842,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ : snprintf(host_header, sizeof(host_header), "%s:%s", remote.host, remote.port); if (host_header_size < 0 || (size_t)host_header_size >= sizeof(host_header)) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } @@ -864,7 +864,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ if (header_size < 0 || (size_t)header_size >= sizeof(header)) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } @@ -874,7 +874,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ int added = snprintf(header + used, sizeof(header) - used, "Authorization: %s\r\n", authorization); if (added < 0 || (size_t)added >= sizeof(header) - used) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } used += (size_t)added; @@ -882,7 +882,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ if (used + 2 > sizeof(header)) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } @@ -890,20 +890,20 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ used += 2; int send_result = _chttpx_io_send_all(socket_fd, tls_session, header, used); - if (send_result == CHTTPX_OK && body_size) + if (send_result == cHTTPX_OK && body_size) send_result = _chttpx_io_send_all(socket_fd, tls_session, body, body_size); - if (send_result != CHTTPX_OK) + if (send_result != cHTTPX_OK) { if (remote.tls) { log_remote_tls_error(source, "remote TLS write failed"); - result = CHTTPX_ERR_TLS; + result = cHTTPX_ERR_TLS; } else { int error = socket_last_error(); - result = socket_error_is_timeout(error) ? CHTTPX_ERR_TIMEOUT : CHTTPX_ERR_UNAVAILABLE; + result = socket_error_is_timeout(error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; } goto done; } @@ -916,7 +916,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ response = malloc(capacity + 1); if (!response) { - result = CHTTPX_ERR_MEMORY; + result = cHTTPX_ERR_MEMORY; goto done; } @@ -927,7 +927,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ { if (capacity >= limit) { - result = CHTTPX_ERR_LIMIT; + result = cHTTPX_ERR_LIMIT; goto done; } @@ -938,7 +938,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ char* resized = realloc(response, next + 1); if (!resized) { - result = CHTTPX_ERR_MEMORY; + result = cHTTPX_ERR_MEMORY; goto done; } @@ -952,12 +952,12 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ if (remote.tls) { log_remote_tls_error(source, "remote TLS read failed"); - result = CHTTPX_ERR_TLS; + result = cHTTPX_ERR_TLS; } else { int error = socket_last_error(); - result = socket_error_is_timeout(error) ? CHTTPX_ERR_TIMEOUT : CHTTPX_ERR_UNAVAILABLE; + result = socket_error_is_timeout(error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; } goto done; } @@ -966,7 +966,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ { if (total == 0) { - result = CHTTPX_ERR_UNAVAILABLE; + result = cHTTPX_ERR_UNAVAILABLE; goto done; } break; @@ -980,14 +980,14 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ int status = 0; if (sscanf(response, "HTTP/%*s %d", &status) != 1 || status < 100 || status > 599) { - result = CHTTPX_ERR_PROTOCOL; + result = cHTTPX_ERR_PROTOCOL; goto done; } char* delimiter = chttpx_memmem(response, total, "\r\n\r\n", 4); if (!delimiter) { - result = CHTTPX_ERR_PROTOCOL; + result = cHTTPX_ERR_PROTOCOL; goto done; } @@ -997,7 +997,7 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ const char* response_type = remote_response_content_type(find_remote_content_type(response)); *res = cHTTPX_ResBinary((uint16_t)status, response_type, (const unsigned char*)body_start, response_body_size); - result = res->status ? CHTTPX_OK : CHTTPX_ERR_MEMORY; + result = res->status ? cHTTPX_OK : cHTTPX_ERR_MEMORY; done: free(response); @@ -1011,7 +1011,7 @@ int cHTTPX_CallEx(chttpx_request_t* source, const char* server_name, const char* { if (!source || !source->_server || !source->_server->app || !server_name || !*server_name || !method || !path || !options || !res || (options->body_size && !options->body)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; const void* body = options->body; size_t body_size = options->body_size; @@ -1027,13 +1027,13 @@ int cHTTPX_CallEx(chttpx_request_t* source, const char* server_name, const char* if (remote) return remote_call(source, remote, method, path, body, body_size, content_type, res); - return CHTTPX_ERR_NOT_FOUND; + return cHTTPX_ERR_NOT_FOUND; } int cHTTPX_Call(chttpx_request_t* req, const char* server_name, const char* method, const char* path, chttpx_response_t* res) { if (!req) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_call_options_t options = { .body = req->body, diff --git a/src/cHTTPX_body.c b/src/cHTTPX_body.c index 7984b58..4232dde 100644 --- a/src/cHTTPX_body.c +++ b/src/cHTTPX_body.c @@ -181,7 +181,7 @@ static int chunked_reader_read(chunked_reader_t* reader, unsigned char* output, wanted = INT_MAX; #endif int result = _chttpx_io_recv(reader->client_fd, reader->tls_session, output, wanted); - if (result == CHTTPX_ERR_TLS) + if (result == cHTTPX_ERR_TLS) _chttpx_tls_log_error(reader->server, reader->request_id, "TLS chunked-body read failed"); return result; } @@ -393,7 +393,7 @@ static int spool_multipart_body(chttpx_request_t* req, chttpx_socket_t client_fd wanted = sizeof(chunk); int received = _chttpx_io_recv(client_fd, req->_tls_session, chunk, wanted); - if (received == CHTTPX_ERR_TLS) + if (received == cHTTPX_ERR_TLS) _chttpx_tls_log_error(req->_server, req->request_id, "TLS multipart-body read failed"); if (received <= 0) { @@ -527,7 +527,7 @@ void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buf wanted = INT_MAX; #endif int n = _chttpx_io_recv(client_fd, req->_tls_session, (char*)req->body + total_read, wanted); - if (n == CHTTPX_ERR_TLS) + if (n == cHTTPX_ERR_TLS) _chttpx_tls_log_error(req->_server, req->request_id, "TLS request-body read failed"); if (n < 0) { diff --git a/src/cHTTPX_compression.c b/src/cHTTPX_compression.c index 385b80d..fad3d31 100644 --- a/src/cHTTPX_compression.c +++ b/src/cHTTPX_compression.c @@ -132,11 +132,11 @@ static int valid_encoding_name(const char* encoding) static int duplicate_strings(const char** source, size_t count, char*** destination) { if (!destination || (count > 0 && !source)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; char** copy = count ? calloc(count, sizeof(*copy)) : NULL; if (count && !copy) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; for (size_t i = 0; i < count; i++) { @@ -145,7 +145,7 @@ static int duplicate_strings(const char** source, size_t count, char*** destinat for (size_t j = 0; j < i; j++) free(copy[j]); free(copy); - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; } copy[i] = strdup(source[i]); @@ -154,12 +154,12 @@ static int duplicate_strings(const char** source, size_t count, char*** destinat for (size_t j = 0; j < i; j++) free(copy[j]); free(copy); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } } *destination = copy; - return CHTTPX_OK; + return cHTTPX_OK; } static void free_compression_state(chttpx_compression_state_t* state) @@ -192,23 +192,23 @@ static int gzip_encode_buffer(const unsigned char* input, (void)user_data; if (!input || input_size == 0 || !output || !output_size || level < -1 || level > 9) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (input_size > (size_t)ULONG_MAX) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; z_stream stream; memset(&stream, 0, sizeof(stream)); int zresult = deflateInit2(&stream, level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); if (zresult != Z_OK) - return CHTTPX_ERR_COMPRESSION; + return cHTTPX_ERR_COMPRESSION; uLong bound_value = deflateBound(&stream, (uLong)input_size); if (bound_value == 0 || (uintmax_t)bound_value > (uintmax_t)SIZE_MAX) { deflateEnd(&stream); - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; } size_t capacity = (size_t)bound_value; @@ -216,7 +216,7 @@ static int gzip_encode_buffer(const unsigned char* input, if (!buffer) { deflateEnd(&stream); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } size_t input_offset = 0; @@ -272,12 +272,12 @@ static int gzip_encode_buffer(const unsigned char* input, deflateEnd(&stream); *output = buffer; *output_size = output_offset; - return CHTTPX_OK; + return cHTTPX_OK; compression_error: deflateEnd(&stream); free(buffer); - return CHTTPX_ERR_COMPRESSION; + return cHTTPX_ERR_COMPRESSION; } chttpx_compression_config_t cHTTPX_CompressionDefault(void) @@ -300,7 +300,7 @@ static int copy_providers(const chttpx_compression_provider_t* providers, char*** encodings_out) { if (!providers_out || !encodings_out || (count > 0 && !providers)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_compression_provider_t* provider_copy = count ? calloc(count, sizeof(*provider_copy)) : NULL; char** encoding_copy = count ? calloc(count, sizeof(*encoding_copy)) : NULL; @@ -308,7 +308,7 @@ static int copy_providers(const chttpx_compression_provider_t* providers, { free(provider_copy); free(encoding_copy); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } for (size_t i = 0; i < count; i++) @@ -319,7 +319,7 @@ static int copy_providers(const chttpx_compression_provider_t* providers, free(encoding_copy[j]); free(provider_copy); free(encoding_copy); - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; } encoding_copy[i] = strdup(providers[i].encoding); @@ -329,7 +329,7 @@ static int copy_providers(const chttpx_compression_provider_t* providers, free(encoding_copy[j]); free(provider_copy); free(encoding_copy); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } provider_copy[i] = providers[i]; @@ -338,7 +338,7 @@ static int copy_providers(const chttpx_compression_provider_t* providers, *providers_out = provider_copy; *encodings_out = encoding_copy; - return CHTTPX_OK; + return cHTTPX_OK; } static int prepare_compression_state(const chttpx_compression_config_t* config, chttpx_compression_state_t** state_out) @@ -347,31 +347,31 @@ static int prepare_compression_state(const chttpx_compression_config_t* config, (config->include_types_count > 0 && !config->include_types) || (config->exclude_types_count > 0 && !config->exclude_types) || (config->providers_count > 0 && !config->providers)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (config->providers_count == 0 && (config->level < -1 || config->level > 9)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_compression_state_t* state = calloc(1, sizeof(*state)); if (!state) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; state->config = *config; int result = duplicate_strings(config->include_types, config->include_types_count, &state->include_types); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) goto error; state->config.include_types = (const char**)state->include_types; result = duplicate_strings(config->exclude_types, config->exclude_types_count, &state->exclude_types); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) goto error; state->config.exclude_types = (const char**)state->exclude_types; if (config->providers_count > 0) { result = copy_providers(config->providers, config->providers_count, &state->providers, &state->provider_encodings); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) goto error; } else @@ -382,14 +382,14 @@ static int prepare_compression_state(const chttpx_compression_config_t* config, .user_data = NULL, }; result = copy_providers(&gzip_provider, 1, &state->providers, &state->provider_encodings); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) goto error; state->config.providers_count = 1; } state->config.providers = state->providers; *state_out = state; - return CHTTPX_OK; + return cHTTPX_OK; error: free_compression_state(state); @@ -508,17 +508,18 @@ static int comma_token_contains(const char* value, const char* token) if (!end) end = cursor + strlen(cursor); - const char* trimmed_end = end; - while (trimmed_end > cursor && isspace((unsigned char)trimmed_end[-1])) - trimmed_end--; + size_t segment_length = (size_t)(end - cursor); + while (segment_length > 0 && isspace((unsigned char)cursor[segment_length - 1])) + segment_length--; - const char* equals = memchr(cursor, '=', (size_t)(trimmed_end - cursor)); + const char* equals = memchr(cursor, '=', segment_length); if (equals) - trimmed_end = equals; - while (trimmed_end > cursor && isspace((unsigned char)trimmed_end[-1])) - trimmed_end--; + segment_length = (size_t)(equals - cursor); - if ((size_t)(trimmed_end - cursor) == token_length && strncasecmp(cursor, token, token_length) == 0) + while (segment_length > 0 && isspace((unsigned char)cursor[segment_length - 1])) + segment_length--; + + if (segment_length == token_length && strncasecmp(cursor, token, token_length) == 0) return 1; cursor = *end ? end + 1 : end; @@ -836,10 +837,10 @@ static chttpx_middleware_result_t compression_middleware(chttpx_request_t* reque unsigned char* compressed = NULL; size_t compressed_size = 0; int result = selected->encode_buffer(response->body, response->body_size, state->config.level, &compressed, &compressed_size, selected->user_data); - if (result != CHTTPX_OK || !compressed || compressed_size == 0) + if (result != cHTTPX_OK || !compressed || compressed_size == 0) { free(compressed); - compression_log(request, CHTTPX_LOG_WARN, "response compression provider failed; using identity when allowed"); + compression_log(request, cHTTPX_LOG_WARN, "response compression provider failed; using identity when allowed"); if (identity_quality <= 0.0) make_empty_error(response, cHTTPX_StatusInternalServerError); return next; @@ -855,25 +856,25 @@ static chttpx_middleware_result_t compression_middleware(chttpx_request_t* reque if (!add_compression_headers(response, selected->encoding)) { free(compressed); - compression_log(request, CHTTPX_LOG_WARN, "response compression headers could not be added; using identity when allowed"); + compression_log(request, cHTTPX_LOG_WARN, "response compression headers could not be added; using identity when allowed"); if (identity_quality <= 0.0) make_empty_error(response, cHTTPX_StatusInternalServerError); return next; } - if (response->body_ownership == CHTTPX_BODY_OWNED) + if (response->body_ownership == cHTTPX_BODY_OWNED) free((void*)response->body); response->body = compressed; response->body_size = compressed_size; - response->body_ownership = CHTTPX_BODY_OWNED; + response->body_ownership = cHTTPX_BODY_OWNED; - if (server->logger && server->log_level <= CHTTPX_LOG_DEBUG) + if (server->logger && server->log_level <= cHTTPX_LOG_DEBUG) { char message[192]; snprintf(message, sizeof(message), "compressed response encoding=%s original=%zu compressed=%zu", selected->encoding, original_size, compressed_size); - server->logger(CHTTPX_LOG_DEBUG, request->request_id, message, server->logger_data); + server->logger(cHTTPX_LOG_DEBUG, request->request_id, message, server->logger_data); } return next; @@ -882,7 +883,7 @@ static chttpx_middleware_result_t compression_middleware(chttpx_request_t* reque int cHTTPX_CompressionUse(chttpx_serv_t* server, const chttpx_compression_config_t* config) { if (!server || !server->initialized) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_compression_config_t defaults; if (!config) @@ -893,11 +894,11 @@ int cHTTPX_CompressionUse(chttpx_serv_t* server, const chttpx_compression_config if (!compression_middleware_registered(server, compression_middleware) && server->middleware.after_middleware_count >= MAX_MIDDLEWARES) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; chttpx_compression_state_t* state = NULL; int result = prepare_compression_state(config, &state); - if (result != CHTTPX_OK) + if (result != cHTTPX_OK) return result; chttpx_compression_state_t* old_state = (chttpx_compression_state_t*)server->compression_state; @@ -907,16 +908,16 @@ int cHTTPX_CompressionUse(chttpx_serv_t* server, const chttpx_compression_config if (!compression_middleware_registered(server, compression_middleware)) cHTTPX_MiddlewareUseAfter(server, compression_middleware); - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_RouteCompression(chttpx_route_t* route, bool enabled) { if (!route) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; route->compression_disabled = !enabled; - return CHTTPX_OK; + return cHTTPX_OK; } void cHTTPX_ResponseCompression(chttpx_response_t* response, bool enabled) diff --git a/src/cHTTPX_i18n.c b/src/cHTTPX_i18n.c index afe6661..7993b27 100644 --- a/src/cHTTPX_i18n.c +++ b/src/cHTTPX_i18n.c @@ -271,7 +271,7 @@ const char* cHTTPX_i18n_t(const char* key, const char* lang) int cHTTPX_i18n_languages(chttpx_serv_t* server, const char** languages, size_t count, const char* fallback) { if (!server || !server->initialized) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; return _chttpx_server_set_languages(server, languages, count, fallback); } diff --git a/src/cHTTPX_i18n.h b/src/cHTTPX_i18n.h index 7f71303..d3ba89b 100644 --- a/src/cHTTPX_i18n.h +++ b/src/cHTTPX_i18n.h @@ -101,7 +101,7 @@ extern "C" * codes. * @param count Number of elements in languages. * @param fallback Fallback language code. - * @return CHTTPX_OK on + * @return cHTTPX_OK on * success, otherwise a negative error code. */ int cHTTPX_i18n_languages(struct chttpx_serv* server, const char** languages, size_t count, const char* fallback); diff --git a/src/cHTTPX_media.c b/src/cHTTPX_media.c index e656a33..0ab04e4 100644 --- a/src/cHTTPX_media.c +++ b/src/cHTTPX_media.c @@ -676,7 +676,7 @@ static void save_raw_upload(chttpx_request_t* req, char* initial_buffer, size_t if (wanted > sizeof(chunk)) wanted = sizeof(chunk); int received = _chttpx_io_recv(req->client_fd, req->_tls_session, chunk, wanted); - if (received == CHTTPX_ERR_TLS) + if (received == cHTTPX_ERR_TLS) _chttpx_tls_log_error(req->_server, req->request_id, "TLS upload read failed"); if (received <= 0 || fwrite(chunk, 1, (size_t)received, file) != (size_t)received) goto bad_request; diff --git a/src/cHTTPX_metrics.c b/src/cHTTPX_metrics.c index 82afb95..1868342 100644 --- a/src/cHTTPX_metrics.c +++ b/src/cHTTPX_metrics.c @@ -9,6 +9,7 @@ #include "cHTTPX_crosspltm.h" #include "cHTTPX_http.h" #include "cHTTPX_response.h" +#include "cHTTPX_runtime.h" #include "cHTTPX_serv.h" #include "cHTTPX_compression.h" @@ -150,17 +151,17 @@ static chttpx_route_metrics_entry_t* route_metrics_entry(chttpx_metrics_state_t* int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) { if (!server) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (!enabled) { server->metrics_state = NULL; - return CHTTPX_OK; + return cHTTPX_OK; } chttpx_metrics_state_t* state = calloc(1, sizeof(*state)); if (!state) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; #ifdef CHTTPX_PLATFORM_WINDOWS InitializeCriticalSection(&state->mutex); @@ -168,12 +169,12 @@ int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) if (pthread_mutex_init(&state->mutex, NULL) != 0) { free(state); - return CHTTPX_ERR_STATE; + return cHTTPX_ERR_STATE; } #endif server->metrics_state = state; - return CHTTPX_OK; + return cHTTPX_OK; } void _chttpx_metrics_server_cleanup(chttpx_serv_t* server) @@ -321,16 +322,35 @@ void _chttpx_metrics_rate_limit_failure(chttpx_serv_t* server) int cHTTPX_ServerMetrics(chttpx_serv_t* server, chttpx_metrics_t* metrics) { if (!server || !metrics) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_metrics_state_t* state = metrics_state(server); if (!state) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; metrics_lock(state); *metrics = state->metrics; metrics_unlock(state); - return CHTTPX_OK; + return cHTTPX_OK; +} + + +int cHTTPX_ServerRuntimeMetrics(chttpx_serv_t* server, chttpx_runtime_metrics_t* metrics) +{ + if (!server || !metrics) + return cHTTPX_ERR_INVALID_ARGUMENT; + + chttpx_worker_stats_t worker_stats; + int result = _chttpx_runtime_worker_stats(server, &worker_stats); + if (result != cHTTPX_OK) + return result; + + metrics->worker_queue_depth = (uint64_t)worker_stats.queue_depth; + metrics->active_workers = (uint64_t)worker_stats.active_workers; + metrics->rejected_jobs_total = worker_stats.rejected_jobs; + metrics->completed_jobs_total = worker_stats.completed_jobs; + metrics->queue_wait_nanoseconds_total = worker_stats.total_queue_wait_ns; + return cHTTPX_OK; } typedef struct @@ -522,6 +542,31 @@ static int prometheus_global_metrics(prometheus_buffer_t* buffer, (unsigned long long)metrics->request_duration_count); } +static int prometheus_runtime_metrics(prometheus_buffer_t* buffer, + const chttpx_runtime_metrics_t* metrics) +{ + if (!metrics) + return 1; + + return prometheus_append(buffer, "# HELP libchttpx_worker_queue_depth Requests waiting for an application worker.\n" + "# TYPE libchttpx_worker_queue_depth gauge\n") && + prometheus_appendf(buffer, "libchttpx_worker_queue_depth %llu\n", + (unsigned long long)metrics->worker_queue_depth) && + prometheus_append(buffer, "# HELP libchttpx_workers_active Application workers currently executing request jobs.\n" + "# TYPE libchttpx_workers_active gauge\n") && + prometheus_appendf(buffer, "libchttpx_workers_active %llu\n", + (unsigned long long)metrics->active_workers) && + prometheus_append(buffer, "# TYPE libchttpx_worker_jobs_rejected_total counter\n") && + prometheus_appendf(buffer, "libchttpx_worker_jobs_rejected_total %llu\n", + (unsigned long long)metrics->rejected_jobs_total) && + prometheus_append(buffer, "# TYPE libchttpx_worker_jobs_completed_total counter\n") && + prometheus_appendf(buffer, "libchttpx_worker_jobs_completed_total %llu\n", + (unsigned long long)metrics->completed_jobs_total) && + prometheus_append(buffer, "# TYPE libchttpx_worker_queue_wait_seconds_total counter\n") && + prometheus_appendf(buffer, "libchttpx_worker_queue_wait_seconds_total %.9f\n", + (double)metrics->queue_wait_nanoseconds_total / 1000000000.0); +} + static int prometheus_route_metrics(prometheus_buffer_t* buffer, chttpx_metrics_state_t* state) { @@ -565,10 +610,13 @@ static void metrics_handler(chttpx_request_t* req, chttpx_response_t* res) } prometheus_buffer_t output = {0}; + chttpx_runtime_metrics_t runtime_snapshot = {0}; + bool have_runtime_metrics = cHTTPX_ServerRuntimeMetrics(server, &runtime_snapshot) == cHTTPX_OK; metrics_lock(state); chttpx_metrics_t snapshot = state->metrics; int ok = prometheus_global_metrics(&output, &snapshot) && + (!have_runtime_metrics || prometheus_runtime_metrics(&output, &runtime_snapshot)) && prometheus_route_metrics(&output, state); metrics_unlock(state); @@ -591,15 +639,15 @@ static void metrics_handler(chttpx_request_t* req, chttpx_response_t* res) int cHTTPX_MetricsRoute(chttpx_router_t* router, const char* path) { if (!router || !router->serv || !path || !*path) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (!metrics_state(router->serv)) - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; chttpx_route_t* route = cHTTPX_Get(router, path, metrics_handler); if (!route) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; cHTTPX_RouteCompression(route, false); - return CHTTPX_OK; + return cHTTPX_OK; } diff --git a/src/cHTTPX_metrics.h b/src/cHTTPX_metrics.h index 045249a..18a64e2 100644 --- a/src/cHTTPX_metrics.h +++ b/src/cHTTPX_metrics.h @@ -54,14 +54,40 @@ extern "C" uint64_t request_duration_buckets[CHTTPX_METRICS_DURATION_BUCKETS]; } chttpx_metrics_t; + /** + * Snapshot of the bounded application worker pool. + * + * The pool is internal to the server runtime and uses a fixed 32 workers. + * Queue depth and active_workers are gauges; the remaining fields are + * monotonic counters for the lifetime of the server runtime. + */ + typedef struct + { + uint64_t worker_queue_depth; + uint64_t active_workers; + uint64_t rejected_jobs_total; + uint64_t completed_jobs_total; + uint64_t queue_wait_nanoseconds_total; + } chttpx_runtime_metrics_t; + + /** * Copy a consistent metrics snapshot. * - * Returns CHTTPX_OK on success or CHTTPX_ERR_UNAVAILABLE when metrics are + * Returns cHTTPX_OK on success or cHTTPX_ERR_UNAVAILABLE when metrics are * disabled for the server. */ int cHTTPX_ServerMetrics(struct chttpx_serv* server, chttpx_metrics_t* metrics); + /** + * Copy a worker-runtime metrics snapshot. + * + * Returns cHTTPX_OK while the server runtime is active, otherwise + * cHTTPX_ERR_UNAVAILABLE. + */ + int cHTTPX_ServerRuntimeMetrics(struct chttpx_serv* server, chttpx_runtime_metrics_t* metrics); + + /** * Register a Prometheus text exposition endpoint on the supplied router. * diff --git a/src/cHTTPX_middlewares.c b/src/cHTTPX_middlewares.c index 483dc76..c8f93c1 100644 --- a/src/cHTTPX_middlewares.c +++ b/src/cHTTPX_middlewares.c @@ -199,8 +199,8 @@ void postmiddleware_logging_write(chttpx_request_t* req, chttpx_response_t* res) snprintf(message, sizeof(message), "%s \"%s %s %s\" %d %zu \"%s\" %.4fms", req->client_ip, req->method ? req->method : "", req->path ? req->path : "", req->protocol, res->status, res->body_size, req->user_agent, ms); - if (server->logger && server->log_level <= CHTTPX_LOG_INFO) - server->logger(CHTTPX_LOG_INFO, req->request_id, message, server->logger_data); + if (server->logger && server->log_level <= cHTTPX_LOG_INFO) + server->logger(cHTTPX_LOG_INFO, req->request_id, message, server->logger_data); } void cHTTPX_MiddlewareLogging(chttpx_serv_t* server) diff --git a/src/cHTTPX_request.c b/src/cHTTPX_request.c index 5216108..66f0883 100644 --- a/src/cHTTPX_request.c +++ b/src/cHTTPX_request.c @@ -522,7 +522,7 @@ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t f return 0; } - if (f->normalizers & CHTTPX_TRIM) + if (f->normalizers & cHTTPX_TRIM) { char* start = v; while (*start && isspace((unsigned char)*start)) @@ -534,12 +534,12 @@ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t f v[--trim_len] = '\0'; } - if (f->normalizers & CHTTPX_LOWERCASE) + if (f->normalizers & cHTTPX_LOWERCASE) { for (char* p = v; *p; p++) *p = (char)tolower((unsigned char)*p); } - else if (f->normalizers & CHTTPX_UPPERCASE) + else if (f->normalizers & cHTTPX_UPPERCASE) { for (char* p = v; *p; p++) *p = (char)toupper((unsigned char)*p); diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h index a9cb79c..add942c 100644 --- a/src/cHTTPX_request.h +++ b/src/cHTTPX_request.h @@ -109,12 +109,19 @@ extern "C" typedef enum { - CHTTPX_NORMALIZE_NONE = 0, - CHTTPX_TRIM = 1 << 0, - CHTTPX_LOWERCASE = 1 << 1, - CHTTPX_UPPERCASE = 1 << 2 + cHTTPX_NORMALIZE_NONE = 0, + cHTTPX_TRIM = 1 << 0, + cHTTPX_LOWERCASE = 1 << 1, + cHTTPX_UPPERCASE = 1 << 2 } chttpx_normalizer_t; +#ifndef CHTTPX_DISABLE_LEGACY_NORMALIZER_NAMES +#define CHTTPX_NORMALIZE_NONE cHTTPX_NORMALIZE_NONE +#define CHTTPX_TRIM cHTTPX_TRIM +#define CHTTPX_LOWERCASE cHTTPX_LOWERCASE +#define CHTTPX_UPPERCASE cHTTPX_UPPERCASE +#endif + typedef struct { const char* name; @@ -418,7 +425,7 @@ extern "C" #define chttpx_validation_string(name, ptr, required, min_length, max_length, validator) \ (chttpx_validation_t) \ { \ - name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, cHTTPX_NORMALIZE_NONE, NULL \ } /** @@ -434,7 +441,7 @@ extern "C" #define chttpx_validation_integer(name, ptr, required) \ (chttpx_validation_t) \ { \ - name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ } /** @@ -450,7 +457,7 @@ extern "C" #define chttpx_validation_boolean(name, ptr, required) \ (chttpx_validation_t) \ { \ - name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, CHTTPX_NORMALIZE_NONE, NULL \ + name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ } #define cHTTPX_StringField(name, ptr, required, min_length, max_length, normalizers, validator) \ diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index e880bcf..c448da0 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -304,8 +304,8 @@ static ssize_t read_req(chttpx_serv_t* server, chttpx_socket_t fd, void* tls_ses return -2; int n = _chttpx_io_recv(fd, tls_session, buffer + total, buffer_size - 1 - total); - if (n == CHTTPX_ERR_TLS) - return CHTTPX_ERR_TLS; + if (n == cHTTPX_ERR_TLS) + return cHTTPX_ERR_TLS; if (n < 0) { #ifdef CHTTPX_PLATFORM_POSIX @@ -313,7 +313,7 @@ static ssize_t read_req(chttpx_serv_t* server, chttpx_socket_t fd, void* tls_ses continue; #endif if (socket_read_timed_out()) - return CHTTPX_ERR_TIMEOUT; + return cHTTPX_ERR_TIMEOUT; return -1; } if (n == 0) @@ -405,7 +405,7 @@ static int append_response_header(char* buffer, size_t capacity, size_t* length, static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, char** output, size_t* output_size) { if (!req || !output || !output_size) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; *output = NULL; *output_size = 0; @@ -416,7 +416,7 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c size_t name_size = strlen(res.headers[i].name); size_t value_size = strlen(res.headers[i].value); if (capacity > SIZE_MAX - name_size - value_size - 4) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; capacity += name_size + value_size + 4; } if (server && server->cors.enabled) @@ -424,13 +424,13 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c size_t methods_size = server->cors.methods ? strlen(server->cors.methods) : 0; size_t headers_size = server->cors.headers ? strlen(server->cors.headers) : 0; if (capacity > SIZE_MAX - methods_size - headers_size - MAX_HEADER_VALUE - 512) - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; capacity += methods_size + headers_size + MAX_HEADER_VALUE + 512; } char* header = malloc(capacity); if (!header) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; size_t length = 0; const char* allowed_origin = server && server->cors.enabled ? allowed_origin_cors(server, cHTTPX_HeaderGet(req, "Origin")) : NULL; @@ -471,7 +471,7 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c if (!response) { free(header); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } memcpy(response, header, length); if (res.body && res.body_size) @@ -479,11 +479,11 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c free(header); *output = response; *output_size = total; - return CHTTPX_OK; + return cHTTPX_OK; limit_error: free(header); - return CHTTPX_ERR_LIMIT; + return cHTTPX_ERR_LIMIT; } static void send_response(chttpx_request_t* req, chttpx_response_t res) @@ -491,10 +491,10 @@ static void send_response(chttpx_request_t* req, chttpx_response_t res) char* response = NULL; size_t response_size = 0; int build_result = build_response_buffer(req, res, &response, &response_size); - if (build_result != CHTTPX_OK) + if (build_result != cHTTPX_OK) return; int write_result = _chttpx_io_send_all(req->client_fd, req->_tls_session, response, response_size); - if (write_result == CHTTPX_ERR_TLS) + if (write_result == cHTTPX_ERR_TLS) _chttpx_tls_log_error(req->_server, req->request_id, "TLS response write failed"); free(response); } @@ -814,7 +814,7 @@ static chttpx_request_t* parse_prefetched_request(chttpx_serv_t* server, chttpx_ int _chttpx_dispatch(chttpx_serv_t* server, chttpx_request_t* req, chttpx_response_t* res) { if (!server || !server->initialized || !req || !res || !req->method || !req->path) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; req->_server = server; @@ -910,7 +910,7 @@ int _chttpx_dispatch(chttpx_serv_t* server, chttpx_request_t* req, chttpx_respon duration_seconds); postmiddleware_logging_write(req, res); - return CHTTPX_OK; + return cHTTPX_OK; } int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size) @@ -920,14 +920,14 @@ int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, free(body); if (body_stream) fclose(body_stream); - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; } *output = NULL; *output_size = 0; chttpx_request_t* req = parse_prefetched_request(server, client_fd, tls_session, headers, header_size, body, body_size, body_stream, content_length); if (!req) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; chttpx_response_t res = {0}; if (req->_parse_status) @@ -939,7 +939,7 @@ int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, else if (!build_cors_preflight(req, &res)) { int dispatch_result = _chttpx_dispatch(server, req, &res); - if (dispatch_result != CHTTPX_OK) + if (dispatch_result != cHTTPX_OK) res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "request dispatch failed"); } @@ -972,7 +972,7 @@ void* chttpx_handle(void* arg) set_client_timeout(server, client_sock); void* tls_session = NULL; - if (_chttpx_tls_accept(server, client_sock, &tls_session) != CHTTPX_OK) + if (_chttpx_tls_accept(server, client_sock, &tls_session) != cHTTPX_OK) { _chttpx_metrics_connection_rejected(server); chttpx_close(client_sock); @@ -991,9 +991,9 @@ void* chttpx_handle(void* arg) } if (received <= 0) { - if (received == CHTTPX_ERR_TIMEOUT) + if (received == cHTTPX_ERR_TIMEOUT) _chttpx_metrics_timeout_failure(server); - else if (received == CHTTPX_ERR_TLS) + else if (received == cHTTPX_ERR_TLS) { _chttpx_metrics_connection_rejected(server); _chttpx_tls_log_error(server, "-", "TLS request-header read failed"); @@ -1024,7 +1024,7 @@ void* chttpx_handle(void* arg) goto cleanup_request; chttpx_response_t res = {0}; - if (_chttpx_dispatch(server, req, &res) == CHTTPX_OK) + if (_chttpx_dispatch(server, req, &res) == cHTTPX_OK) { send_response(req, res); cHTTPX_ResponseCleanup(&res); @@ -1103,7 +1103,7 @@ static chttpx_response_t response_format(uint16_t status, const char* content_ty .body_size = strlen(fallback)}; } vsnprintf((char*)body, len + 1, fmt, args); - return (chttpx_response_t){.status = status, .content_type = content_type, .body = body, .body_size = len, .body_ownership = CHTTPX_BODY_OWNED}; + return (chttpx_response_t){.status = status, .content_type = content_type, .body = body, .body_size = len, .body_ownership = cHTTPX_BODY_OWNED}; } chttpx_response_t cHTTPX_ResJson(uint16_t status, const char* fmt, ...) @@ -1167,7 +1167,7 @@ chttpx_response_t cHTTPX_ResBinary(uint16_t status, const char* content_type, co .content_type = content_type, .body = buffer, .body_size = body_size, - .body_ownership = CHTTPX_BODY_OWNED, + .body_ownership = cHTTPX_BODY_OWNED, .start_ts = {0}, .end_ts = {0}}; } @@ -1225,7 +1225,7 @@ chttpx_response_t cHTTPX_ResFile(uint16_t status, const char* content_type, cons .content_type = content_type, .body = data, .body_size = size, - .body_ownership = CHTTPX_BODY_OWNED, + .body_ownership = cHTTPX_BODY_OWNED, .start_ts = {0}, .end_ts = {0}}; } @@ -1234,11 +1234,11 @@ void cHTTPX_ResponseCleanup(chttpx_response_t* res) { if (!res) return; - if (res->body_ownership == CHTTPX_BODY_OWNED) + if (res->body_ownership == cHTTPX_BODY_OWNED) free((void*)res->body); res->body = NULL; res->body_size = 0; - res->body_ownership = CHTTPX_BODY_BORROWED; + res->body_ownership = cHTTPX_BODY_BORROWED; } chttpx_response_t cHTTPX_ResNoContent(void) diff --git a/src/cHTTPX_response.h b/src/cHTTPX_response.h index 0b59425..34bcb02 100644 --- a/src/cHTTPX_response.h +++ b/src/cHTTPX_response.h @@ -22,10 +22,15 @@ extern "C" // RESponse typedef enum { - CHTTPX_BODY_BORROWED = 0, - CHTTPX_BODY_OWNED = 1 + cHTTPX_BODY_BORROWED = 0, + cHTTPX_BODY_OWNED = 1 } chttpx_body_ownership_t; +#ifndef CHTTPX_DISABLE_LEGACY_BODY_OWNERSHIP_NAMES +#define CHTTPX_BODY_BORROWED cHTTPX_BODY_BORROWED +#define CHTTPX_BODY_OWNED cHTTPX_BODY_OWNED +#endif + typedef struct chttpx_response { /* Response status code */ @@ -140,7 +145,7 @@ extern "C" * @param data Buffer * to send. * @param size Buffer size in bytes. - * @return CHTTPX_OK on success, otherwise a negative error code. + * @return cHTTPX_OK on success, otherwise a negative error code. */ int cHTTPX_SendAll(chttpx_socket_t fd, const void* data, size_t size); diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c index 943b2dc..2669d27 100644 --- a/src/cHTTPX_runtime.c +++ b/src/cHTTPX_runtime.c @@ -1,6 +1,7 @@ #include "cHTTPX_runtime.h" #include "cHTTPX_event.h" +#include "cHTTPX_worker.h" #include "cHTTPX_http.h" #include "cHTTPX_metrics.h" #include "cHTTPX_tls.h" @@ -13,7 +14,6 @@ #include #include -#define CHTTPX_WORKER_THREADS 32 #define CHTTPX_RUNTIME_EVENTS 256 #define CHTTPX_CHUNK_LINE_MAX 128 @@ -23,7 +23,8 @@ typedef enum CHTTPX_CONN_READING_HEADERS, CHTTPX_CONN_READING_BODY, CHTTPX_CONN_PROCESSING, - CHTTPX_CONN_WRITING + CHTTPX_CONN_WRITING, + CHTTPX_CONN_CLOSING } chttpx_connection_state_t; typedef enum @@ -80,24 +81,13 @@ typedef struct chttpx_runtime { chttpx_serv_t* server; chttpx_event_loop_t* event_loop; - thread_t workers[CHTTPX_WORKER_THREADS]; - size_t workers_started; - chttpx_connection_t** jobs; - size_t job_capacity; - size_t job_head; - size_t job_tail; - size_t job_count; - bool workers_stopping; + chttpx_worker_pool_t* worker_pool; bool stopping; bool shutdown_started; bool listener_registered; #ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION job_mutex; - CONDITION_VARIABLE job_ready; CRITICAL_SECTION completion_mutex; #else - pthread_mutex_t job_mutex; - pthread_cond_t job_ready; pthread_mutex_t completion_mutex; #endif chttpx_connection_t* completions; @@ -131,51 +121,6 @@ static uint64_t monotonic_ms(void) #endif } -static void job_lock(chttpx_runtime_t* runtime) -{ -#ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&runtime->job_mutex); -#else - pthread_mutex_lock(&runtime->job_mutex); -#endif -} - -static void job_unlock(chttpx_runtime_t* runtime) -{ -#ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&runtime->job_mutex); -#else - pthread_mutex_unlock(&runtime->job_mutex); -#endif -} - -static void job_wait(chttpx_runtime_t* runtime) -{ -#ifdef CHTTPX_PLATFORM_WINDOWS - SleepConditionVariableCS(&runtime->job_ready, &runtime->job_mutex, INFINITE); -#else - pthread_cond_wait(&runtime->job_ready, &runtime->job_mutex); -#endif -} - -static void job_signal(chttpx_runtime_t* runtime) -{ -#ifdef CHTTPX_PLATFORM_WINDOWS - WakeConditionVariable(&runtime->job_ready); -#else - pthread_cond_signal(&runtime->job_ready); -#endif -} - -static void job_broadcast(chttpx_runtime_t* runtime) -{ -#ifdef CHTTPX_PLATFORM_WINDOWS - WakeAllConditionVariable(&runtime->job_ready); -#else - pthread_cond_broadcast(&runtime->job_ready); -#endif -} - static void completion_lock(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -197,25 +142,10 @@ static void completion_unlock(chttpx_runtime_t* runtime) static int sync_init(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&runtime->job_mutex); - InitializeConditionVariable(&runtime->job_ready); InitializeCriticalSection(&runtime->completion_mutex); return 0; #else - if (pthread_mutex_init(&runtime->job_mutex, NULL) != 0) - return -1; - if (pthread_cond_init(&runtime->job_ready, NULL) != 0) - { - pthread_mutex_destroy(&runtime->job_mutex); - return -1; - } - if (pthread_mutex_init(&runtime->completion_mutex, NULL) != 0) - { - pthread_cond_destroy(&runtime->job_ready); - pthread_mutex_destroy(&runtime->job_mutex); - return -1; - } - return 0; + return pthread_mutex_init(&runtime->completion_mutex, NULL) == 0 ? 0 : -1; #endif } @@ -223,11 +153,8 @@ static void sync_destroy(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS DeleteCriticalSection(&runtime->completion_mutex); - DeleteCriticalSection(&runtime->job_mutex); #else pthread_mutex_destroy(&runtime->completion_mutex); - pthread_cond_destroy(&runtime->job_ready); - pthread_mutex_destroy(&runtime->job_mutex); #endif } @@ -249,73 +176,36 @@ static chttpx_connection_t* completion_take_all(chttpx_runtime_t* runtime) return list; } -static bool worker_submit(chttpx_runtime_t* runtime, chttpx_connection_t* connection) +static void runtime_worker_execute(void* job, void* context) { - job_lock(runtime); - if (runtime->workers_stopping || runtime->job_count >= runtime->job_capacity) - { - job_unlock(runtime); - return false; - } - runtime->jobs[runtime->job_tail] = connection; - runtime->job_tail = (runtime->job_tail + 1) % runtime->job_capacity; - runtime->job_count++; - job_signal(runtime); - job_unlock(runtime); - return true; -} + chttpx_connection_t* connection = job; + chttpx_runtime_t* runtime = context; -static chttpx_connection_t* worker_take(chttpx_runtime_t* runtime) -{ - job_lock(runtime); - while (runtime->job_count == 0 && !runtime->workers_stopping) - job_wait(runtime); - if (runtime->job_count == 0 && runtime->workers_stopping) - { - job_unlock(runtime); - return NULL; - } - chttpx_connection_t* connection = runtime->jobs[runtime->job_head]; - runtime->jobs[runtime->job_head] = NULL; - runtime->job_head = (runtime->job_head + 1) % runtime->job_capacity; - runtime->job_count--; - job_unlock(runtime); - return connection; -} + unsigned char* body = connection->body; + FILE* stream = connection->body_stream; + connection->body = NULL; + connection->body_stream = NULL; -static void* runtime_worker(void* argument) -{ - chttpx_runtime_t* runtime = argument; - for (;;) + if (runtime_is_stopping(runtime)) { - chttpx_connection_t* connection = worker_take(runtime); - if (!connection) - break; + free(body); - unsigned char* body = connection->body; - FILE* stream = connection->body_stream; - connection->body = NULL; - connection->body_stream = NULL; + if (stream) + fclose(stream); - if (runtime_is_stopping(runtime)) - { - free(body); - if (stream) - fclose(stream); - connection->worker_result = CHTTPX_ERR_STATE; - } - else - { - connection->worker_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session, connection->headers, connection->header_end, body, connection->body_size, stream, connection->content_length, &connection->write_buffer, &connection->write_size); - } - - free(connection->headers); - connection->headers = NULL; - connection->header_size = 0; - connection->header_capacity = 0; - completion_push(runtime, connection); + connection->worker_result = cHTTPX_ERR_STATE; + } + else + { + connection->worker_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session, connection->headers, connection->header_end, body, connection->body_size, stream, connection->content_length, &connection->write_buffer, &connection->write_size); } - return NULL; + + free(connection->headers); + connection->headers = NULL; + connection->header_size = 0; + connection->header_capacity = 0; + + completion_push(runtime, connection); } static void connection_unlink(chttpx_runtime_t* runtime, chttpx_connection_t* connection) @@ -336,19 +226,28 @@ static void connection_close(chttpx_runtime_t* runtime, chttpx_connection_t* con { if (!runtime || !connection) return; + + connection->state = CHTTPX_CONN_CLOSING; _chttpx_event_del(runtime->event_loop, connection->fd); connection_unlink(runtime, connection); + _chttpx_tls_session_close(connection->tls_session); connection->tls_session = NULL; + if (socket_valid(connection->fd)) chttpx_close(connection->fd); + free(connection->headers); free(connection->body); + if (connection->body_stream) fclose(connection->body_stream); + free(connection->write_buffer); + _chttpx_metrics_connection_closed(connection->server); __atomic_fetch_sub(&connection->server->current_clients, 1, __ATOMIC_SEQ_CST); + free(connection); } @@ -378,16 +277,21 @@ static int parse_size_value(const char* value, size_t value_size, size_t* output { if (!value || !value_size || !output || value_size >= 32) return 0; + char buffer[32]; memcpy(buffer, value, value_size); buffer[value_size] = '\0'; + if (buffer[0] == '-') return 0; + errno = 0; char* end = NULL; unsigned long long parsed = strtoull(buffer, &end, 10); + if (errno == ERANGE || !end || *end || parsed > SIZE_MAX) return 0; + *output = (size_t)parsed; return 1; } @@ -726,7 +630,7 @@ static int connection_submit(chttpx_runtime_t* runtime, chttpx_connection_t* con } _chttpx_event_del(runtime->event_loop, connection->fd); connection->state = CHTTPX_CONN_PROCESSING; - if (!worker_submit(runtime, connection)) + if (!_chttpx_worker_submit(runtime->worker_pool, connection)) { connection_close(runtime, connection); return 0; @@ -841,7 +745,7 @@ static int connection_process_bytes(chttpx_runtime_t* runtime, chttpx_connection static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { int result = _chttpx_tls_accept_step(connection->tls_session); - if (result == CHTTPX_OK) + if (result == cHTTPX_OK) { connection->state = CHTTPX_CONN_READING_HEADERS; connection_touch(connection); @@ -914,7 +818,7 @@ static int connection_read_ready(chttpx_runtime_t* runtime, chttpx_connection_t* } return 1; } - if (result == CHTTPX_ERR_TLS) + if (result == cHTTPX_ERR_TLS) _chttpx_tls_log_error(connection->server, "-", "TLS request read failed"); connection_close(runtime, connection); return 0; @@ -950,7 +854,7 @@ static int connection_write_ready(chttpx_runtime_t* runtime, chttpx_connection_t } return 1; } - if (result == CHTTPX_ERR_TLS) + if (result == cHTTPX_ERR_TLS) _chttpx_tls_log_error(connection->server, "-", "TLS response write failed"); connection_close(runtime, connection); return 0; @@ -966,7 +870,7 @@ static void drain_completions(chttpx_runtime_t* runtime) { chttpx_connection_t* next = connection->completion_next; connection->completion_next = NULL; - if (runtime_is_stopping(runtime) || connection->worker_result != CHTTPX_OK || !connection->write_buffer) + if (runtime_is_stopping(runtime) || connection->worker_result != cHTTPX_OK || !connection->write_buffer) connection_close(runtime, connection); else { @@ -1039,7 +943,7 @@ static void accept_connections(chttpx_runtime_t* runtime) _chttpx_metrics_connection_opened(server); int tls_result = _chttpx_tls_accept_begin(server, fd, &connection->tls_session); - if (tls_result != CHTTPX_OK) + if (tls_result != cHTTPX_OK) { _chttpx_metrics_connection_rejected(server); connection_close(runtime, connection); @@ -1050,7 +954,7 @@ static void accept_connections(chttpx_runtime_t* runtime) if (server->tls.enabled) { int step = _chttpx_tls_accept_step(connection->tls_session); - if (step == CHTTPX_OK) + if (step == cHTTPX_OK) connection->state = CHTTPX_CONN_READING_HEADERS; else if (step == CHTTPX_IO_WANT_READ) interest = CHTTPX_EVENT_READ; @@ -1121,24 +1025,17 @@ static void begin_shutdown(chttpx_runtime_t* runtime) int _chttpx_runtime_init(chttpx_serv_t* server) { if (!server || !socket_valid(server->server_fd) || server->max_clients == 0) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; chttpx_runtime_t* runtime = calloc(1, sizeof(*runtime)); if (!runtime) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; runtime->server = server; - runtime->job_capacity = server->max_clients; - runtime->jobs = calloc(runtime->job_capacity, sizeof(*runtime->jobs)); - if (!runtime->jobs) - { - free(runtime); - return CHTTPX_ERR_MEMORY; - } + if (sync_init(runtime) != 0) { - free(runtime->jobs); free(runtime); - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } runtime->event_loop = _chttpx_event_create(); @@ -1146,29 +1043,21 @@ int _chttpx_runtime_init(chttpx_serv_t* server) goto error; runtime->listener_registered = true; - for (size_t i = 0; i < CHTTPX_WORKER_THREADS; i++) - { - if (_thread_create(&runtime->workers[i], runtime_worker, runtime) != 0) - goto error; - runtime->workers_started++; - } + if (_chttpx_worker_pool_create(&runtime->worker_pool, server->max_clients, runtime_worker_execute, runtime) != 0) + goto error; server->runtime_state = runtime; - return CHTTPX_OK; + return cHTTPX_OK; error: - job_lock(runtime); - runtime->workers_stopping = true; - job_broadcast(runtime); - job_unlock(runtime); - for (size_t i = 0; i < runtime->workers_started; i++) - _thread_join(runtime->workers[i]); + _chttpx_worker_pool_destroy(runtime->worker_pool); + if (runtime->event_loop) _chttpx_event_destroy(runtime->event_loop); + sync_destroy(runtime); - free(runtime->jobs); free(runtime); - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } void _chttpx_runtime_listen(chttpx_serv_t* server) @@ -1230,11 +1119,25 @@ void _chttpx_runtime_listen(chttpx_serv_t* server) drain_completions(runtime); } +int _chttpx_runtime_worker_stats(chttpx_serv_t* server, chttpx_worker_stats_t* stats) +{ + if (!server || !stats) + return cHTTPX_ERR_INVALID_ARGUMENT; + + chttpx_runtime_t* runtime = server->runtime_state; + if (!runtime || !runtime->worker_pool) + return cHTTPX_ERR_UNAVAILABLE; + + _chttpx_worker_stats(runtime->worker_pool, stats); + return cHTTPX_OK; +} + void _chttpx_runtime_request_stop(chttpx_serv_t* server) { chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; if (!runtime) return; + __atomic_store_n(&runtime->stopping, true, __ATOMIC_RELEASE); _chttpx_event_wake(runtime->event_loop); } @@ -1248,13 +1151,8 @@ void _chttpx_runtime_cleanup(chttpx_serv_t* server) __atomic_store_n(&runtime->stopping, true, __ATOMIC_RELEASE); begin_shutdown(runtime); - job_lock(runtime); - runtime->workers_stopping = true; - job_broadcast(runtime); - job_unlock(runtime); - - for (size_t i = 0; i < runtime->workers_started; i++) - _thread_join(runtime->workers[i]); + _chttpx_worker_pool_destroy(runtime->worker_pool); + runtime->worker_pool = NULL; drain_completions(runtime); @@ -1263,7 +1161,6 @@ void _chttpx_runtime_cleanup(chttpx_serv_t* server) _chttpx_event_destroy(runtime->event_loop); sync_destroy(runtime); - free(runtime->jobs); free(runtime); server->runtime_state = NULL; } diff --git a/src/cHTTPX_runtime.h b/src/cHTTPX_runtime.h index 313d629..a50e736 100644 --- a/src/cHTTPX_runtime.h +++ b/src/cHTTPX_runtime.h @@ -2,10 +2,12 @@ #define CHTTPX_RUNTIME_H #include "cHTTPX_serv.h" +#include "cHTTPX_worker.h" int _chttpx_runtime_init(chttpx_serv_t* server); void _chttpx_runtime_listen(chttpx_serv_t* server); void _chttpx_runtime_request_stop(chttpx_serv_t* server); void _chttpx_runtime_cleanup(chttpx_serv_t* server); +int _chttpx_runtime_worker_stats(chttpx_serv_t* server, chttpx_worker_stats_t* stats); #endif diff --git a/src/cHTTPX_serv.c b/src/cHTTPX_serv.c index d6fdc63..f750305 100644 --- a/src/cHTTPX_serv.c +++ b/src/cHTTPX_serv.c @@ -25,8 +25,8 @@ static void default_logger(chttpx_log_level_t level, const char* request_id, con { (void)user_data; static const char* names[] = {"DEBUG", "INFO", "WARN", "ERROR", "OFF"}; - if (level < CHTTPX_LOG_DEBUG || level > CHTTPX_LOG_OFF) - level = CHTTPX_LOG_ERROR; + if (level < cHTTPX_LOG_DEBUG || level > cHTTPX_LOG_OFF) + level = cHTTPX_LOG_ERROR; fprintf(stderr, "[%s] request_id=%s %s\n", names[level], request_id && *request_id ? request_id : "-", message ? message : ""); } @@ -75,7 +75,7 @@ static void free_server_languages(chttpx_serv_t* server) int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, size_t count, const char* fallback) { if (!server || !fallback || (count > 0 && !languages)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; char** owned_languages = count ? calloc(count, sizeof(*owned_languages)) : NULL; char* owned_fallback = strdup(fallback); @@ -83,7 +83,7 @@ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, { free(owned_languages); free(owned_fallback); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } for (size_t i = 0; i < count; i++) @@ -94,7 +94,7 @@ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, free(owned_languages[j]); free(owned_languages); free(owned_fallback); - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; } owned_languages[i] = strdup(languages[i]); @@ -104,7 +104,7 @@ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, free(owned_languages[j]); free(owned_languages); free(owned_fallback); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } } @@ -112,7 +112,7 @@ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, server->languages = (const char**)owned_languages; server->languages_count = count; server->default_language = owned_fallback; - return CHTTPX_OK; + return cHTTPX_OK; } static void free_route_upload_policy(chttpx_route_t* registered) @@ -130,7 +130,7 @@ static void free_route_upload_policy(chttpx_route_t* registered) chttpx_config_t cHTTPX_DefaultConfig(void) { return (chttpx_config_t){.port = 8080, - .network_mode = CHTTPX_NETWORK_DUAL, + .network_mode = cHTTPX_NETWORK_DUAL, .max_clients = MAX_CLIENTS_DEFAULT, .read_timeout_sec = 30, .write_timeout_sec = 30, @@ -141,15 +141,15 @@ chttpx_config_t cHTTPX_DefaultConfig(void) .request_id_enabled = true, .metrics_enabled = false, .default_language = "en", - .log_level = CHTTPX_LOG_INFO}; + .log_level = cHTTPX_LOG_INFO}; } int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const char* name, const chttpx_config_t* config) { if (!server || !app || !name || !*name || !config || config->max_clients == 0 || - config->network_mode < CHTTPX_NETWORK_IPV4 || config->network_mode > CHTTPX_NETWORK_DUAL || + config->network_mode < cHTTPX_NETWORK_IPV4 || config->network_mode > cHTTPX_NETWORK_DUAL || (config->languages_count > 0 && !config->languages)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; memset(server, 0, sizeof(*server)); invalidate_socket(server); @@ -157,7 +157,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha server->app = app; server->name = strdup(name); if (!server->name) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; server->port = config->port; server->network_mode = config->network_mode; @@ -175,7 +175,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha int languages_result = _chttpx_server_set_languages(server, config->languages, config->languages_count, config->default_language ? config->default_language : "en"); - if (languages_result != CHTTPX_OK) + if (languages_result != cHTTPX_OK) { free(server->name); server->name = NULL; @@ -184,14 +184,14 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha _recovery_init(); - int family = config->network_mode == CHTTPX_NETWORK_IPV4 ? AF_INET : AF_INET6; + int family = config->network_mode == cHTTPX_NETWORK_IPV4 ? AF_INET : AF_INET6; server->server_fd = socket(family, SOCK_STREAM, 0); if (!socket_valid(server->server_fd)) goto socket_error; if (family == AF_INET6) { - int ipv6_only = config->network_mode == CHTTPX_NETWORK_IPV6 ? 1 : 0; + int ipv6_only = config->network_mode == cHTTPX_NETWORK_IPV6 ? 1 : 0; if (setsockopt(server->server_fd, IPPROTO_IPV6, IPV6_V6ONLY, #ifdef CHTTPX_PLATFORM_WINDOWS (const char*)&ipv6_only, @@ -241,7 +241,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha free_server_languages(server); free(server->name); server->name = NULL; - return CHTTPX_ERR_BIND; + return cHTTPX_ERR_BIND; } if (config->port == 0) @@ -255,7 +255,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha free_server_languages(server); free(server->name); server->name = NULL; - return CHTTPX_ERR_SOCKET; + return cHTTPX_ERR_SOCKET; } if (bound.ss_family == AF_INET) @@ -269,7 +269,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha free_server_languages(server); free(server->name); server->name = NULL; - return CHTTPX_ERR_SOCKET; + return cHTTPX_ERR_SOCKET; } } @@ -280,11 +280,11 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha free_server_languages(server); free(server->name); server->name = NULL; - return CHTTPX_ERR_LISTEN; + return cHTTPX_ERR_LISTEN; } int tls_result = _chttpx_tls_server_init(server, &config->tls); - if (tls_result != CHTTPX_OK) + if (tls_result != cHTTPX_OK) { chttpx_close(server->server_fd); invalidate_socket(server); @@ -295,7 +295,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha } int metrics_result = _chttpx_metrics_server_init(server, config->metrics_enabled); - if (metrics_result != CHTTPX_OK) + if (metrics_result != cHTTPX_OK) { _chttpx_tls_server_cleanup(server); chttpx_close(server->server_fd); @@ -307,7 +307,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha } int runtime_result = _chttpx_runtime_init(server); - if (runtime_result != CHTTPX_OK) + if (runtime_result != cHTTPX_OK) { _chttpx_metrics_server_cleanup(server); _chttpx_tls_server_cleanup(server); @@ -320,13 +320,13 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha } server->initialized = true; - return CHTTPX_OK; + return cHTTPX_OK; socket_error: free_server_languages(server); free(server->name); server->name = NULL; - return CHTTPX_ERR_SOCKET; + return cHTTPX_ERR_SOCKET; } static chttpx_route_t* route(chttpx_router_t* router, const char* method, const char* path, chttpx_handler_t handler) @@ -440,43 +440,43 @@ chttpx_router_t cHTTPX_RouteGroup(const chttpx_router_t* parent, const char* pre int cHTTPX_RouterUse(chttpx_router_t* router, chttpx_middleware_t middleware) { if (!router || !middleware || router->middleware_count >= MAX_MIDDLEWARES) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; router->middlewares[router->middleware_count++] = middleware; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_RouterUseAfter(chttpx_router_t* router, chttpx_middleware_t middleware) { if (!router || !middleware || router->after_middleware_count >= MAX_MIDDLEWARES) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; router->after_middlewares[router->after_middleware_count++] = middleware; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_RouteUse(chttpx_route_t* registered, chttpx_middleware_t middleware) { if (!registered || !middleware || registered->middleware_count >= MAX_MIDDLEWARES) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; registered->middlewares[registered->middleware_count++] = middleware; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_RouteUseAfter(chttpx_route_t* registered, chttpx_middleware_t middleware) { if (!registered || !middleware || registered->after_middleware_count >= MAX_MIDDLEWARES) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; registered->after_middlewares[registered->after_middleware_count++] = middleware; - return CHTTPX_OK; + return cHTTPX_OK; } int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_policy_t* policy) { if (!registered || !policy || (policy->allowed_types_count > 0 && !policy->allowed_types)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; char** owned_types = policy->allowed_types_count ? calloc(policy->allowed_types_count, sizeof(*owned_types)) : NULL; if (policy->allowed_types_count && !owned_types) - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; for (size_t i = 0; i < policy->allowed_types_count; i++) { @@ -485,7 +485,7 @@ int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_pol for (size_t j = 0; j < i; j++) free(owned_types[j]); free(owned_types); - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; } owned_types[i] = strdup(policy->allowed_types[i]); @@ -494,7 +494,7 @@ int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_pol for (size_t j = 0; j < i; j++) free(owned_types[j]); free(owned_types); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } } @@ -502,7 +502,7 @@ int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_pol registered->upload_policy = *policy; registered->upload_policy.allowed_types = (const char**)owned_types; registered->has_upload_policy = true; - return CHTTPX_OK; + return cHTTPX_OK; } void cHTTPX_RouterFree(chttpx_router_t* router) diff --git a/src/cHTTPX_serv.h b/src/cHTTPX_serv.h index b1c8438..af6fafa 100644 --- a/src/cHTTPX_serv.h +++ b/src/cHTTPX_serv.h @@ -28,41 +28,75 @@ extern "C" typedef enum { - CHTTPX_OK = 0, - CHTTPX_ERR_MEMORY = -1, - CHTTPX_ERR_SOCKET = -2, - CHTTPX_ERR_BIND = -3, - CHTTPX_ERR_LISTEN = -4, - CHTTPX_ERR_INVALID_ARGUMENT = -5, - CHTTPX_ERR_LIMIT = -6, - CHTTPX_ERR_IO = -7, - CHTTPX_ERR_NOT_FOUND = -8, - CHTTPX_ERR_PROTOCOL = -9, - CHTTPX_ERR_STATE = -10, - CHTTPX_ERR_UNAVAILABLE = -11, - CHTTPX_ERR_TIMEOUT = -12, - CHTTPX_ERR_TLS = -13, - CHTTPX_ERR_COMPRESSION = -14 + cHTTPX_OK = 0, + cHTTPX_ERR_MEMORY = -1, + cHTTPX_ERR_SOCKET = -2, + cHTTPX_ERR_BIND = -3, + cHTTPX_ERR_LISTEN = -4, + cHTTPX_ERR_INVALID_ARGUMENT = -5, + cHTTPX_ERR_LIMIT = -6, + cHTTPX_ERR_IO = -7, + cHTTPX_ERR_NOT_FOUND = -8, + cHTTPX_ERR_PROTOCOL = -9, + cHTTPX_ERR_STATE = -10, + cHTTPX_ERR_UNAVAILABLE = -11, + cHTTPX_ERR_TIMEOUT = -12, + cHTTPX_ERR_TLS = -13, + cHTTPX_ERR_COMPRESSION = -14 } chttpx_error_t; + /* Backward compatibility for the legacy all-uppercase result names. */ +#ifndef CHTTPX_DISABLE_LEGACY_ERROR_NAMES +#define CHTTPX_OK cHTTPX_OK +#define CHTTPX_ERR_MEMORY cHTTPX_ERR_MEMORY +#define CHTTPX_ERR_SOCKET cHTTPX_ERR_SOCKET +#define CHTTPX_ERR_BIND cHTTPX_ERR_BIND +#define CHTTPX_ERR_LISTEN cHTTPX_ERR_LISTEN +#define CHTTPX_ERR_INVALID_ARGUMENT cHTTPX_ERR_INVALID_ARGUMENT +#define CHTTPX_ERR_LIMIT cHTTPX_ERR_LIMIT +#define CHTTPX_ERR_IO cHTTPX_ERR_IO +#define CHTTPX_ERR_NOT_FOUND cHTTPX_ERR_NOT_FOUND +#define CHTTPX_ERR_PROTOCOL cHTTPX_ERR_PROTOCOL +#define CHTTPX_ERR_STATE cHTTPX_ERR_STATE +#define CHTTPX_ERR_UNAVAILABLE cHTTPX_ERR_UNAVAILABLE +#define CHTTPX_ERR_TIMEOUT cHTTPX_ERR_TIMEOUT +#define CHTTPX_ERR_TLS cHTTPX_ERR_TLS +#define CHTTPX_ERR_COMPRESSION cHTTPX_ERR_COMPRESSION +#endif + + typedef enum { - CHTTPX_LOG_DEBUG, - CHTTPX_LOG_INFO, - CHTTPX_LOG_WARN, - CHTTPX_LOG_ERROR, - CHTTPX_LOG_OFF + cHTTPX_LOG_DEBUG, + cHTTPX_LOG_INFO, + cHTTPX_LOG_WARN, + cHTTPX_LOG_ERROR, + cHTTPX_LOG_OFF } chttpx_log_level_t; +#ifndef CHTTPX_DISABLE_LEGACY_SERVER_ENUM_NAMES +#define CHTTPX_LOG_DEBUG cHTTPX_LOG_DEBUG +#define CHTTPX_LOG_INFO cHTTPX_LOG_INFO +#define CHTTPX_LOG_WARN cHTTPX_LOG_WARN +#define CHTTPX_LOG_ERROR cHTTPX_LOG_ERROR +#define CHTTPX_LOG_OFF cHTTPX_LOG_OFF +#endif + typedef void (*chttpx_logger_fn)(chttpx_log_level_t level, const char* request_id, const char* message, void* user_data); typedef enum { - CHTTPX_NETWORK_IPV4 = 0, - CHTTPX_NETWORK_IPV6, - CHTTPX_NETWORK_DUAL + cHTTPX_NETWORK_IPV4 = 0, + cHTTPX_NETWORK_IPV6, + cHTTPX_NETWORK_DUAL } chttpx_network_mode_t; +#ifndef CHTTPX_DISABLE_LEGACY_SERVER_ENUM_NAMES +#define CHTTPX_NETWORK_IPV4 cHTTPX_NETWORK_IPV4 +#define CHTTPX_NETWORK_IPV6 cHTTPX_NETWORK_IPV6 +#define CHTTPX_NETWORK_DUAL cHTTPX_NETWORK_DUAL +#endif + typedef struct { bool enabled; diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index b024d99..c4b6479 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -22,7 +22,7 @@ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const char* prefix) { - if (!server || !server->logger || server->log_level > CHTTPX_LOG_ERROR) + if (!server || !server->logger || server->log_level > cHTTPX_LOG_ERROR) return; #ifdef CHTTPX_ENABLE_TLS @@ -36,9 +36,9 @@ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const snprintf(message, sizeof(message), "%s: %s", prefix, detail); else snprintf(message, sizeof(message), "%s", prefix); - server->logger(CHTTPX_LOG_ERROR, request_id && *request_id ? request_id : "-", message, server->logger_data); + server->logger(cHTTPX_LOG_ERROR, request_id && *request_id ? request_id : "-", message, server->logger_data); #else - server->logger(CHTTPX_LOG_ERROR, request_id && *request_id ? request_id : "-", prefix, server->logger_data); + server->logger(cHTTPX_LOG_ERROR, request_id && *request_id ? request_id : "-", prefix, server->logger_data); #endif } @@ -65,19 +65,19 @@ static void free_server_tls_strings(chttpx_serv_t* server) int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* config) { if (!server || !config) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (!config->enabled) - return CHTTPX_OK; + return cHTTPX_OK; if (!config->cert_file || !*config->cert_file || !config->key_file || !*config->key_file) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; #ifndef CHTTPX_ENABLE_TLS - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else if (OPENSSL_init_ssl(0, NULL) != 1) - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; server->tls.enabled = true; server->tls.require_client_cert = config->require_client_cert; @@ -89,7 +89,7 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co (config->client_ca_file && !server->tls.client_ca_file)) { free_server_tls_strings(server); - return CHTTPX_ERR_MEMORY; + return cHTTPX_ERR_MEMORY; } SSL_CTX* ctx = SSL_CTX_new(TLS_server_method()); @@ -136,12 +136,12 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co } server->_tls_ctx = ctx; - return CHTTPX_OK; + return cHTTPX_OK; tls_error: _chttpx_tls_log_error(server, "-", "TLS server initialization failed"); free_server_tls_strings(server); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; #endif } @@ -161,76 +161,76 @@ void _chttpx_tls_server_cleanup(chttpx_serv_t* server) int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session) { if (!server || !session) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; *session = NULL; if (!server->tls.enabled) - return CHTTPX_OK; + return cHTTPX_OK; #ifndef CHTTPX_ENABLE_TLS (void)client_fd; - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else SSL* ssl = SSL_new((SSL_CTX*)server->_tls_ctx); if (!ssl) { _chttpx_tls_log_error(server, "-", "TLS session allocation failed"); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } if (SSL_set_fd(ssl, (int)client_fd) != 1 || SSL_accept(ssl) != 1) { _chttpx_tls_log_error(server, "-", "TLS handshake failed"); SSL_free(ssl); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } *session = ssl; - return CHTTPX_OK; + return cHTTPX_OK; #endif } int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session) { if (!server || !session) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; *session = NULL; if (!server->tls.enabled) - return CHTTPX_OK; + return cHTTPX_OK; #ifndef CHTTPX_ENABLE_TLS (void)client_fd; - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else SSL* ssl = SSL_new((SSL_CTX*)server->_tls_ctx); if (!ssl) - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; if (SSL_set_fd(ssl, (int)client_fd) != 1) { SSL_free(ssl); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } SSL_set_accept_state(ssl); *session = ssl; - return CHTTPX_OK; + return cHTTPX_OK; #endif } int _chttpx_tls_accept_step(void* session) { if (!session) - return CHTTPX_OK; + return cHTTPX_OK; #ifndef CHTTPX_ENABLE_TLS - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else int result = SSL_accept((SSL*)session); if (result == 1) - return CHTTPX_OK; + return cHTTPX_OK; int error = SSL_get_error((SSL*)session, result); if (error == SSL_ERROR_WANT_READ) return CHTTPX_IO_WANT_READ; if (error == SSL_ERROR_WANT_WRITE) return CHTTPX_IO_WANT_WRITE; - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; #endif } @@ -252,7 +252,7 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, void** context, void** session) { if (!host || !*host || !context || !session) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; *context = NULL; *session = NULL; @@ -260,16 +260,16 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, #ifndef CHTTPX_ENABLE_TLS (void)socket_fd; (void)config; - return CHTTPX_ERR_UNAVAILABLE; + return cHTTPX_ERR_UNAVAILABLE; #else chttpx_tls_client_config_t selected = config ? *config : (chttpx_tls_client_config_t){.verify_peer = true}; if ((selected.client_cert_file && !selected.client_key_file) || (!selected.client_cert_file && selected.client_key_file)) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); if (!ctx) - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION); @@ -283,7 +283,7 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, if (trust_ok != 1) { SSL_CTX_free(ctx); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } } else @@ -296,7 +296,7 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, SSL_CTX_check_private_key(ctx) != 1) { SSL_CTX_free(ctx); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } } @@ -304,7 +304,7 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, if (!ssl) { SSL_CTX_free(ctx); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } unsigned char ip_buffer[16]; @@ -336,12 +336,12 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, *context = ctx; *session = ssl; - return CHTTPX_OK; + return cHTTPX_OK; error: SSL_free(ssl); SSL_CTX_free(ctx); - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; #endif } @@ -364,7 +364,7 @@ void _chttpx_tls_client_close(void* context, void* session) int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size) { if (!buffer || size == 0) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; size_t wanted = size > INT_MAX ? INT_MAX : size; @@ -384,7 +384,7 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t if (error == SSL_ERROR_SYSCALL && errno == EINTR) continue; #endif - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } } #else @@ -407,7 +407,7 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size) { if (!buffer || size == 0) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; size_t wanted = size > INT_MAX ? INT_MAX : size; #ifdef CHTTPX_ENABLE_TLS if (tls_session) @@ -422,7 +422,7 @@ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buf return CHTTPX_IO_WANT_WRITE; if (error == SSL_ERROR_ZERO_RETURN) return 0; - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } #else (void)tls_session; @@ -442,13 +442,13 @@ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buf if (errno == EINTR) return CHTTPX_IO_WANT_READ; #endif - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) { if (size > 0 && !data) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; if (size == 0) return 0; size_t wanted = size > INT_MAX ? INT_MAX : size; @@ -463,7 +463,7 @@ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const voi return CHTTPX_IO_WANT_READ; if (error == SSL_ERROR_WANT_WRITE) return CHTTPX_IO_WANT_WRITE; - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } #else (void)tls_session; @@ -487,13 +487,13 @@ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const voi if (errno == EINTR) return CHTTPX_IO_WANT_WRITE; #endif - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) { if (size > 0 && !data) - return CHTTPX_ERR_INVALID_ARGUMENT; + return cHTTPX_ERR_INVALID_ARGUMENT; const unsigned char* cursor = data; size_t sent = 0; @@ -527,7 +527,7 @@ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, if (error == SSL_ERROR_SYSCALL && errno == EINTR) continue; #endif - return CHTTPX_ERR_TLS; + return cHTTPX_ERR_TLS; } sent += (size_t)result; continue; @@ -545,12 +545,12 @@ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, if (errno == EINTR) continue; #endif - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; } if (result == 0) - return CHTTPX_ERR_IO; + return cHTTPX_ERR_IO; sent += (size_t)result; } - return CHTTPX_OK; + return cHTTPX_OK; } diff --git a/src/cHTTPX_worker.c b/src/cHTTPX_worker.c new file mode 100644 index 0000000..32b2136 --- /dev/null +++ b/src/cHTTPX_worker.c @@ -0,0 +1,307 @@ +#include "cHTTPX_worker.h" + +#include "cHTTPX_crosspltm.h" +#include "cHTTPX_utils.h" + +#include +#include +#include + +typedef struct +{ + void* data; + uint64_t queued_at_ns; +} chttpx_worker_job_t; + +struct chttpx_worker_pool +{ + thread_t threads[CHTTPX_WORKER_THREADS]; + size_t threads_started; + + chttpx_worker_job_t* queue; + size_t queue_capacity; + size_t queue_head; + size_t queue_tail; + size_t queue_count; + + size_t active_workers; + uint64_t rejected_jobs; + uint64_t completed_jobs; + uint64_t total_queue_wait_ns; + + bool stopping; + chttpx_worker_execute_fn execute; + void* context; + +#ifdef CHTTPX_PLATFORM_WINDOWS + CRITICAL_SECTION mutex; + CONDITION_VARIABLE ready; +#else + pthread_mutex_t mutex; + pthread_cond_t ready; +#endif +}; + +static uint64_t monotonic_ns(void) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + return GetTickCount64() * 1000000ULL; +#else + struct timespec now; + + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) + return 0; + + return (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; +#endif +} + +static void pool_lock(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + EnterCriticalSection(&pool->mutex); +#else + pthread_mutex_lock(&pool->mutex); +#endif +} + +static void pool_unlock(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + LeaveCriticalSection(&pool->mutex); +#else + pthread_mutex_unlock(&pool->mutex); +#endif +} + +static void pool_wait(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + SleepConditionVariableCS(&pool->ready, &pool->mutex, INFINITE); +#else + pthread_cond_wait(&pool->ready, &pool->mutex); +#endif +} + +static void pool_signal(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + WakeConditionVariable(&pool->ready); +#else + pthread_cond_signal(&pool->ready); +#endif +} + +static void pool_broadcast(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + WakeAllConditionVariable(&pool->ready); +#else + pthread_cond_broadcast(&pool->ready); +#endif +} + +static int pool_sync_init(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + InitializeCriticalSection(&pool->mutex); + InitializeConditionVariable(&pool->ready); + return 0; +#else + if (pthread_mutex_init(&pool->mutex, NULL) != 0) + return -1; + + if (pthread_cond_init(&pool->ready, NULL) != 0) + { + pthread_mutex_destroy(&pool->mutex); + return -1; + } + + return 0; +#endif +} + +static void pool_sync_destroy(chttpx_worker_pool_t* pool) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + DeleteCriticalSection(&pool->mutex); +#else + pthread_cond_destroy(&pool->ready); + pthread_mutex_destroy(&pool->mutex); +#endif +} + +static bool worker_take(chttpx_worker_pool_t* pool, chttpx_worker_job_t* job) +{ + pool_lock(pool); + + while (pool->queue_count == 0 && !pool->stopping) + pool_wait(pool); + + if (pool->queue_count == 0 && pool->stopping) + { + pool_unlock(pool); + return false; + } + + *job = pool->queue[pool->queue_head]; + memset(&pool->queue[pool->queue_head], 0, sizeof(pool->queue[pool->queue_head])); + pool->queue_head = (pool->queue_head + 1) % pool->queue_capacity; + pool->queue_count--; + pool->active_workers++; + + uint64_t now = monotonic_ns(); + if (now >= job->queued_at_ns) + pool->total_queue_wait_ns += now - job->queued_at_ns; + + pool_unlock(pool); + return true; +} + +static void* worker_main(void* argument) +{ + chttpx_worker_pool_t* pool = argument; + + for (;;) + { + chttpx_worker_job_t job; + + if (!worker_take(pool, &job)) + break; + + pool->execute(job.data, pool->context); + + pool_lock(pool); + + if (pool->active_workers > 0) + pool->active_workers--; + + pool->completed_jobs++; + pool_unlock(pool); + } + + return NULL; +} + +int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, + size_t queue_capacity, + chttpx_worker_execute_fn execute, + void* context) +{ + if (!pool || !queue_capacity || !execute) + return -1; + + *pool = NULL; + + chttpx_worker_pool_t* created = calloc(1, sizeof(*created)); + if (!created) + return -1; + + created->queue = calloc(queue_capacity, sizeof(*created->queue)); + if (!created->queue) + { + free(created); + return -1; + } + + created->queue_capacity = queue_capacity; + created->execute = execute; + created->context = context; + + if (pool_sync_init(created) != 0) + { + free(created->queue); + free(created); + return -1; + } + + for (size_t i = 0; i < CHTTPX_WORKER_THREADS; i++) + { + if (_thread_create(&created->threads[i], worker_main, created) != 0) + { + _chttpx_worker_stop(created); + + for (size_t j = 0; j < created->threads_started; j++) + _thread_join(created->threads[j]); + + pool_sync_destroy(created); + free(created->queue); + free(created); + return -1; + } + + created->threads_started++; + } + + *pool = created; + return 0; +} + +bool _chttpx_worker_submit(chttpx_worker_pool_t* pool, void* job) +{ + if (!pool || !job) + return false; + + pool_lock(pool); + + if (pool->stopping || pool->queue_count >= pool->queue_capacity) + { + pool->rejected_jobs++; + pool_unlock(pool); + return false; + } + + pool->queue[pool->queue_tail].data = job; + pool->queue[pool->queue_tail].queued_at_ns = monotonic_ns(); + pool->queue_tail = (pool->queue_tail + 1) % pool->queue_capacity; + pool->queue_count++; + + pool_signal(pool); + pool_unlock(pool); + return true; +} + +void _chttpx_worker_stop(chttpx_worker_pool_t* pool) +{ + if (!pool) + return; + + pool_lock(pool); + pool->stopping = true; + pool_broadcast(pool); + pool_unlock(pool); +} + +void _chttpx_worker_pool_destroy(chttpx_worker_pool_t* pool) +{ + if (!pool) + return; + + _chttpx_worker_stop(pool); + + for (size_t i = 0; i < pool->threads_started; i++) + _thread_join(pool->threads[i]); + + pool_sync_destroy(pool); + free(pool->queue); + free(pool); +} + +void _chttpx_worker_stats(chttpx_worker_pool_t* pool, chttpx_worker_stats_t* stats) +{ + if (!stats) + return; + + memset(stats, 0, sizeof(*stats)); + + if (!pool) + return; + + pool_lock(pool); + stats->queue_depth = pool->queue_count; + stats->active_workers = pool->active_workers; + stats->rejected_jobs = pool->rejected_jobs; + stats->completed_jobs = pool->completed_jobs; + stats->total_queue_wait_ns = pool->total_queue_wait_ns; + pool_unlock(pool); +} diff --git a/src/cHTTPX_worker.h b/src/cHTTPX_worker.h new file mode 100644 index 0000000..1de6b79 --- /dev/null +++ b/src/cHTTPX_worker.h @@ -0,0 +1,31 @@ +#ifndef CHTTPX_WORKER_H +#define CHTTPX_WORKER_H + +#include +#include +#include + +#define CHTTPX_WORKER_THREADS 32 + +typedef struct chttpx_worker_pool chttpx_worker_pool_t; +typedef void (*chttpx_worker_execute_fn)(void* job, void* context); + +typedef struct +{ + size_t queue_depth; + size_t active_workers; + uint64_t rejected_jobs; + uint64_t completed_jobs; + uint64_t total_queue_wait_ns; +} chttpx_worker_stats_t; + +int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, + size_t queue_capacity, + chttpx_worker_execute_fn execute, + void* context); +bool _chttpx_worker_submit(chttpx_worker_pool_t* pool, void* job); +void _chttpx_worker_stop(chttpx_worker_pool_t* pool); +void _chttpx_worker_pool_destroy(chttpx_worker_pool_t* pool); +void _chttpx_worker_stats(chttpx_worker_pool_t* pool, chttpx_worker_stats_t* stats); + +#endif diff --git a/tests/fuzz_headers.c b/tests/fuzz_headers.c new file mode 100644 index 0000000..740a750 --- /dev/null +++ b/tests/fuzz_headers.c @@ -0,0 +1,26 @@ +#include "cHTTPX_headers.h" + +#include +#include +#include + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (!data || size == 0 || size > 32768) + return 0; + + char* buffer = malloc(size + 1); + if (!buffer) + return 0; + + memcpy(buffer, data, size); + buffer[size] = '\0'; + + chttpx_request_t request; + memset(&request, 0, sizeof(request)); + + _parse_req_headers(&request, buffer, size); + + free(buffer); + return 0; +} diff --git a/tests/test_compression.c b/tests/test_compression.c index 16876fd..8b8522d 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -115,7 +115,7 @@ static test_response_t exchange(uint16_t port, const char* request) address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); assert(connect(socket_fd, (struct sockaddr*)&address, sizeof(address)) == 0); - assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == CHTTPX_OK); + assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == cHTTPX_OK); #ifdef CHTTPX_PLATFORM_WINDOWS shutdown(socket_fd, SD_SEND); @@ -211,14 +211,14 @@ static int failing_provider(const unsigned char* input, (void)user_data; *output = NULL; *output_size = 0; - return CHTTPX_ERR_COMPRESSION; + return cHTTPX_ERR_COMPRESSION; } int main(void) { fill_bodies(); chttpx_app_t app; - assert(cHTTPX_AppInit(&app) == CHTTPX_OK); + assert(cHTTPX_AppInit(&app) == cHTTPX_OK); chttpx_config_t server_config = cHTTPX_DefaultConfig(); server_config.port = 0; @@ -229,7 +229,7 @@ int main(void) chttpx_compression_config_t compression = cHTTPX_CompressionDefault(); compression.min_size = 128; compression.level = 5; - assert(cHTTPX_CompressionUse(server, &compression) == CHTTPX_OK); + assert(cHTTPX_CompressionUse(server, &compression) == cHTTPX_OK); chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); assert(cHTTPX_Get(&router, "/large", large_handler)); @@ -241,9 +241,9 @@ int main(void) chttpx_route_t* route_disabled = cHTTPX_Get(&router, "/route-disabled", large_handler); assert(route_disabled); - assert(cHTTPX_RouteCompression(route_disabled, false) == CHTTPX_OK); + assert(cHTTPX_RouteCompression(route_disabled, false) == cHTTPX_OK); - assert(cHTTPX_AppStart(&app) == CHTTPX_OK); + assert(cHTTPX_AppStart(&app) == cHTTPX_OK); wait_until_listening(server); test_response_t response = exchange( @@ -326,7 +326,7 @@ int main(void) failing_config.min_size = 128; failing_config.providers = &failing; failing_config.providers_count = 1; - assert(cHTTPX_CompressionUse(server, &failing_config) == CHTTPX_OK); + assert(cHTTPX_CompressionUse(server, &failing_config) == cHTTPX_OK); response = exchange( server->port, diff --git a/tests/test_core.c b/tests/test_core.c index 7f7d7fe..77e7c68 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -89,7 +89,7 @@ static void test_bind_and_json(void) req.body_size = strlen(body); strcpy(req.language, "en"); char* username = NULL; - chttpx_validation_t fields[] = {cHTTPX_StringField("username", &username, true, 3, 32, CHTTPX_TRIM | CHTTPX_LOWERCASE, username_validator)}; + chttpx_validation_t fields[] = {cHTTPX_StringField("username", &username, true, 3, 32, cHTTPX_TRIM | cHTTPX_LOWERCASE, username_validator)}; chttpx_response_t response = {0}; assert(cHTTPX_BindJSON(&req, &response, fields, CHTTPX_ARRAY_LEN(fields))); assert(strcmp(username, "valid_user") == 0); @@ -99,7 +99,7 @@ static void test_bind_and_json(void) assert(cHTTPX_JsonString(object, "message", "quote: \"") == 0); assert(cHTTPX_JsonNumber(object, "id", 42) == 0); response = cHTTPX_ResJsonObject(cHTTPX_StatusOK, object); - assert(response.body_ownership == CHTTPX_BODY_OWNED); + assert(response.body_ownership == cHTTPX_BODY_OWNED); assert(strstr((const char*)response.body, "\\\"") != NULL); cHTTPX_ResponseCleanup(&response); cHTTPX_RequestCleanup(&req); @@ -324,7 +324,7 @@ static void test_routing_api(void) server.initialized = true; chttpx_router_t api = cHTTPX_RoutePathPrefix(&server, "/api/v2"); chttpx_router_t private_routes = cHTTPX_RouteGroup(&api, ""); - assert(cHTTPX_RouterUse(&private_routes, middleware) == CHTTPX_OK); + assert(cHTTPX_RouterUse(&private_routes, middleware) == cHTTPX_OK); chttpx_route_t* route = cHTTPX_Get(&private_routes, "/users/me", handler); assert(route && strcmp(route->path, "/api/v2/users/me") == 0); @@ -339,13 +339,13 @@ static void test_routing_api(void) } assert(strcmp(route->path, "/api/v2/users/me") == 0); - assert(cHTTPX_RouteUseAfter(route, middleware) == CHTTPX_OK); + assert(cHTTPX_RouteUseAfter(route, middleware) == cHTTPX_OK); assert(route->after_middleware_count == 1); char mutable_type[] = "image/*"; const char* allowed_types[] = {mutable_type}; chttpx_upload_policy_t policy = {.max_size = 4096, .allowed_types = allowed_types, .allowed_types_count = 1}; - assert(cHTTPX_RouteUploadPolicy(route, &policy) == CHTTPX_OK); + assert(cHTTPX_RouteUploadPolicy(route, &policy) == cHTTPX_OK); mutable_type[0] = 'v'; assert(strcmp(route->upload_policy.allowed_types[0], "image/*") == 0); diff --git a/tests/test_metrics.c b/tests/test_metrics.c index 5a3ebbe..bb8263f 100644 --- a/tests/test_metrics.c +++ b/tests/test_metrics.c @@ -74,7 +74,7 @@ static http_response_t exchange(uint16_t port, const char* path) "GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", path); assert(length > 0 && (size_t)length < sizeof(request)); - assert(cHTTPX_SendAll(fd, request, (size_t)length) == CHTTPX_OK); + assert(cHTTPX_SendAll(fd, request, (size_t)length) == cHTTPX_OK); shutdown(fd, SHUT_WR); while (response.size + 1 < sizeof(response.bytes)) @@ -115,7 +115,7 @@ static void* snapshot_reader(void* data) while (!__atomic_load_n(&ctx->stop, __ATOMIC_ACQUIRE)) { chttpx_metrics_t metrics; - assert(cHTTPX_ServerMetrics(ctx->server, &metrics) == CHTTPX_OK); + assert(cHTTPX_ServerMetrics(ctx->server, &metrics) == cHTTPX_OK); assert(metrics.requests_in_flight <= metrics.requests_total); } return NULL; @@ -130,25 +130,28 @@ static double diff_seconds(struct timespec start, struct timespec end) int main(void) { chttpx_app_t app; - assert(cHTTPX_AppInit(&app) == CHTTPX_OK); + assert(cHTTPX_AppInit(&app) == cHTTPX_OK); chttpx_config_t disabled_config = cHTTPX_DefaultConfig(); disabled_config.port = 0; - disabled_config.network_mode = CHTTPX_NETWORK_IPV4; + disabled_config.network_mode = cHTTPX_NETWORK_IPV4; chttpx_serv_t* disabled = cHTTPX_AppServer(&app, "metrics-disabled", &disabled_config); assert(disabled); chttpx_metrics_t disabled_metrics; - assert(cHTTPX_ServerMetrics(disabled, &disabled_metrics) == CHTTPX_ERR_UNAVAILABLE); + assert(cHTTPX_ServerMetrics(disabled, &disabled_metrics) == cHTTPX_ERR_UNAVAILABLE); + + chttpx_runtime_metrics_t disabled_runtime_metrics; + assert(cHTTPX_ServerRuntimeMetrics(disabled, &disabled_runtime_metrics) == cHTTPX_OK); chttpx_router_t disabled_router = cHTTPX_RoutePathPrefix(disabled, ""); - assert(cHTTPX_MetricsRoute(&disabled_router, "/metrics") == CHTTPX_ERR_UNAVAILABLE); + assert(cHTTPX_MetricsRoute(&disabled_router, "/metrics") == cHTTPX_ERR_UNAVAILABLE); chttpx_config_t config = cHTTPX_DefaultConfig(); config.port = 0; - config.network_mode = CHTTPX_NETWORK_IPV4; + config.network_mode = cHTTPX_NETWORK_IPV4; config.metrics_enabled = true; chttpx_serv_t* server = cHTTPX_AppServer(&app, "metrics", &config); @@ -156,9 +159,9 @@ int main(void) chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); assert(cHTTPX_Get(&router, "/users/{id}", user_handler)); - assert(cHTTPX_MetricsRoute(&router, "/metrics") == CHTTPX_OK); + assert(cHTTPX_MetricsRoute(&router, "/metrics") == cHTTPX_OK); - assert(cHTTPX_AppStart(&app) == CHTTPX_OK); + assert(cHTTPX_AppStart(&app) == cHTTPX_OK); wait_until_listening(server); snapshot_ctx_t snapshot_ctx = {.server = server, .stop = 0}; @@ -184,7 +187,7 @@ int main(void) (uint64_t)WORKER_COUNT * (uint64_t)REQUESTS_PER_WORKER; chttpx_metrics_t metrics; - assert(cHTTPX_ServerMetrics(server, &metrics) == CHTTPX_OK); + assert(cHTTPX_ServerMetrics(server, &metrics) == cHTTPX_OK); assert(metrics.requests_total == expected_requests); assert(metrics.requests_in_flight == 0); assert(metrics.responses_2xx_total == expected_requests); @@ -194,6 +197,11 @@ int main(void) assert(metrics.connections_accepted_total >= expected_requests); assert(metrics.response_bytes_total == expected_requests * 2); + chttpx_runtime_metrics_t runtime_metrics; + assert(cHTTPX_ServerRuntimeMetrics(server, &runtime_metrics) == cHTTPX_OK); + assert(runtime_metrics.completed_jobs_total >= expected_requests); + assert(runtime_metrics.rejected_jobs_total == 0); + http_response_t scrape = exchange(server->port, "/metrics"); assert(strncmp(scrape.bytes, "HTTP/1.1 200 OK", 15) == 0); @@ -201,6 +209,9 @@ int main(void) assert(strstr(body, "# TYPE libchttpx_requests_total counter")); assert(strstr(body, "libchttpx_route_requests_total{method=\"GET\",route=\"/users/{id}\",class=\"2xx\"}")); assert(strstr(body, "libchttpx_request_duration_seconds_bucket{le=\"+Inf\"}")); + assert(strstr(body, "libchttpx_worker_queue_depth")); + assert(strstr(body, "libchttpx_workers_active")); + assert(strstr(body, "libchttpx_worker_jobs_completed_total")); assert(strstr(body, "/users/0") == NULL); assert(strstr(body, "/users/29") == NULL); @@ -208,7 +219,7 @@ int main(void) struct timespec end; clock_gettime(CLOCK_MONOTONIC, &start); for (int i = 0; i < 100000; i++) - assert(cHTTPX_ServerMetrics(server, &metrics) == CHTTPX_OK); + assert(cHTTPX_ServerMetrics(server, &metrics) == cHTTPX_OK); clock_gettime(CLOCK_MONOTONIC, &end); double ns_per_snapshot = diff_seconds(start, end) * 1e9 / 100000.0; diff --git a/tests/test_server.c b/tests/test_server.c index f39039e..56e3db7 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -52,14 +52,14 @@ static void internal_handler(chttpx_request_t* req, chttpx_response_t* res) static void proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { - if (cHTTPX_Call(req, "internal", cHTTPX_MethodPost, "/process", res) != CHTTPX_OK) + if (cHTTPX_Call(req, "internal", cHTTPX_MethodPost, "/process", res) != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "internal server call failed"); } static void remote_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "remote-payments", cHTTPX_MethodPost, "/process", res); - if (last_remote_call_result != CHTTPX_OK) + if (last_remote_call_result != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "remote server call failed"); } @@ -72,14 +72,14 @@ static void remote_error_handler(chttpx_request_t* req, chttpx_response_t* res) static void remote_error_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "remote-payments", cHTTPX_MethodPost, "/error", res); - if (last_remote_call_result != CHTTPX_OK) + if (last_remote_call_result != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "remote server call failed"); } static void unavailable_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "unavailable", cHTTPX_MethodPost, "/process", res); - if (last_remote_call_result == CHTTPX_ERR_UNAVAILABLE) + if (last_remote_call_result == cHTTPX_ERR_UNAVAILABLE) { *res = cHTTPX_ResError(cHTTPX_StatusServiceUnavailable, "remote server unavailable"); return; @@ -97,7 +97,7 @@ static void custom_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) .content_type = cHTTPX_CTYPE_JSON, }; - if (cHTTPX_CallEx(req, "internal", cHTTPX_MethodPost, "/process", &options, res) != CHTTPX_OK) + if (cHTTPX_CallEx(req, "internal", cHTTPX_MethodPost, "/process", &options, res) != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "custom internal call failed"); } @@ -131,7 +131,7 @@ static void exchange_family(uint16_t port, int family, const char* request, char } assert(connect(socket_fd, (struct sockaddr*)&storage, address_size) == 0); - assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == CHTTPX_OK); + assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == cHTTPX_OK); #ifdef CHTTPX_PLATFORM_WINDOWS shutdown(socket_fd, SD_SEND); @@ -196,10 +196,10 @@ static void wait_until_listening(chttpx_serv_t* server) int main(void) { chttpx_app_t remote_app; - assert(cHTTPX_AppInit(&remote_app) == CHTTPX_OK); + assert(cHTTPX_AppInit(&remote_app) == cHTTPX_OK); chttpx_config_t remote_config = cHTTPX_DefaultConfig(); - assert(remote_config.network_mode == CHTTPX_NETWORK_DUAL); + assert(remote_config.network_mode == cHTTPX_NETWORK_DUAL); remote_config.port = 0; chttpx_serv_t* remote_server = cHTTPX_AppServer(&remote_app, "payments", &remote_config); @@ -209,11 +209,11 @@ int main(void) assert(cHTTPX_Post(&remote_router, "/process", internal_handler)); assert(cHTTPX_Post(&remote_router, "/error", remote_error_handler)); - assert(cHTTPX_AppStart(&remote_app) == CHTTPX_OK); + assert(cHTTPX_AppStart(&remote_app) == cHTTPX_OK); wait_until_listening(remote_server); chttpx_app_t app; - assert(cHTTPX_AppInit(&app) == CHTTPX_OK); + assert(cHTTPX_AppInit(&app) == cHTTPX_OK); char language_en[] = "en"; char language_ru[] = "ru"; @@ -257,13 +257,13 @@ int main(void) char remote_url[128]; snprintf(remote_url, sizeof(remote_url), "http://127.0.0.1:%u", remote_server->port); - assert(cHTTPX_AppRemote(&app, "remote-payments", remote_url) == CHTTPX_OK); - assert(cHTTPX_AppRemote(&app, "unavailable", "http://127.0.0.1:1") == CHTTPX_OK); + assert(cHTTPX_AppRemote(&app, "remote-payments", remote_url) == cHTTPX_OK); + assert(cHTTPX_AppRemote(&app, "unavailable", "http://127.0.0.1:1") == cHTTPX_OK); const char* cors_origins[] = {"https://example.com"}; cHTTPX_Cors(public_api, cors_origins, CHTTPX_ARRAY_LEN(cors_origins), NULL, NULL); - assert(cHTTPX_AppStart(&app) == CHTTPX_OK); + assert(cHTTPX_AppStart(&app) == cHTTPX_OK); wait_until_listening(public_api); wait_until_listening(internal_api); @@ -365,7 +365,7 @@ int main(void) response, sizeof(response)); assert(strstr(response, "HTTP/1.1 200 OK") != NULL); assert(strstr(response, "\"server\":\"internal\"") != NULL); - assert(last_remote_call_result == CHTTPX_OK); + assert(last_remote_call_result == cHTTPX_OK); exchange(public_port, "POST /proxy-remote-error HTTP/1.1\r\n" @@ -374,7 +374,7 @@ int main(void) "\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/1.1 500 Internal Server Error") != NULL); - assert(last_remote_call_result == CHTTPX_OK); + assert(last_remote_call_result == cHTTPX_OK); exchange(public_port, "POST /proxy-unavailable HTTP/1.1\r\n" @@ -383,7 +383,7 @@ int main(void) "\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/1.1 503 Service Unavailable") != NULL); - assert(last_remote_call_result == CHTTPX_ERR_UNAVAILABLE); + assert(last_remote_call_result == cHTTPX_ERR_UNAVAILABLE); exchange(public_port, "POST /proxy-custom HTTP/1.1\r\n" diff --git a/tests/test_tls.c b/tests/test_tls.c index 510ad46..2f3c504 100644 --- a/tests/test_tls.c +++ b/tests/test_tls.c @@ -19,7 +19,7 @@ int main(int argc, char** argv) } chttpx_app_t app; - assert(cHTTPX_AppInit(&app) == CHTTPX_OK); + assert(cHTTPX_AppInit(&app) == cHTTPX_OK); chttpx_config_t config = cHTTPX_DefaultConfig(); config.port = 0; @@ -37,14 +37,14 @@ int main(int argc, char** argv) snprintf(base_url, sizeof(base_url), "https://localhost:%u", server->port); /* HTTPS verifies certificates by default: the self-signed test cert must fail. */ - assert(cHTTPX_AppRemote(&app, "untrusted", base_url) == CHTTPX_OK); + assert(cHTTPX_AppRemote(&app, "untrusted", base_url) == cHTTPX_OK); chttpx_tls_client_config_t tls = cHTTPX_DefaultTLSClientConfig(); assert(tls.verify_peer); tls.ca_file = argv[1]; - assert(cHTTPX_AppRemoteEx(&app, "trusted", base_url, &tls) == CHTTPX_OK); + assert(cHTTPX_AppRemoteEx(&app, "trusted", base_url, &tls) == cHTTPX_OK); - assert(cHTTPX_AppStart(&app) == CHTTPX_OK); + assert(cHTTPX_AppStart(&app) == cHTTPX_OK); chttpx_request_t source = {0}; source._server = server; @@ -57,11 +57,11 @@ int main(int argc, char** argv) }; chttpx_response_t rejected = {0}; - assert(cHTTPX_CallEx(&source, "untrusted", cHTTPX_MethodGet, "/health", &options, &rejected) == CHTTPX_ERR_TLS); + assert(cHTTPX_CallEx(&source, "untrusted", cHTTPX_MethodGet, "/health", &options, &rejected) == cHTTPX_ERR_TLS); cHTTPX_ResponseCleanup(&rejected); chttpx_response_t response = {0}; - assert(cHTTPX_CallEx(&source, "trusted", cHTTPX_MethodGet, "/health", &options, &response) == CHTTPX_OK); + assert(cHTTPX_CallEx(&source, "trusted", cHTTPX_MethodGet, "/health", &options, &response) == cHTTPX_OK); assert(response.status == cHTTPX_StatusOK); assert(response.body != NULL); assert(response.body_size == strlen("{\"secure\":true}")); From 0daf3fd0e86607f2dc39ef5803efd95a5da68a2d Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:14:53 +0500 Subject: [PATCH 03/48] perf: resolve medium and low bottlenecks from #56 (#61) * perf(request): pool cleanup entries and hash named contexts * perf(json): parse request body without full copy * perf(cors): sort origins for logarithmic lookup * perf(cors): use binary search for origin checks * perf(metrics): remove global lock and hash route metrics * test(request): cover pooled cleanup, hashed contexts and bounded JSON * test(cors): cover lookup in unsorted configured origins * test(metrics): exercise route hash table growth * docs(metrics): document parameters and return values * docs(metrics): sync public header comments * docs(cors): document parameters and sorted lookup * docs(cors): document parameters and sorted lookup * docs(request): normalize lifecycle and validation API comments * docs(request): normalize lifecycle and validation API comments * docs(cors): document configuration parameters * fix(perf): use canonical FNV-1a offset basis * fix(perf): use canonical FNV-1a offset basis * fix(metrics): preserve gauge snapshot invariants * docs(request): document bearer and body chunk helpers --- include/libchttpx.h | 166 ++++++++-------- src/cHTTPX_cors.c | 38 ++-- src/cHTTPX_cors.h | 24 +-- src/cHTTPX_metrics.c | 442 ++++++++++++++++++++++++++++++++---------- src/cHTTPX_metrics.h | 28 ++- src/cHTTPX_request.c | 323 +++++++++++++++++++++++++----- src/cHTTPX_request.h | 110 +++++------ src/cHTTPX_response.c | 22 ++- tests/test_core.c | 53 ++++- tests/test_metrics.c | 25 ++- tests/test_server.c | 6 +- 11 files changed, 896 insertions(+), 341 deletions(-) diff --git a/include/libchttpx.h b/include/libchttpx.h index 38be397..4703756 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -580,13 +580,12 @@ extern "C" /** * Allocate zero-initialized memory owned by the current request. * - * The library automatically releases the allocation after - * the request. + * The allocation is released automatically during request cleanup unless + * it is detached with cHTTPX_Detach(). * * @param req Current HTTP request. * @param size Number of bytes to allocate. - * @return Allocated memory, or - * NULL on invalid input or allocation failure. + * @return Request-owned memory or NULL on invalid input/allocation failure. */ void* cHTTPX_Alloc(chttpx_request_t* req, size_t size); @@ -594,123 +593,108 @@ extern "C" * Duplicate a string into request-owned memory. * * @param req Current HTTP request. - * @param str Null-terminated string - * to duplicate. - * @return Request-owned string, or NULL on failure. The caller must not free it. + * @param str Null-terminated source string. + * @return Request-owned copy or NULL on failure. */ char* cHTTPX_Strdup(chttpx_request_t* req, const char* str); /** - * Register an arbitrary resource for cleanup after the request. + * Register an arbitrary resource for automatic request cleanup. * * @param req Current HTTP request. - * @param resource - * Resource passed to cleanup_fn during cleanup. - * @param cleanup_fn Function that releases the resource. - * @return 0 on success, -1 on - * invalid input or allocation failure. + * @param resource Resource passed to cleanup_fn. + * @param cleanup_fn Callback that releases resource. + * @return 0 on success or -1 on invalid input/allocation failure. */ int cHTTPX_Defer(chttpx_request_t* req, void* resource, chttpx_cleanup_fn cleanup_fn); /** * Remove a resource from automatic request cleanup. * - * Ownership is transferred to the caller after a successful detach. - * - - * * @param req Current HTTP request. - * @param resource Previously registered resource. - * @return The detached resource, or NULL when it - * was not registered. + * @param req Current HTTP request. + * @param resource Previously deferred resource. + * @return Detached resource owned by the caller, or NULL when not found. */ void* cHTTPX_Detach(chttpx_request_t* req, void* resource); /** - * Run all registered request cleanup callbacks. + * Run request cleanup callbacks and release internal request-owned state. * - * This is an internal lifecycle function normally called by the server. - * - - * * @param req Request whose resources must be released. + * This is normally invoked by the server lifecycle rather than application + * code. + * + * @param req Request whose scoped resources should be released. */ void cHTTPX_RequestCleanup(chttpx_request_t* req); /** * Store or replace a named request context. * - * The cleanup callback is invoked automatically after the request. Replacing - - * * an existing value also cleans up the previous value. + * Named contexts use a request-local hash table. Replacing a context invokes + * the previous cleanup callback when the previous value differs. * * @param req Current HTTP request. - * @param name Context name. - * + * @param name Context key. * @param value Application value; may be NULL. - * @param cleanup_fn Optional value cleanup callback. - * @return 0 on success, -1 on - * invalid input or allocation failure. + * @param cleanup_fn Optional callback used to release value. + * @return 0 on success or -1 on invalid input/allocation failure. */ int cHTTPX_ContextSet(chttpx_request_t* req, const char* name, void* value, chttpx_context_free_fn cleanup_fn); /** - * Get a named request context. + * Look up a named request context. * * @param req Current HTTP request. - * @param name Context name. - * @return Borrowed - * context value, or NULL when it does not exist. + * @param name Context key. + * @return Borrowed context value or NULL when absent. */ void* cHTTPX_ContextGet(chttpx_request_t* req, const char* name); /** - * Detach a named context from automatic cleanup. + * Detach a named context without running its cleanup callback. * * @param req Current HTTP request. - * @param name Context name. - * - * @return Detached value owned by the caller, or NULL when not found. + * @param name Context key. + * @return Detached value owned by the caller, or NULL when absent. */ void* cHTTPX_ContextDetach(chttpx_request_t* req, const char* name); /** - * Extract a Bearer token from the Authorization header. - * - * The Bearer prefix is matched case-insensitively. - * + * Extract a Bearer token from the Authorization request header. * * @param req Current HTTP request. - * @return Borrowed token pointer, or NULL for a missing or invalid header. + * @return Borrowed token pointer or NULL for a missing/invalid header. */ const char* cHTTPX_BearerToken(chttpx_request_t* req); /** - * Consume the request body through a chunk callback. + * Replay the request body through a bounded chunk callback. * - * Buffered bodies and temporary uploads are replayed in bounded chunks. - - * * * @param req Current HTTP request. * @param callback Function invoked for each body chunk. - * @param user_data Application - * value passed to callback. - * @return 0 on success, -1 on invalid input, I/O error, or callback failure. + * @param user_data Caller value forwarded to callback. + * @return 0 on success or -1 on invalid input, I/O error, or callback failure. */ int cHTTPX_OnBodyChunk(chttpx_request_t* req, chttpx_body_chunk_fn callback, void* user_data); /** - * Parse a JSON body and validate fields according to the provided definitions. - * @param req Pointer to the HTTP request. - * @param fields Array of field validation definitions (cHTTPX_FieldValidation). - * @param field_count Number of fields in the array. - * @return 1 if parsing and validation succeed, 0 if there is an error. - * This function automatically checks required fields, string length, boolean types, etc. + * Parse a JSON body into validation targets. + * + * @param req Current HTTP request. + * @param fields Field definitions and output targets. + * @param field_count Number of entries in fields. + * @return 1 on success or 0 on parse/type/allocation failure. */ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); - /* - * Validates an array of cHTTPX_FieldValidation structures. - * This function ensures that required fields are present, string lengths are within limits, - * and basic validation for integers and boolean fields is performed. + /** + * Validate already parsed field values. + * + * @param req Current HTTP request. + * @param fields Field definitions and parsed targets. + * @param field_count Number of entries in fields. + * @param l Language code used for validation messages. + * @return 1 when all values pass validation, otherwise 0. */ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count, const char* l); @@ -1129,21 +1113,17 @@ extern "C" /** * Enable and configure CORS (Cross-Origin Resource Sharing). * - * This function enables CORS support for the HTTP server and configures - * which origins, HTTP methods, and request headers are allowed. - * - * The CORS configuration is applied globally and is typically used together - * with the built-in CORS middleware. - * - * @param origins Array of allowed origin strings (e.g. "https://example.com"). - * Each origin must match exactly the value of the "Origin" header. - * @param origins_count Number of elements in the origins array. - * @param methods Comma-separated list of allowed HTTP methods. - * If NULL, defaults to: - * "GET, POST, PUT, DELETE, OPTIONS" - * @param headers Comma-separated list of allowed request headers. - * If NULL, defaults to: - * "Content-Type" + * The function copies the supplied configuration. Allowed origins are + * normalized into an immutable sorted table used for logarithmic lookup + * while requests are served. + * + * @param server Initialized HTTP server to configure. + * @param origins Array of exact allowed Origin header values. + * @param origins_count Number of elements in origins. + * @param methods Comma-separated allowed methods, or NULL for the default + * "GET, POST, PUT, DELETE, OPTIONS" set. + * @param headers Comma-separated allowed request headers, or NULL for + * "Content-Type". */ void cHTTPX_Cors(struct chttpx_serv* server, const char** origins, size_t origins_count, const char* methods, const char* headers); @@ -1522,18 +1502,38 @@ extern "C" } chttpx_runtime_metrics_t; - /** Copy a consistent per-server metrics snapshot. */ + /** + * Copy the current server metrics into a caller-owned snapshot. + * + * Global counters are read atomically. Individual fields may advance while + * the snapshot is copied, which is expected for monitoring data. + * + * @param server Server whose metrics should be read. + * @param metrics Output structure populated on success. + * @return cHTTPX_OK on success, cHTTPX_ERR_INVALID_ARGUMENT for invalid + * input, or cHTTPX_ERR_UNAVAILABLE when metrics are disabled. + */ int cHTTPX_ServerMetrics(chttpx_serv_t* server, chttpx_metrics_t* metrics); /** - * Copy a worker-runtime metrics snapshot. + * Copy bounded worker-pool runtime metrics. * - * Returns cHTTPX_OK while the server runtime is active, otherwise - * cHTTPX_ERR_UNAVAILABLE. + * @param server Server whose worker runtime should be inspected. + * @param metrics Output runtime metrics snapshot. + * @return cHTTPX_OK on success or cHTTPX_ERR_UNAVAILABLE when runtime + * metrics are not available. */ int cHTTPX_ServerRuntimeMetrics(chttpx_serv_t* server, chttpx_runtime_metrics_t* metrics); - /** Register a Prometheus text exposition route. */ + /** + * Register a Prometheus text exposition endpoint. + * + * Metrics must be enabled before the server is created. + * + * @param router Router that owns the metrics endpoint. + * @param path Route path used for the Prometheus endpoint. + * @return cHTTPX_OK on success or an appropriate error code. + */ int cHTTPX_MetricsRoute(chttpx_router_t* router, const char* path); #ifdef __cplusplus diff --git a/src/cHTTPX_cors.c b/src/cHTTPX_cors.c index b532faa..37fe8a9 100644 --- a/src/cHTTPX_cors.c +++ b/src/cHTTPX_cors.c @@ -27,25 +27,33 @@ #include #include +#include /** - * Enable and configure CORS (Cross-Origin Resource Sharing). + * Compare two owned CORS origin strings for qsort(). * - * This function enables CORS support for the HTTP server and configures - * which origins, HTTP methods, and request headers are allowed. + * @param left Pointer to the first origin pointer. + * @param right Pointer to the second origin pointer. + * @return Negative, zero, or positive value using strcmp ordering. + */ +static int compare_origins(const void* left, const void* right) +{ + const char* const* lhs = left; + const char* const* rhs = right; + return strcmp(*lhs, *rhs); +} + +/** + * Enable and configure CORS for one initialized HTTP server. * - * The CORS configuration is applied globally and is typically used together - * with the built-in CORS middleware. + * The supplied strings are copied. Origins are sorted once during + * configuration so request-time checks can use binary search. * - * @param origins Array of allowed origin strings (e.g. "https://example.com"). - * Each origin must match exactly the value of the "Origin" header. - * @param origins_count Number of elements in the origins array. - * @param methods Comma-separated list of allowed HTTP methods. - * If NULL, defaults to: - * "GET, POST, PUT, DELETE, OPTIONS" - * @param headers Comma-separated list of allowed request headers. - * If NULL, defaults to: - * "Content-Type" + * @param server Initialized HTTP server to configure. + * @param origins Array of exact allowed Origin header values. + * @param origins_count Number of elements in origins. + * @param methods Comma-separated allowed methods, or NULL for defaults. + * @param headers Comma-separated allowed request headers, or NULL for defaults. */ void cHTTPX_Cors(chttpx_serv_t* server, const char** origins, size_t origins_count, const char* methods, const char* headers) { @@ -72,6 +80,8 @@ void cHTTPX_Cors(chttpx_serv_t* server, const char** origins, size_t origins_cou goto memory_error; } } + if (origins_count > 1) + qsort(owned_origins, origins_count, sizeof(*owned_origins), compare_origins); for (size_t i = 0; i < server->cors.origins_count; i++) free((void*)server->cors.origins[i]); free((void*)server->cors.origins); diff --git a/src/cHTTPX_cors.h b/src/cHTTPX_cors.h index 5a9187f..f868eb2 100644 --- a/src/cHTTPX_cors.h +++ b/src/cHTTPX_cors.h @@ -34,21 +34,17 @@ extern "C" /** * Enable and configure CORS (Cross-Origin Resource Sharing). * - * This function enables CORS support for the HTTP server and configures - * which origins, HTTP methods, and request headers are allowed. + * The function copies the supplied configuration. Allowed origins are + * normalized into an immutable sorted table used for logarithmic lookup + * while requests are served. * - * The CORS configuration is applied globally and is typically used together - * with the built-in CORS middleware. - * - * @param origins Array of allowed origin strings (e.g. "https://example.com"). - * Each origin must match exactly the value of the "Origin" header. - * @param origins_count Number of elements in the origins array. - * @param methods Comma-separated list of allowed HTTP methods. - * If NULL, defaults to: - * "GET, POST, PUT, DELETE, OPTIONS" - * @param headers Comma-separated list of allowed request headers. - * If NULL, defaults to: - * "Content-Type" + * @param server Initialized HTTP server to configure. + * @param origins Array of exact allowed Origin header values. + * @param origins_count Number of elements in origins. + * @param methods Comma-separated allowed methods, or NULL for the default + * "GET, POST, PUT, DELETE, OPTIONS" set. + * @param headers Comma-separated allowed request headers, or NULL for + * "Content-Type". */ void cHTTPX_Cors(struct chttpx_serv* server, const char** origins, size_t origins_count, const char* methods, const char* headers); diff --git a/src/cHTTPX_metrics.c b/src/cHTTPX_metrics.c index 1868342..885df3d 100644 --- a/src/cHTTPX_metrics.c +++ b/src/cHTTPX_metrics.c @@ -33,11 +33,12 @@ typedef struct typedef struct { #ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION mutex; + CRITICAL_SECTION routes_mutex; #else - pthread_mutex_t mutex; + pthread_mutex_t routes_mutex; #endif chttpx_metrics_t metrics; + uint64_t request_duration_nanoseconds_total; chttpx_route_metrics_entry_t* routes; size_t routes_count; size_t routes_capacity; @@ -54,29 +55,91 @@ static const double duration_bounds_seconds[] = { 1.000, }; -static void metrics_lock(chttpx_metrics_state_t* state) +/** + * Lock the route metrics hash table. + * + * Global counters intentionally use relaxed atomics and do not share this + * lock, avoiding one server-wide mutex on every metrics update. + * + * @param state Metrics state containing the route table. + */ +static void routes_lock(chttpx_metrics_state_t* state) { #ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&state->mutex); + EnterCriticalSection(&state->routes_mutex); #else - pthread_mutex_lock(&state->mutex); + pthread_mutex_lock(&state->routes_mutex); #endif } -static void metrics_unlock(chttpx_metrics_state_t* state) +/** + * Unlock the route metrics hash table. + * + * @param state Metrics state containing the route table. + */ +static void routes_unlock(chttpx_metrics_state_t* state) { #ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&state->mutex); + LeaveCriticalSection(&state->routes_mutex); #else - pthread_mutex_unlock(&state->mutex); + pthread_mutex_unlock(&state->routes_mutex); #endif } +/** + * Return the internal metrics state associated with a server. + * + * @param server HTTP server instance. + * @return Internal metrics state or NULL when metrics are disabled. + */ static chttpx_metrics_state_t* metrics_state(chttpx_serv_t* server) { return server ? (chttpx_metrics_state_t*)server->metrics_state : NULL; } +/** + * Load an integral metrics field without taking the route-table lock. + * + * @param value Counter or gauge address. + * @return Current value. + */ +static uint64_t metric_load(const uint64_t* value) +{ + return __atomic_load_n(value, __ATOMIC_RELAXED); +} + +/** + * Add to an integral metrics field without taking a global mutex. + * + * @param value Counter or gauge address. + * @param amount Amount to add. + */ +static void metric_add(uint64_t* value, uint64_t amount) +{ + __atomic_fetch_add(value, amount, __ATOMIC_RELAXED); +} + +/** + * Decrement a gauge only when it is greater than zero. + * + * @param value Gauge address. + */ +static void metric_decrement_nonzero(uint64_t* value) +{ + uint64_t current = __atomic_load_n(value, __ATOMIC_RELAXED); + while (current > 0) + { + if (__atomic_compare_exchange_n(value, ¤t, current - 1, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) + return; + } +} + +/** + * Convert an HTTP status code to the 1xx..5xx metrics bucket index. + * + * @param status HTTP status code. + * @return Bucket index in the range 0..4. + */ static size_t status_class_index(int status) { if (status >= 100 && status < 200) @@ -90,28 +153,103 @@ static size_t status_class_index(int status) return 4; } -static void increment_status_class(chttpx_metrics_t* metrics, int status) +/** + * Increment the response status-class counter. + * + * @param state Metrics state. + * @param status HTTP response status. + */ +static void increment_status_class(chttpx_metrics_state_t* state, int status) { + uint64_t* counter = NULL; switch (status_class_index(status)) { case 0: - metrics->responses_1xx_total++; + counter = &state->metrics.responses_1xx_total; break; case 1: - metrics->responses_2xx_total++; + counter = &state->metrics.responses_2xx_total; break; case 2: - metrics->responses_3xx_total++; + counter = &state->metrics.responses_3xx_total; break; case 3: - metrics->responses_4xx_total++; + counter = &state->metrics.responses_4xx_total; break; default: - metrics->responses_5xx_total++; + counter = &state->metrics.responses_5xx_total; break; } + metric_add(counter, 1); +} + +/** + * Hash a route identity with FNV-1a. + * + * @param method Registered HTTP method. + * @param route Registered route template. + * @return Stable 64-bit hash. + */ +static uint64_t route_metrics_hash(const char* method, const char* route) +{ + uint64_t hash = 14695981039346656037ULL; + const char* values[] = {method, route}; + + for (size_t part = 0; part < CHTTPX_ARRAY_LEN(values); ++part) + { + for (const unsigned char* p = (const unsigned char*)values[part]; *p; ++p) + { + hash ^= (uint64_t)*p; + hash *= 1099511628211ULL; + } + hash ^= 0xffU; + hash *= 1099511628211ULL; + } + + return hash; +} + +/** + * Rebuild the open-addressed route metrics table. + * + * @param state Metrics state. + * @param capacity New power-of-two capacity. + * @return 1 on success, 0 on allocation failure. + */ +static int route_metrics_rehash(chttpx_metrics_state_t* state, size_t capacity) +{ + chttpx_route_metrics_entry_t* routes = calloc(capacity, sizeof(*routes)); + if (!routes) + return 0; + + for (size_t i = 0; i < state->routes_capacity; ++i) + { + chttpx_route_metrics_entry_t entry = state->routes[i]; + if (!entry.method) + continue; + + size_t index = (size_t)(route_metrics_hash(entry.method, entry.route) & (uint64_t)(capacity - 1)); + while (routes[index].method) + index = (index + 1) & (capacity - 1); + routes[index] = entry; + } + + free(state->routes); + state->routes = routes; + state->routes_capacity = capacity; + return 1; } +/** + * Find or create route metrics in average O(1) time. + * + * Caller must hold routes_mutex. + * + * @param state Metrics state. + * @param method Registered HTTP method. + * @param route_template Registered route template. + * @return Route metrics entry or NULL on allocation failure. + */ static chttpx_route_metrics_entry_t* route_metrics_entry(chttpx_metrics_state_t* state, const char* method, const char* route_template) @@ -119,35 +257,80 @@ static chttpx_route_metrics_entry_t* route_metrics_entry(chttpx_metrics_state_t* if (!state || !method || !route_template) return NULL; - for (size_t i = 0; i < state->routes_count; i++) + if (state->routes_capacity == 0) { - chttpx_route_metrics_entry_t* entry = &state->routes[i]; - if (strcmp(entry->method, method) == 0 && strcmp(entry->route, route_template) == 0) - return entry; + if (!route_metrics_rehash(state, 16)) + return NULL; } - - if (state->routes_count == state->routes_capacity) + else if ((state->routes_count + 1) * 10 >= state->routes_capacity * 7) { - size_t capacity = state->routes_capacity ? state->routes_capacity * 2 : 16; - if (capacity < state->routes_capacity || capacity > SIZE_MAX / sizeof(*state->routes)) + if (state->routes_capacity > SIZE_MAX / 2) return NULL; - - chttpx_route_metrics_entry_t* routes = - realloc(state->routes, capacity * sizeof(*state->routes)); - if (!routes) + if (!route_metrics_rehash(state, state->routes_capacity * 2)) return NULL; + } - state->routes = routes; - state->routes_capacity = capacity; + size_t index = (size_t)(route_metrics_hash(method, route_template) & (uint64_t)(state->routes_capacity - 1)); + while (state->routes[index].method) + { + chttpx_route_metrics_entry_t* entry = &state->routes[index]; + if (strcmp(entry->method, method) == 0 && strcmp(entry->route, route_template) == 0) + return entry; + index = (index + 1) & (state->routes_capacity - 1); } - chttpx_route_metrics_entry_t* entry = &state->routes[state->routes_count++]; - memset(entry, 0, sizeof(*entry)); + chttpx_route_metrics_entry_t* entry = &state->routes[index]; entry->method = method; entry->route = route_template; + state->routes_count++; return entry; } +/** + * Copy all atomic global metrics into the public snapshot representation. + * + * @param state Internal metrics state. + * @param metrics Output snapshot. + */ +static void metrics_snapshot(chttpx_metrics_state_t* state, chttpx_metrics_t* metrics) +{ + memset(metrics, 0, sizeof(*metrics)); + + metrics->requests_total = metric_load(&state->metrics.requests_total); + metrics->requests_in_flight = metric_load(&state->metrics.requests_in_flight); + if (metrics->requests_in_flight > metrics->requests_total) + metrics->requests_in_flight = metrics->requests_total; + + metrics->connections_active = metric_load(&state->metrics.connections_active); + metrics->connections_accepted_total = metric_load(&state->metrics.connections_accepted_total); + if (metrics->connections_active > metrics->connections_accepted_total) + metrics->connections_active = metrics->connections_accepted_total; + metrics->connections_rejected_total = metric_load(&state->metrics.connections_rejected_total); + metrics->responses_1xx_total = metric_load(&state->metrics.responses_1xx_total); + metrics->responses_2xx_total = metric_load(&state->metrics.responses_2xx_total); + metrics->responses_3xx_total = metric_load(&state->metrics.responses_3xx_total); + metrics->responses_4xx_total = metric_load(&state->metrics.responses_4xx_total); + metrics->responses_5xx_total = metric_load(&state->metrics.responses_5xx_total); + metrics->request_bytes_total = metric_load(&state->metrics.request_bytes_total); + metrics->response_bytes_total = metric_load(&state->metrics.response_bytes_total); + metrics->parser_failures_total = metric_load(&state->metrics.parser_failures_total); + metrics->timeout_failures_total = metric_load(&state->metrics.timeout_failures_total); + metrics->rate_limit_failures_total = metric_load(&state->metrics.rate_limit_failures_total); + metrics->request_duration_count = metric_load(&state->metrics.request_duration_count); + metrics->request_duration_seconds_sum = + (double)metric_load(&state->request_duration_nanoseconds_total) / 1000000000.0; + + for (size_t i = 0; i < CHTTPX_METRICS_DURATION_BUCKETS; ++i) + metrics->request_duration_buckets[i] = metric_load(&state->metrics.request_duration_buckets[i]); +} + +/** + * Initialize per-server metrics state. + * + * @param server HTTP server instance. + * @param enabled Non-zero when metrics collection is enabled. + * @return cHTTPX_OK on success or an error code. + */ int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) { if (!server) @@ -164,9 +347,9 @@ int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) return cHTTPX_ERR_MEMORY; #ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&state->mutex); + InitializeCriticalSection(&state->routes_mutex); #else - if (pthread_mutex_init(&state->mutex, NULL) != 0) + if (pthread_mutex_init(&state->routes_mutex, NULL) != 0) { free(state); return cHTTPX_ERR_STATE; @@ -177,6 +360,11 @@ int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) return cHTTPX_OK; } +/** + * Release per-server metrics state. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_server_cleanup(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); @@ -184,69 +372,94 @@ void _chttpx_metrics_server_cleanup(chttpx_serv_t* server) return; #ifdef CHTTPX_PLATFORM_WINDOWS - DeleteCriticalSection(&state->mutex); + DeleteCriticalSection(&state->routes_mutex); #else - pthread_mutex_destroy(&state->mutex); + pthread_mutex_destroy(&state->routes_mutex); #endif free(state->routes); free(state); server->metrics_state = NULL; } +/** + * Record an accepted network connection. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_connection_accepted(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.connections_accepted_total++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.connections_accepted_total, 1); } +/** + * Record a rejected network connection. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_connection_rejected(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.connections_rejected_total++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.connections_rejected_total, 1); } +/** + * Record a newly active network connection. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_connection_opened(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.connections_active++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.connections_active, 1); } +/** + * Record a closed network connection. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_connection_closed(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - if (state->metrics.connections_active > 0) - state->metrics.connections_active--; - metrics_unlock(state); + if (state) + metric_decrement_nonzero(&state->metrics.connections_active); } +/** + * Record the start of an HTTP request. + * + * @param server HTTP server instance. + * @param request_bytes Buffered request body size in bytes. + */ void _chttpx_metrics_request_begin(chttpx_serv_t* server, size_t request_bytes) { chttpx_metrics_state_t* state = metrics_state(server); if (!state) return; - metrics_lock(state); - state->metrics.requests_total++; - state->metrics.requests_in_flight++; - state->metrics.request_bytes_total += (uint64_t)request_bytes; - metrics_unlock(state); + metric_add(&state->metrics.requests_total, 1); + metric_add(&state->metrics.requests_in_flight, 1); + metric_add(&state->metrics.request_bytes_total, (uint64_t)request_bytes); } +/** + * Record completion of an HTTP request. + * + * Global counters use relaxed atomics. Only the route hash table takes a + * mutex, so unrelated connection/request counters no longer contend on one + * server-wide lock. + * + * @param server HTTP server instance. + * @param method Registered request method. + * @param route_template Registered route template. + * @param status HTTP response status. + * @param response_bytes Response body size in bytes. + * @param duration_seconds Request duration in seconds. + */ void _chttpx_metrics_request_end(chttpx_serv_t* server, const char* method, const char* route_template, @@ -261,64 +474,82 @@ void _chttpx_metrics_request_end(chttpx_serv_t* server, if (duration_seconds < 0.0) duration_seconds = 0.0; - metrics_lock(state); + metric_decrement_nonzero(&state->metrics.requests_in_flight); + increment_status_class(state, status); + metric_add(&state->metrics.response_bytes_total, (uint64_t)response_bytes); + metric_add(&state->metrics.request_duration_count, 1); - if (state->metrics.requests_in_flight > 0) - state->metrics.requests_in_flight--; - - increment_status_class(&state->metrics, status); - state->metrics.response_bytes_total += (uint64_t)response_bytes; - state->metrics.request_duration_count++; - state->metrics.request_duration_seconds_sum += duration_seconds; + double duration_nanoseconds = duration_seconds * 1000000000.0; + uint64_t duration_ns = duration_nanoseconds >= (double)UINT64_MAX + ? UINT64_MAX + : (uint64_t)duration_nanoseconds; + metric_add(&state->request_duration_nanoseconds_total, duration_ns); for (size_t i = 0; i < CHTTPX_ARRAY_LEN(duration_bounds_seconds); i++) { if (duration_seconds <= duration_bounds_seconds[i]) - state->metrics.request_duration_buckets[i]++; + metric_add(&state->metrics.request_duration_buckets[i], 1); } - state->metrics.request_duration_buckets[CHTTPX_METRICS_DURATION_BUCKETS - 1]++; + metric_add(&state->metrics.request_duration_buckets[CHTTPX_METRICS_DURATION_BUCKETS - 1], 1); - chttpx_route_metrics_entry_t* route_entry = - route_metrics_entry(state, method, route_template); + routes_lock(state); + chttpx_route_metrics_entry_t* route_entry = route_metrics_entry(state, method, route_template); if (route_entry) { route_entry->requests_total++; route_entry->status_class[status_class_index(status)]++; } - - metrics_unlock(state); + routes_unlock(state); } +/** + * Record an HTTP parser failure. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_parser_failure(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.parser_failures_total++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.parser_failures_total, 1); } +/** + * Record a request/connection timeout. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_timeout_failure(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.timeout_failures_total++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.timeout_failures_total, 1); } +/** + * Record a rate-limit rejection. + * + * @param server HTTP server instance. + */ void _chttpx_metrics_rate_limit_failure(chttpx_serv_t* server) { chttpx_metrics_state_t* state = metrics_state(server); - if (!state) - return; - metrics_lock(state); - state->metrics.rate_limit_failures_total++; - metrics_unlock(state); + if (state) + metric_add(&state->metrics.rate_limit_failures_total, 1); } +/** + * Copy a lock-free snapshot of global server metrics. + * + * Individual fields are atomically consistent. As with most monitoring + * snapshots, fields may have advanced independently while the snapshot is + * being copied. + * + * @param server HTTP server instance. + * @param metrics Output snapshot. + * @return cHTTPX_OK on success, cHTTPX_ERR_INVALID_ARGUMENT for bad input, or + * cHTTPX_ERR_UNAVAILABLE when metrics are disabled. + */ int cHTTPX_ServerMetrics(chttpx_serv_t* server, chttpx_metrics_t* metrics) { if (!server || !metrics) @@ -328,13 +559,17 @@ int cHTTPX_ServerMetrics(chttpx_serv_t* server, chttpx_metrics_t* metrics) if (!state) return cHTTPX_ERR_UNAVAILABLE; - metrics_lock(state); - *metrics = state->metrics; - metrics_unlock(state); + metrics_snapshot(state, metrics); return cHTTPX_OK; } - +/** + * Copy the current bounded worker-pool metrics. + * + * @param server HTTP server instance. + * @param metrics Output runtime metrics snapshot. + * @return cHTTPX_OK on success or an error code when runtime metrics are unavailable. + */ int cHTTPX_ServerRuntimeMetrics(chttpx_serv_t* server, chttpx_runtime_metrics_t* metrics) { if (!server || !metrics) @@ -575,9 +810,12 @@ static int prometheus_route_metrics(prometheus_buffer_t* buffer, "# TYPE libchttpx_route_requests_total counter\n")) return 0; - for (size_t i = 0; i < state->routes_count; i++) + for (size_t i = 0; i < state->routes_capacity; i++) { chttpx_route_metrics_entry_t* entry = &state->routes[i]; + if (!entry->method) + continue; + for (size_t status = 0; status < 5; status++) { if (entry->status_class[status] == 0) @@ -613,12 +851,18 @@ static void metrics_handler(chttpx_request_t* req, chttpx_response_t* res) chttpx_runtime_metrics_t runtime_snapshot = {0}; bool have_runtime_metrics = cHTTPX_ServerRuntimeMetrics(server, &runtime_snapshot) == cHTTPX_OK; - metrics_lock(state); - chttpx_metrics_t snapshot = state->metrics; + chttpx_metrics_t snapshot; + metrics_snapshot(state, &snapshot); + int ok = prometheus_global_metrics(&output, &snapshot) && - (!have_runtime_metrics || prometheus_runtime_metrics(&output, &runtime_snapshot)) && - prometheus_route_metrics(&output, state); - metrics_unlock(state); + (!have_runtime_metrics || prometheus_runtime_metrics(&output, &runtime_snapshot)); + + if (ok) + { + routes_lock(state); + ok = prometheus_route_metrics(&output, state); + routes_unlock(state); + } if (!ok) { diff --git a/src/cHTTPX_metrics.h b/src/cHTTPX_metrics.h index 18a64e2..8c26d4a 100644 --- a/src/cHTTPX_metrics.h +++ b/src/cHTTPX_metrics.h @@ -72,26 +72,38 @@ extern "C" /** - * Copy a consistent metrics snapshot. + * Copy the current server metrics into a caller-owned snapshot. * - * Returns cHTTPX_OK on success or cHTTPX_ERR_UNAVAILABLE when metrics are - * disabled for the server. + * Global counters are read atomically. Individual fields may advance while + * the snapshot is copied, which is expected for monitoring data. + * + * @param server Server whose metrics should be read. + * @param metrics Output structure populated on success. + * @return cHTTPX_OK on success, cHTTPX_ERR_INVALID_ARGUMENT for invalid + * input, or cHTTPX_ERR_UNAVAILABLE when metrics are disabled. */ int cHTTPX_ServerMetrics(struct chttpx_serv* server, chttpx_metrics_t* metrics); /** - * Copy a worker-runtime metrics snapshot. + * Copy bounded worker-pool runtime metrics. * - * Returns cHTTPX_OK while the server runtime is active, otherwise - * cHTTPX_ERR_UNAVAILABLE. + * @param server Server whose worker runtime should be inspected. + * @param metrics Output runtime metrics snapshot. + * @return cHTTPX_OK on success or cHTTPX_ERR_UNAVAILABLE when runtime + * metrics are not available. */ int cHTTPX_ServerRuntimeMetrics(struct chttpx_serv* server, chttpx_runtime_metrics_t* metrics); - /** - * Register a Prometheus text exposition endpoint on the supplied router. + * Register a Prometheus text exposition endpoint. * * Metrics must be enabled in chttpx_config_t before the server is created. + * + * @param router Router that owns the metrics endpoint. + * @param path Route path used for the Prometheus endpoint. + * @return cHTTPX_OK on success, cHTTPX_ERR_INVALID_ARGUMENT for invalid + * input, cHTTPX_ERR_UNAVAILABLE when metrics are disabled, or + * cHTTPX_ERR_MEMORY when route registration fails. */ int cHTTPX_MetricsRoute(struct chttpx_router* router, const char* path); diff --git a/src/cHTTPX_request.c b/src/cHTTPX_request.c index 66f0883..b1b3cef 100644 --- a/src/cHTTPX_request.c +++ b/src/cHTTPX_request.c @@ -37,6 +37,9 @@ #include #include +#define CHTTPX_CLEANUP_BLOCK_CAPACITY 32 +#define CHTTPX_CONTEXT_BUCKET_COUNT 32 + typedef struct chttpx_cleanup_entry { void* resource; @@ -44,14 +47,134 @@ typedef struct chttpx_cleanup_entry struct chttpx_cleanup_entry* next; } chttpx_cleanup_entry_t; +typedef struct chttpx_cleanup_block +{ + size_t used; + chttpx_cleanup_entry_t entries[CHTTPX_CLEANUP_BLOCK_CAPACITY]; + struct chttpx_cleanup_block* next; +} chttpx_cleanup_block_t; + +typedef struct +{ + chttpx_cleanup_entry_t* head; + chttpx_cleanup_block_t first_block; + chttpx_cleanup_block_t* extra_blocks; +} chttpx_cleanup_state_t; + typedef struct chttpx_context_entry { - char* name; void* value; chttpx_context_free_fn cleanup_fn; struct chttpx_context_entry* next; + char name[]; } chttpx_context_entry_t; +typedef struct +{ + chttpx_context_entry_t* buckets[CHTTPX_CONTEXT_BUCKET_COUNT]; +} chttpx_context_table_t; + +/** + * Hash a short request-scoped lookup key with FNV-1a. + * + * @param value Null-terminated key. + * @return Stable 64-bit hash value. + */ +static uint64_t request_hash_string(const char* value) +{ + uint64_t hash = 14695981039346656037ULL; + for (const unsigned char* p = (const unsigned char*)value; *p; ++p) + { + hash ^= (uint64_t)*p; + hash *= 1099511628211ULL; + } + return hash; +} + +/** + * Return the request cleanup state, creating it on first use when requested. + * + * @param req Current request. + * @param create Non-zero to allocate missing state. + * @return Cleanup state or NULL. + */ +static chttpx_cleanup_state_t* cleanup_state(chttpx_request_t* req, int create) +{ + if (!req) + return NULL; + + chttpx_cleanup_state_t* state = req->_cleanup_entries; + if (!state && create) + { + state = calloc(1, sizeof(*state)); + if (!state) + return NULL; + req->_cleanup_entries = state; + } + return state; +} + +/** + * Allocate one cleanup entry from request-local blocks. + * + * Entries are pooled in groups to avoid one heap allocation per deferred + * resource while preserving cHTTPX_Detach semantics for the resource itself. + * + * @param state Cleanup state owned by the request. + * @return Reusable cleanup entry or NULL on allocation failure. + */ +static chttpx_cleanup_entry_t* cleanup_entry_alloc(chttpx_cleanup_state_t* state) +{ + if (!state) + return NULL; + + if (state->first_block.used < CHTTPX_CLEANUP_BLOCK_CAPACITY) + return &state->first_block.entries[state->first_block.used++]; + + chttpx_cleanup_block_t* block = state->extra_blocks; + if (!block || block->used == CHTTPX_CLEANUP_BLOCK_CAPACITY) + { + chttpx_cleanup_block_t* created = calloc(1, sizeof(*created)); + if (!created) + return NULL; + created->next = state->extra_blocks; + state->extra_blocks = created; + block = created; + } + + return &block->entries[block->used++]; +} + +/** + * Return the named-context hash table, creating it on first use when requested. + * + * @param req Current request. + * @param create Non-zero to allocate the table when absent. + * @return Context table or NULL. + */ +static chttpx_context_table_t* context_table(chttpx_request_t* req, int create) +{ + if (!req) + return NULL; + + chttpx_context_table_t* table = req->_contexts; + if (!table && create) + { + table = calloc(1, sizeof(*table)); + if (!table) + return NULL; + req->_contexts = table; + } + return table; +} + +/** + * Allocate zero-initialized memory owned by the current request. + * + * @param req Current HTTP request. + * @param size Number of bytes to allocate. + * @return Request-owned memory or NULL on failure. + */ void* cHTTPX_Alloc(chttpx_request_t* req, size_t size) { if (!req || size == 0) @@ -70,6 +193,13 @@ void* cHTTPX_Alloc(chttpx_request_t* req, size_t size) return memory; } +/** + * Duplicate a string into request-owned memory. + * + * @param req Current HTTP request. + * @param str Null-terminated source string. + * @return Request-owned copy or NULL on failure. + */ char* cHTTPX_Strdup(chttpx_request_t* req, const char* str) { if (!str) @@ -82,35 +212,57 @@ char* cHTTPX_Strdup(chttpx_request_t* req, const char* str) return copy; } +/** + * Register an arbitrary resource for cleanup at the end of the request. + * + * @param req Current HTTP request. + * @param resource Resource passed to cleanup_fn. + * @param cleanup_fn Function that releases resource. + * @return 0 on success, -1 on invalid input or allocation failure. + */ int cHTTPX_Defer(chttpx_request_t* req, void* resource, chttpx_cleanup_fn cleanup_fn) { if (!req || !resource || !cleanup_fn) return -1; - chttpx_cleanup_entry_t* entry = malloc(sizeof(*entry)); + chttpx_cleanup_state_t* state = cleanup_state(req, 1); + if (!state) + return -1; + + chttpx_cleanup_entry_t* entry = cleanup_entry_alloc(state); if (!entry) return -1; entry->resource = resource; entry->cleanup_fn = cleanup_fn; - entry->next = req->_cleanup_entries; - req->_cleanup_entries = entry; + entry->next = state->head; + state->head = entry; return 0; } +/** + * Remove a resource from automatic request cleanup. + * + * @param req Current HTTP request. + * @param resource Previously deferred resource. + * @return Detached resource or NULL when not registered. + */ void* cHTTPX_Detach(chttpx_request_t* req, void* resource) { - if (!req || !resource) + chttpx_cleanup_state_t* state = cleanup_state(req, 0); + if (!state || !resource) return NULL; - chttpx_cleanup_entry_t** current = (chttpx_cleanup_entry_t**)&req->_cleanup_entries; + chttpx_cleanup_entry_t** current = &state->head; while (*current) { if ((*current)->resource == resource) { chttpx_cleanup_entry_t* detached = *current; *current = detached->next; - free(detached); + detached->resource = NULL; + detached->cleanup_fn = NULL; + detached->next = NULL; return resource; } current = &(*current)->next; @@ -119,12 +271,29 @@ void* cHTTPX_Detach(chttpx_request_t* req, void* resource) return NULL; } +/** + * Store or replace a named request context. + * + * Contexts are indexed through a small request-local hash table so lookup does + * not grow linearly with the number of named contexts. + * + * @param req Current HTTP request. + * @param name Context name. + * @param value Application-owned value. + * @param cleanup_fn Optional callback invoked when the context is replaced or cleaned up. + * @return 0 on success, -1 on invalid input or allocation failure. + */ int cHTTPX_ContextSet(chttpx_request_t* req, const char* name, void* value, chttpx_context_free_fn cleanup_fn) { if (!req || !name || !*name) return -1; - chttpx_context_entry_t* entry = req->_contexts; + chttpx_context_table_t* table = context_table(req, 1); + if (!table) + return -1; + + size_t bucket = (size_t)(request_hash_string(name) % CHTTPX_CONTEXT_BUCKET_COUNT); + chttpx_context_entry_t* entry = table->buckets[bucket]; while (entry) { if (strcmp(entry->name, name) == 0) @@ -138,41 +307,58 @@ int cHTTPX_ContextSet(chttpx_request_t* req, const char* name, void* value, chtt entry = entry->next; } - entry = calloc(1, sizeof(*entry)); - if (!entry) + size_t name_size = strlen(name) + 1; + if (name_size > SIZE_MAX - sizeof(*entry)) return -1; - entry->name = strdup(name); - if (!entry->name) - { - free(entry); + + entry = malloc(sizeof(*entry) + name_size); + if (!entry) return -1; - } + entry->value = value; entry->cleanup_fn = cleanup_fn; - entry->next = req->_contexts; - req->_contexts = entry; + entry->next = table->buckets[bucket]; + memcpy(entry->name, name, name_size); + table->buckets[bucket] = entry; return 0; } +/** + * Look up a named request context. + * + * @param req Current HTTP request. + * @param name Context name. + * @return Borrowed context value or NULL when absent. + */ void* cHTTPX_ContextGet(chttpx_request_t* req, const char* name) { - if (!req || !name) + chttpx_context_table_t* table = context_table(req, 0); + if (!table || !name) return NULL; - chttpx_context_entry_t* entry = req->_contexts; - while (entry) - { + + size_t bucket = (size_t)(request_hash_string(name) % CHTTPX_CONTEXT_BUCKET_COUNT); + for (chttpx_context_entry_t* entry = table->buckets[bucket]; entry; entry = entry->next) if (strcmp(entry->name, name) == 0) return entry->value; - entry = entry->next; - } + return NULL; } +/** + * Detach a named request context without running its cleanup callback. + * + * @param req Current HTTP request. + * @param name Context name. + * @return Detached value owned by the caller or NULL when absent. + */ void* cHTTPX_ContextDetach(chttpx_request_t* req, const char* name) { - if (!req || !name) + chttpx_context_table_t* table = context_table(req, 0); + if (!table || !name) return NULL; - chttpx_context_entry_t** current = (chttpx_context_entry_t**)&req->_contexts; + + size_t bucket = (size_t)(request_hash_string(name) % CHTTPX_CONTEXT_BUCKET_COUNT); + chttpx_context_entry_t** current = &table->buckets[bucket]; while (*current) { if (strcmp((*current)->name, name) == 0) @@ -180,7 +366,6 @@ void* cHTTPX_ContextDetach(chttpx_request_t* req, const char* name) chttpx_context_entry_t* detached = *current; void* value = detached->value; *current = detached->next; - free(detached->name); free(detached); return value; } @@ -189,22 +374,34 @@ void* cHTTPX_ContextDetach(chttpx_request_t* req, const char* name) return NULL; } +/** + * Run all request-owned cleanup callbacks and release internal lookup state. + * + * @param req Request whose scoped resources must be released. + */ void cHTTPX_RequestCleanup(chttpx_request_t* req) { if (!req) return; - chttpx_context_entry_t* context = req->_contexts; - while (context) + chttpx_context_table_t* table = context_table(req, 0); + if (table) { - chttpx_context_entry_t* next_context = context->next; - if (context->cleanup_fn && context->value) - context->cleanup_fn(context->value); - free(context->name); - free(context); - context = next_context; + for (size_t bucket = 0; bucket < CHTTPX_CONTEXT_BUCKET_COUNT; ++bucket) + { + chttpx_context_entry_t* entry = table->buckets[bucket]; + while (entry) + { + chttpx_context_entry_t* next = entry->next; + if (entry->cleanup_fn && entry->value) + entry->cleanup_fn(entry->value); + free(entry); + entry = next; + } + } + free(table); + req->_contexts = NULL; } - req->_contexts = NULL; if (req->context) { @@ -214,17 +411,37 @@ void cHTTPX_RequestCleanup(chttpx_request_t* req) req->context_free = NULL; } - chttpx_cleanup_entry_t* cleanup = req->_cleanup_entries; - while (cleanup) + chttpx_cleanup_state_t* state = cleanup_state(req, 0); + if (state) { - chttpx_cleanup_entry_t* next_cleanup = cleanup->next; - cleanup->cleanup_fn(cleanup->resource); - free(cleanup); - cleanup = next_cleanup; + chttpx_cleanup_entry_t* entry = state->head; + while (entry) + { + chttpx_cleanup_entry_t* next = entry->next; + if (entry->cleanup_fn && entry->resource) + entry->cleanup_fn(entry->resource); + entry = next; + } + + chttpx_cleanup_block_t* block = state->extra_blocks; + while (block) + { + chttpx_cleanup_block_t* next = block->next; + free(block); + block = next; + } + + free(state); + req->_cleanup_entries = NULL; } - req->_cleanup_entries = NULL; } +/** + * Extract a Bearer token from the Authorization request header. + * + * @param req Current HTTP request. + * @return Borrowed token pointer or NULL when the header is absent/invalid. + */ const char* cHTTPX_BearerToken(chttpx_request_t* req) { const char* authorization = cHTTPX_HeaderGet(req, "Authorization"); @@ -233,6 +450,14 @@ const char* cHTTPX_BearerToken(chttpx_request_t* req) return authorization + 7; } +/** + * Replay the request body through a bounded chunk callback. + * + * @param req Current HTTP request. + * @param callback Function invoked for each body chunk. + * @param user_data Caller value forwarded to callback. + * @return 0 on success or -1 on invalid input, I/O error, or callback failure. + */ int cHTTPX_OnBodyChunk(chttpx_request_t* req, chttpx_body_chunk_fn callback, void* user_data) { if (!req || !callback) @@ -308,15 +533,11 @@ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t fiel if (!req || !fields || !req->body) return 0; - char* body = malloc(req->body_size + 1); - if (!body) - return 0; - - memcpy(body, (const void*)req->body, req->body_size); - body[req->body_size] = '\0'; - - cJSON* json = cJSON_Parse(body); - free(body); + /* + * cJSON can parse a bounded byte range directly. Avoid duplicating the + * complete request body only to append a temporary NUL terminator. + */ + cJSON* json = cJSON_ParseWithLengthOpts((const char*)req->body, req->body_size, NULL, false); if (!json) { diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h index add942c..f6b1400 100644 --- a/src/cHTTPX_request.h +++ b/src/cHTTPX_request.h @@ -256,13 +256,12 @@ extern "C" /** * Allocate zero-initialized memory owned by the current request. * - * The library automatically releases the allocation after - * the request. + * The allocation is released automatically during request cleanup unless + * it is detached with cHTTPX_Detach(). * * @param req Current HTTP request. * @param size Number of bytes to allocate. - * @return Allocated memory, or - * NULL on invalid input or allocation failure. + * @return Request-owned memory or NULL on invalid input/allocation failure. */ void* cHTTPX_Alloc(chttpx_request_t* req, size_t size); @@ -270,123 +269,108 @@ extern "C" * Duplicate a string into request-owned memory. * * @param req Current HTTP request. - * @param str Null-terminated string - * to duplicate. - * @return Request-owned string, or NULL on failure. The caller must not free it. + * @param str Null-terminated source string. + * @return Request-owned copy or NULL on failure. */ char* cHTTPX_Strdup(chttpx_request_t* req, const char* str); /** - * Register an arbitrary resource for cleanup after the request. + * Register an arbitrary resource for automatic request cleanup. * * @param req Current HTTP request. - * @param resource - * Resource passed to cleanup_fn during cleanup. - * @param cleanup_fn Function that releases the resource. - * @return 0 on success, -1 on - * invalid input or allocation failure. + * @param resource Resource passed to cleanup_fn. + * @param cleanup_fn Callback that releases resource. + * @return 0 on success or -1 on invalid input/allocation failure. */ int cHTTPX_Defer(chttpx_request_t* req, void* resource, chttpx_cleanup_fn cleanup_fn); /** * Remove a resource from automatic request cleanup. * - * Ownership is transferred to the caller after a successful detach. - * - - * * @param req Current HTTP request. - * @param resource Previously registered resource. - * @return The detached resource, or NULL when it - * was not registered. + * @param req Current HTTP request. + * @param resource Previously deferred resource. + * @return Detached resource owned by the caller, or NULL when not found. */ void* cHTTPX_Detach(chttpx_request_t* req, void* resource); /** - * Run all registered request cleanup callbacks. + * Run request cleanup callbacks and release internal request-owned state. * - * This is an internal lifecycle function normally called by the server. - * - - * * @param req Request whose resources must be released. + * This is normally invoked by the server lifecycle rather than application + * code. + * + * @param req Request whose scoped resources should be released. */ void cHTTPX_RequestCleanup(chttpx_request_t* req); /** * Store or replace a named request context. * - * The cleanup callback is invoked automatically after the request. Replacing - - * * an existing value also cleans up the previous value. + * Named contexts use a request-local hash table. Replacing a context invokes + * the previous cleanup callback when the previous value differs. * * @param req Current HTTP request. - * @param name Context name. - * + * @param name Context key. * @param value Application value; may be NULL. - * @param cleanup_fn Optional value cleanup callback. - * @return 0 on success, -1 on - * invalid input or allocation failure. + * @param cleanup_fn Optional callback used to release value. + * @return 0 on success or -1 on invalid input/allocation failure. */ int cHTTPX_ContextSet(chttpx_request_t* req, const char* name, void* value, chttpx_context_free_fn cleanup_fn); /** - * Get a named request context. + * Look up a named request context. * * @param req Current HTTP request. - * @param name Context name. - * @return Borrowed - * context value, or NULL when it does not exist. + * @param name Context key. + * @return Borrowed context value or NULL when absent. */ void* cHTTPX_ContextGet(chttpx_request_t* req, const char* name); /** - * Detach a named context from automatic cleanup. + * Detach a named context without running its cleanup callback. * * @param req Current HTTP request. - * @param name Context name. - * - * @return Detached value owned by the caller, or NULL when not found. + * @param name Context key. + * @return Detached value owned by the caller, or NULL when absent. */ void* cHTTPX_ContextDetach(chttpx_request_t* req, const char* name); /** - * Extract a Bearer token from the Authorization header. - * - * The Bearer prefix is matched case-insensitively. - * + * Extract a Bearer token from the Authorization request header. * * @param req Current HTTP request. - * @return Borrowed token pointer, or NULL for a missing or invalid header. + * @return Borrowed token pointer or NULL for a missing/invalid header. */ const char* cHTTPX_BearerToken(chttpx_request_t* req); /** - * Consume the request body through a chunk callback. + * Replay the request body through a bounded chunk callback. * - * Buffered bodies and temporary uploads are replayed in bounded chunks. - - * * * @param req Current HTTP request. * @param callback Function invoked for each body chunk. - * @param user_data Application - * value passed to callback. - * @return 0 on success, -1 on invalid input, I/O error, or callback failure. + * @param user_data Caller value forwarded to callback. + * @return 0 on success or -1 on invalid input, I/O error, or callback failure. */ int cHTTPX_OnBodyChunk(chttpx_request_t* req, chttpx_body_chunk_fn callback, void* user_data); /** - * Parse a JSON body and validate fields according to the provided definitions. - * @param req Pointer to the HTTP request. - * @param fields Array of field validation definitions (cHTTPX_FieldValidation). - * @param field_count Number of fields in the array. - * @return 1 if parsing and validation succeed, 0 if there is an error. - * This function automatically checks required fields, string length, boolean types, etc. + * Parse a JSON body into validation targets. + * + * @param req Current HTTP request. + * @param fields Field definitions and output targets. + * @param field_count Number of entries in fields. + * @return 1 on success or 0 on parse/type/allocation failure. */ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); - /* - * Validates an array of cHTTPX_FieldValidation structures. - * This function ensures that required fields are present, string lengths are within limits, - * and basic validation for integers and boolean fields is performed. + /** + * Validate already parsed field values. + * + * @param req Current HTTP request. + * @param fields Field definitions and parsed targets. + * @param field_count Number of entries in fields. + * @param l Language code used for validation messages. + * @return 1 when all values pass validation, otherwise 0. */ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count, const char* l); diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index c448da0..bd79915 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -362,16 +362,30 @@ static void set_client_timeout(chttpx_serv_t* server, chttpx_socket_t client_fd) #endif } -/* Cors */ +/** + * Find an allowed CORS origin in the sorted server origin table. + * + * @param server HTTP server containing the immutable CORS configuration. + * @param req_origin Origin header value supplied by the client. + * @return Borrowed configured origin string or NULL when it is not allowed. + */ static const char* allowed_origin_cors(chttpx_serv_t* server, const char* req_origin) { if (!server || !server->cors.enabled || !req_origin) return NULL; - for (size_t i = 0; i < server->cors.origins_count; i++) + size_t left = 0; + size_t right = server->cors.origins_count; + while (left < right) { - if (strcmp(server->cors.origins[i], req_origin) == 0) - return server->cors.origins[i]; + size_t middle = left + (right - left) / 2; + int order = strcmp(server->cors.origins[middle], req_origin); + if (order == 0) + return server->cors.origins[middle]; + if (order < 0) + left = middle + 1; + else + right = middle; } return NULL; diff --git a/tests/test_core.c b/tests/test_core.c index 77e7c68..8d1724e 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -37,6 +37,7 @@ static void handler(chttpx_request_t* req, chttpx_response_t* res) static void test_request_lifecycle(void) { + cleanup_calls = 0; chttpx_request_t req = {0}; char* text = cHTTPX_Strdup(&req, "owned"); assert(text && strcmp(text, "owned") == 0); @@ -54,6 +55,47 @@ static void test_request_lifecycle(void) assert(cleanup_calls == 5); } +static void test_request_internal_indexes(void) +{ + chttpx_request_t req = {0}; + + for (size_t i = 0; i < 80; ++i) + { + char name[32]; + snprintf(name, sizeof(name), "ctx-%zu", i); + + int* value = malloc(sizeof(*value)); + assert(value); + *value = (int)i; + + assert(cHTTPX_ContextSet(&req, name, value, free) == 0); + } + + for (size_t i = 0; i < 80; ++i) + { + char name[32]; + snprintf(name, sizeof(name), "ctx-%zu", i); + + int* value = cHTTPX_ContextGet(&req, name); + assert(value && *value == (int)i); + } + + int* detached = cHTTPX_ContextDetach(&req, "ctx-40"); + assert(detached && *detached == 40); + assert(cHTTPX_ContextGet(&req, "ctx-40") == NULL); + free(detached); + + for (size_t i = 0; i < 96; ++i) + { + int* value = malloc(sizeof(*value)); + assert(value); + *value = (int)i; + assert(cHTTPX_Defer(&req, value, free) == 0); + } + + cHTTPX_RequestCleanup(&req); +} + static void test_typed_values(void) { chttpx_request_t req = {0}; @@ -84,9 +126,11 @@ static void test_typed_values(void) static void test_bind_and_json(void) { chttpx_request_t req = {0}; - const char body[] = "{\"username\":\" VALID_USER \"}"; - req.body = (unsigned char*)body; - req.body_size = strlen(body); + const char source[] = "{\"username\":\" VALID_USER \"}"; + req.body_size = strlen(source); + req.body = malloc(req.body_size); + assert(req.body); + memcpy(req.body, source, req.body_size); strcpy(req.language, "en"); char* username = NULL; chttpx_validation_t fields[] = {cHTTPX_StringField("username", &username, true, 3, 32, cHTTPX_TRIM | cHTTPX_LOWERCASE, username_validator)}; @@ -102,6 +146,8 @@ static void test_bind_and_json(void) assert(response.body_ownership == cHTTPX_BODY_OWNED); assert(strstr((const char*)response.body, "\\\"") != NULL); cHTTPX_ResponseCleanup(&response); + free(req.body); + req.body = NULL; cHTTPX_RequestCleanup(&req); } @@ -369,6 +415,7 @@ static void test_helpers(void) int main(void) { test_request_lifecycle(); + test_request_internal_indexes(); test_typed_values(); test_bind_and_json(); test_multipart(); diff --git a/tests/test_metrics.c b/tests/test_metrics.c index bb8263f..9f7a552 100644 --- a/tests/test_metrics.c +++ b/tests/test_metrics.c @@ -10,6 +10,7 @@ #define WORKER_COUNT 6 #define REQUESTS_PER_WORKER 30 #define RESPONSE_CAPACITY 65536 +#define EXTRA_ROUTE_COUNT 24 typedef struct { @@ -38,6 +39,13 @@ static void user_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "text/plain", body, sizeof(body) - 1); } +static void static_handler(chttpx_request_t* req, chttpx_response_t* res) +{ + (void)req; + static const unsigned char body[] = "ok"; + *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "text/plain", body, sizeof(body) - 1); +} + static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) @@ -159,6 +167,12 @@ int main(void) chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); assert(cHTTPX_Get(&router, "/users/{id}", user_handler)); + for (size_t i = 0; i < EXTRA_ROUTE_COUNT; ++i) + { + char path[64]; + snprintf(path, sizeof(path), "/route/%zu", i); + assert(cHTTPX_Get(&router, path, static_handler)); + } assert(cHTTPX_MetricsRoute(&router, "/metrics") == cHTTPX_OK); assert(cHTTPX_AppStart(&app) == cHTTPX_OK); @@ -180,11 +194,19 @@ int main(void) for (size_t i = 0; i < WORKER_COUNT; i++) assert(pthread_join(workers[i], NULL) == 0); + for (size_t i = 0; i < EXTRA_ROUTE_COUNT; ++i) + { + char path[64]; + snprintf(path, sizeof(path), "/route/%zu", i); + http_response_t response = exchange(server->port, path); + assert(strncmp(response.bytes, "HTTP/1.1 200 OK", 15) == 0); + } + __atomic_store_n(&snapshot_ctx.stop, 1, __ATOMIC_RELEASE); assert(pthread_join(snapshot_thread, NULL) == 0); const uint64_t expected_requests = - (uint64_t)WORKER_COUNT * (uint64_t)REQUESTS_PER_WORKER; + (uint64_t)WORKER_COUNT * (uint64_t)REQUESTS_PER_WORKER + EXTRA_ROUTE_COUNT; chttpx_metrics_t metrics; assert(cHTTPX_ServerMetrics(server, &metrics) == cHTTPX_OK); @@ -208,6 +230,7 @@ int main(void) const char* body = response_body(&scrape); assert(strstr(body, "# TYPE libchttpx_requests_total counter")); assert(strstr(body, "libchttpx_route_requests_total{method=\"GET\",route=\"/users/{id}\",class=\"2xx\"}")); + assert(strstr(body, "libchttpx_route_requests_total{method=\"GET\",route=\"/route/23\",class=\"2xx\"}")); assert(strstr(body, "libchttpx_request_duration_seconds_bucket{le=\"+Inf\"}")); assert(strstr(body, "libchttpx_worker_queue_depth")); assert(strstr(body, "libchttpx_workers_active")); diff --git a/tests/test_server.c b/tests/test_server.c index 56e3db7..9deca49 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -260,7 +260,11 @@ int main(void) assert(cHTTPX_AppRemote(&app, "remote-payments", remote_url) == cHTTPX_OK); assert(cHTTPX_AppRemote(&app, "unavailable", "http://127.0.0.1:1") == cHTTPX_OK); - const char* cors_origins[] = {"https://example.com"}; + const char* cors_origins[] = { + "https://z.example.com", + "https://example.com", + "https://a.example.com", + }; cHTTPX_Cors(public_api, cors_origins, CHTTPX_ARRAY_LEN(cors_origins), NULL, NULL); assert(cHTTPX_AppStart(&app) == cHTTPX_OK); From fbef4be36006abcc6648af0e6f487268d398a8d8 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:29:19 +0500 Subject: [PATCH 04/48] feat: add internal HTTP/2 transport interface --- src/cHTTPX_http2.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/cHTTPX_http2.h diff --git a/src/cHTTPX_http2.h b/src/cHTTPX_http2.h new file mode 100644 index 0000000..07d5579 --- /dev/null +++ b/src/cHTTPX_http2.h @@ -0,0 +1,27 @@ +#ifndef CHTTPX_HTTP2_H +#define CHTTPX_HTTP2_H + +#include "cHTTPX_serv.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session); + +int _chttpx_http2_call(chttpx_request_t* source, + const char* base_url, + const chttpx_tls_client_config_t* tls_config, + const char* method, + const char* path, + const void* body, + size_t body_size, + const char* content_type, + chttpx_response_t* res); + +#ifdef __cplusplus +} +#endif + +#endif From e0f7e0a41b4632baf4dc36a328cef3ecce3f215b Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:29:22 +0500 Subject: [PATCH 05/48] feat: implement HTTP/2 server and client transport --- src/cHTTPX_http2.c | 1083 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1083 insertions(+) create mode 100644 src/cHTTPX_http2.c diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c new file mode 100644 index 0000000..6d60f1a --- /dev/null +++ b/src/cHTTPX_http2.c @@ -0,0 +1,1083 @@ +#include "cHTTPX_http2.h" + +#include "cHTTPX_crosspltm.h" +#include "cHTTPX_headers.h" +#include "cHTTPX_http.h" +#include "cHTTPX_response.h" +#include "cHTTPX_tls.h" + +#include + +#include +#include +#include +#include +#include + +#define CHTTPX_H2_PROTOCOL "HTTP/2" +#define CHTTPX_H2_CALL_TIMEOUT_SEC 30 +#define CHTTPX_H2_MAX_AUTHORITY 512 + +typedef struct chttpx_h2_stream +{ + int32_t id; + char method[16]; + char path[CHTTPX_MAX_PATH]; + char authority[CHTTPX_H2_MAX_AUTHORITY]; + chttpx_header_t headers[MAX_HEADERS]; + size_t headers_count; + unsigned char* body; + size_t body_size; + size_t body_capacity; + size_t body_limit; + bool responded; + char* response; + size_t response_size; + size_t response_body_offset; + size_t response_body_sent; +} chttpx_h2_stream_t; + +typedef struct +{ + chttpx_serv_t* server; + chttpx_socket_t fd; + void* tls_session; + nghttp2_session* session; +} chttpx_h2_server_t; + +typedef struct +{ + chttpx_socket_t fd; + void* tls_session; + nghttp2_session* session; + int32_t stream_id; + bool done; + int status; + char content_type[512]; + unsigned char* body; + size_t body_size; + size_t body_capacity; +} chttpx_h2_client_t; + +typedef struct +{ + const unsigned char* body; + size_t size; + size_t offset; +} chttpx_h2_client_body_t; + +typedef struct +{ + bool tls; + char host[CHTTPX_H2_MAX_AUTHORITY]; + char port[16]; + char base_path[CHTTPX_MAX_PATH]; +} chttpx_h2_url_t; + +static nghttp2_nv h2_nv(const char* name, const char* value) +{ + return (nghttp2_nv){ + .name = (uint8_t*)name, + .value = (uint8_t*)value, + .namelen = strlen(name), + .valuelen = strlen(value), + .flags = NGHTTP2_NV_FLAG_NONE, + }; +} + +static bool h2_forbidden_header(const char* name) +{ + return strcasecmp(name, "connection") == 0 || + strcasecmp(name, "keep-alive") == 0 || + strcasecmp(name, "proxy-connection") == 0 || + strcasecmp(name, "transfer-encoding") == 0 || + strcasecmp(name, "upgrade") == 0; +} + +static chttpx_h2_stream_t* h2_stream(nghttp2_session* session, int32_t stream_id) +{ + return (chttpx_h2_stream_t*)nghttp2_session_get_stream_user_data(session, stream_id); +} + +static void h2_stream_free(chttpx_h2_stream_t* stream) +{ + if (!stream) + return; + free(stream->body); + free(stream->response); + free(stream); +} + +static int h2_stream_add_header(chttpx_h2_stream_t* stream, + const uint8_t* name, + size_t namelen, + const uint8_t* value, + size_t valuelen) +{ + if (!stream || !name || !value) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + if (namelen == 7 && memcmp(name, ":method", 7) == 0) + { + if (valuelen >= sizeof(stream->method)) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + memcpy(stream->method, value, valuelen); + stream->method[valuelen] = '\0'; + return 0; + } + + if (namelen == 5 && memcmp(name, ":path", 5) == 0) + { + if (valuelen >= sizeof(stream->path)) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + memcpy(stream->path, value, valuelen); + stream->path[valuelen] = '\0'; + return 0; + } + + if (namelen == 10 && memcmp(name, ":authority", 10) == 0) + { + if (valuelen >= sizeof(stream->authority)) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + memcpy(stream->authority, value, valuelen); + stream->authority[valuelen] = '\0'; + return 0; + } + + if (namelen > 0 && name[0] == ':') + return 0; + + if (stream->headers_count >= MAX_HEADERS || namelen >= MAX_HEADER_NAME || valuelen >= MAX_HEADER_VALUE) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + + chttpx_header_t* header = &stream->headers[stream->headers_count++]; + memcpy(header->name, name, namelen); + header->name[namelen] = '\0'; + memcpy(header->value, value, valuelen); + header->value[valuelen] = '\0'; + return 0; +} + +static const char* h2_request_header(const chttpx_h2_stream_t* stream, const char* name) +{ + if (!stream || !name) + return NULL; + for (size_t i = 0; i < stream->headers_count; i++) + if (strcasecmp(stream->headers[i].name, name) == 0) + return stream->headers[i].value; + return NULL; +} + +static size_t h2_body_limit(const chttpx_h2_server_t* connection, const chttpx_h2_stream_t* stream) +{ + const char* content_type = h2_request_header(stream, "content-type"); + if (content_type && + (strncasecmp(content_type, "application/json", 16) == 0 || + strncasecmp(content_type, "application/x-www-form-urlencoded", 33) == 0 || + strncasecmp(content_type, "text/", 5) == 0)) + return connection->server->max_body_size; + return connection->server->max_upload_size; +} + +static int h2_append_body(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream, const uint8_t* data, size_t len) +{ + if (!stream || (len && !data)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + if (stream->body_limit == 0) + stream->body_limit = h2_body_limit(connection, stream); + + if (stream->body_size > stream->body_limit || len > stream->body_limit - stream->body_size) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + + size_t required = stream->body_size + len + 1; + if (required > stream->body_capacity) + { + size_t capacity = stream->body_capacity ? stream->body_capacity : 4096; + while (capacity < required) + { + size_t next = capacity * 2; + if (next < capacity || next > stream->body_limit + 1) + next = stream->body_limit + 1; + capacity = next; + if (capacity < required && capacity == stream->body_limit + 1) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + unsigned char* resized = realloc(stream->body, capacity); + if (!resized) + return NGHTTP2_ERR_CALLBACK_FAILURE; + stream->body = resized; + stream->body_capacity = capacity; + } + + if (len) + memcpy(stream->body + stream->body_size, data, len); + stream->body_size += len; + stream->body[stream->body_size] = '\0'; + return 0; +} + +static int h2_build_request_text(const chttpx_h2_server_t* connection, + const chttpx_h2_stream_t* stream, + char** output, + size_t* output_size) +{ + if (!connection || !stream || !output || !output_size || !stream->method[0] || !stream->path[0]) + return cHTTPX_ERR_PROTOCOL; + + size_t capacity = 256 + strlen(stream->method) + strlen(stream->path) + strlen(stream->authority); + for (size_t i = 0; i < stream->headers_count; i++) + { + size_t name_len = strlen(stream->headers[i].name); + size_t value_len = strlen(stream->headers[i].value); + if (capacity > SIZE_MAX - name_len - value_len - 4) + return cHTTPX_ERR_LIMIT; + capacity += name_len + value_len + 4; + } + capacity += 64; + + size_t header_limit = connection->server->max_header_size ? connection->server->max_header_size : BUFFER_SIZE - 1; + if (capacity > header_limit + 512) + return cHTTPX_ERR_LIMIT; + + char* buffer = malloc(capacity); + if (!buffer) + return cHTTPX_ERR_MEMORY; + + size_t used = 0; + int written = snprintf(buffer, capacity, "%s %s %s\r\n", stream->method, stream->path, CHTTPX_H2_PROTOCOL); + if (written < 0 || (size_t)written >= capacity) + goto limit_error; + used = (size_t)written; + + bool has_host = false; + bool has_content_length = false; + for (size_t i = 0; i < stream->headers_count; i++) + { + const chttpx_header_t* header = &stream->headers[i]; + if (h2_forbidden_header(header->name)) + continue; + if (strcasecmp(header->name, "host") == 0) + has_host = true; + if (strcasecmp(header->name, "content-length") == 0) + has_content_length = true; + + written = snprintf(buffer + used, capacity - used, "%s: %s\r\n", header->name, header->value); + if (written < 0 || (size_t)written >= capacity - used) + goto limit_error; + used += (size_t)written; + } + + if (!has_host && stream->authority[0]) + { + written = snprintf(buffer + used, capacity - used, "Host: %s\r\n", stream->authority); + if (written < 0 || (size_t)written >= capacity - used) + goto limit_error; + used += (size_t)written; + } + + if (!has_content_length) + { + written = snprintf(buffer + used, capacity - used, "Content-Length: %zu\r\n", stream->body_size); + if (written < 0 || (size_t)written >= capacity - used) + goto limit_error; + used += (size_t)written; + } + + if (capacity - used < 3) + goto limit_error; + memcpy(buffer + used, "\r\n", 3); + used += 2; + + if (used > header_limit) + goto limit_error; + + *output = buffer; + *output_size = used; + return cHTTPX_OK; + +limit_error: + free(buffer); + return cHTTPX_ERR_LIMIT; +} + +static ssize_t h2_server_send(nghttp2_session* session, + const uint8_t* data, + size_t length, + int flags, + void* user_data) +{ + (void)session; + (void)flags; + chttpx_h2_server_t* connection = user_data; + if (!connection) + return NGHTTP2_ERR_CALLBACK_FAILURE; + return _chttpx_io_send_all(connection->fd, connection->tls_session, data, length) == cHTTPX_OK + ? (ssize_t)length + : NGHTTP2_ERR_CALLBACK_FAILURE; +} + +static ssize_t h2_response_read(nghttp2_session* session, + int32_t stream_id, + uint8_t* buf, + size_t length, + uint32_t* data_flags, + nghttp2_data_source* source, + void* user_data) +{ + (void)session; + (void)stream_id; + (void)user_data; + chttpx_h2_stream_t* stream = source ? source->ptr : NULL; + if (!stream || !data_flags) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + size_t body_size = stream->response_size - stream->response_body_offset; + size_t remaining = body_size - stream->response_body_sent; + size_t take = remaining < length ? remaining : length; + if (take) + memcpy(buf, stream->response + stream->response_body_offset + stream->response_body_sent, take); + stream->response_body_sent += take; + if (stream->response_body_sent >= body_size) + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + return (ssize_t)take; +} + +static int h2_submit_text_response(chttpx_h2_server_t* connection, + chttpx_h2_stream_t* stream, + int status, + const char* content_type) +{ + char status_text[4]; + snprintf(status_text, sizeof(status_text), "%03d", status); + + nghttp2_nv headers[3]; + size_t count = 0; + headers[count++] = h2_nv(":status", status_text); + headers[count++] = h2_nv("content-length", "0"); + if (content_type) + headers[count++] = h2_nv("content-type", content_type); + + int rv = nghttp2_submit_response(connection->session, stream->id, headers, count, NULL); + if (rv == 0) + stream->responded = true; + return rv; +} + +static int h2_submit_serialized_response(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream) +{ + if (!connection || !stream || !stream->response) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + char* delimiter = strstr(stream->response, "\r\n\r\n"); + if (!delimiter) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + stream->response_body_offset = (size_t)(delimiter - stream->response) + 4; + if (stream->response_body_offset > stream->response_size) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + char* line_end = strstr(stream->response, "\r\n"); + if (!line_end) + return NGHTTP2_ERR_CALLBACK_FAILURE; + *line_end = '\0'; + + int status = 0; + if (sscanf(stream->response, CHTTPX_H2_PROTOCOL " %d", &status) != 1 || status < 100 || status > 599) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + char status_text[4]; + snprintf(status_text, sizeof(status_text), "%03d", status); + + nghttp2_nv headers[MAX_HEADERS + 4]; + size_t count = 0; + headers[count++] = h2_nv(":status", status_text); + + char* cursor = line_end + 2; + while (cursor < delimiter && *cursor) + { + char* end = strstr(cursor, "\r\n"); + if (!end || end > delimiter) + break; + *end = '\0'; + + char* colon = strchr(cursor, ':'); + if (colon) + { + *colon = '\0'; + char* value = colon + 1; + while (*value == ' ' || *value == '\t') + value++; + for (char* p = cursor; *p; p++) + *p = (char)tolower((unsigned char)*p); + + if (!h2_forbidden_header(cursor) && count < CHTTPX_ARRAY_LEN(headers)) + headers[count++] = h2_nv(cursor, value); + } + cursor = end + 2; + } + + size_t body_size = stream->response_size - stream->response_body_offset; + nghttp2_data_provider provider = { + .source = {.ptr = stream}, + .read_callback = h2_response_read, + }; + + int rv = nghttp2_submit_response(connection->session, + stream->id, + headers, + count, + body_size ? &provider : NULL); + if (rv == 0) + stream->responded = true; + return rv; +} + +static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream) +{ + if (!connection || !stream || stream->responded) + return 0; + + char* headers = NULL; + size_t header_size = 0; + int build_result = h2_build_request_text(connection, stream, &headers, &header_size); + if (build_result != cHTTPX_OK) + return h2_submit_text_response(connection, + stream, + build_result == cHTTPX_ERR_LIMIT ? cHTTPX_StatusRequestHeaderFieldsTooLarge : cHTTPX_StatusBadRequest, + cHTTPX_CTYPE_TEXT); + + unsigned char* body = stream->body; + size_t body_size = stream->body_size; + stream->body = NULL; + stream->body_size = 0; + stream->body_capacity = 0; + + char* response = NULL; + size_t response_size = 0; + int execute_result = _chttpx_execute_prefetched(connection->server, + connection->fd, + connection->tls_session, + headers, + header_size, + body, + body_size, + NULL, + body_size, + &response, + &response_size); + free(headers); + + if (execute_result != cHTTPX_OK || !response) + { + free(response); + return h2_submit_text_response(connection, stream, cHTTPX_StatusInternalServerError, cHTTPX_CTYPE_TEXT); + } + + stream->response = response; + stream->response_size = response_size; + return h2_submit_serialized_response(connection, stream); +} + +static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) +{ + (void)user_data; + if (!frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) + return 0; + + chttpx_h2_stream_t* stream = calloc(1, sizeof(*stream)); + if (!stream) + return NGHTTP2_ERR_CALLBACK_FAILURE; + stream->id = frame->hd.stream_id; + + int rv = nghttp2_session_set_stream_user_data(session, stream->id, stream); + if (rv != 0) + { + free(stream); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int h2_server_header(nghttp2_session* session, + const nghttp2_frame* frame, + const uint8_t* name, + size_t namelen, + const uint8_t* value, + size_t valuelen, + uint8_t flags, + void* user_data) +{ + (void)flags; + (void)user_data; + if (!frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) + return 0; + chttpx_h2_stream_t* stream = h2_stream(session, frame->hd.stream_id); + return h2_stream_add_header(stream, name, namelen, value, valuelen); +} + +static int h2_server_data(nghttp2_session* session, + uint8_t flags, + int32_t stream_id, + const uint8_t* data, + size_t len, + void* user_data) +{ + (void)flags; + chttpx_h2_server_t* connection = user_data; + chttpx_h2_stream_t* stream = h2_stream(session, stream_id); + return h2_append_body(connection, stream, data, len); +} + +static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) +{ + chttpx_h2_server_t* connection = user_data; + if (!connection || !frame) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + if (frame->hd.stream_id <= 0 || !(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) + return 0; + + if (frame->hd.type != NGHTTP2_HEADERS && frame->hd.type != NGHTTP2_DATA) + return 0; + + chttpx_h2_stream_t* stream = h2_stream(session, frame->hd.stream_id); + if (!stream) + return 0; + + int rv = h2_process_request(connection, stream); + return rv == 0 ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; +} + +static int h2_server_stream_close(nghttp2_session* session, + int32_t stream_id, + uint32_t error_code, + void* user_data) +{ + (void)error_code; + (void)user_data; + chttpx_h2_stream_t* stream = h2_stream(session, stream_id); + if (stream) + { + nghttp2_session_set_stream_user_data(session, stream_id, NULL); + h2_stream_free(stream); + } + return 0; +} + +int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session) +{ + if (!server) + return cHTTPX_ERR_INVALID_ARGUMENT; + + chttpx_h2_server_t connection = { + .server = server, + .fd = client_fd, + .tls_session = tls_session, + }; + + nghttp2_session_callbacks* callbacks = NULL; + if (nghttp2_session_callbacks_new(&callbacks) != 0) + return cHTTPX_ERR_MEMORY; + + nghttp2_session_callbacks_set_send_callback(callbacks, h2_server_send); + nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, h2_server_begin_headers); + nghttp2_session_callbacks_set_on_header_callback(callbacks, h2_server_header); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, h2_server_data); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, h2_server_frame_recv); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, h2_server_stream_close); + + int rv = nghttp2_session_server_new(&connection.session, callbacks, &connection); + nghttp2_session_callbacks_del(callbacks); + if (rv != 0) + return cHTTPX_ERR_MEMORY; + + nghttp2_settings_entry settings[] = { + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 128}, + {NGHTTP2_SETTINGS_ENABLE_PUSH, 0}, + }; + if (nghttp2_submit_settings(connection.session, NGHTTP2_FLAG_NONE, settings, CHTTPX_ARRAY_LEN(settings)) != 0 || + nghttp2_session_send(connection.session) != 0) + { + nghttp2_session_del(connection.session); + return cHTTPX_ERR_PROTOCOL; + } + + unsigned char buffer[BUFFER_SIZE]; + int result = cHTTPX_OK; + + while (!server->shutdown_requested && (nghttp2_session_want_read(connection.session) || nghttp2_session_want_write(connection.session))) + { + int received = _chttpx_io_recv(client_fd, tls_session, buffer, sizeof(buffer)); + if (received == 0) + break; + if (received < 0) + { + result = received == cHTTPX_ERR_TIMEOUT ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_IO; + break; + } + + size_t offset = 0; + while (offset < (size_t)received) + { + ssize_t consumed = nghttp2_session_mem_recv(connection.session, buffer + offset, (size_t)received - offset); + if (consumed < 0) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + if (consumed == 0) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + offset += (size_t)consumed; + } + + if (nghttp2_session_send(connection.session) != 0) + { + result = cHTTPX_ERR_IO; + break; + } + } + +done: + nghttp2_session_del(connection.session); + return result; +} + +static int h2_parse_url(const char* base_url, chttpx_h2_url_t* parsed) +{ + if (!base_url || !parsed) + return 0; + + memset(parsed, 0, sizeof(*parsed)); + const char* cursor = NULL; + if (strncmp(base_url, "https://", 8) == 0) + { + parsed->tls = true; + cursor = base_url + 8; + } + else if (strncmp(base_url, "http://", 7) == 0) + cursor = base_url + 7; + else + return 0; + + const char* slash = strchr(cursor, '/'); + size_t authority_len = slash ? (size_t)(slash - cursor) : strlen(cursor); + if (!authority_len || authority_len >= CHTTPX_H2_MAX_AUTHORITY) + return 0; + + char authority[CHTTPX_H2_MAX_AUTHORITY]; + memcpy(authority, cursor, authority_len); + authority[authority_len] = '\0'; + + if (authority[0] == '[') + { + char* close = strchr(authority, ']'); + if (!close) + return 0; + *close = '\0'; + snprintf(parsed->host, sizeof(parsed->host), "%s", authority + 1); + if (close[1] == ':') + snprintf(parsed->port, sizeof(parsed->port), "%s", close + 2); + } + else + { + char* colon = strrchr(authority, ':'); + if (colon && strchr(authority, ':') == colon) + { + *colon = '\0'; + snprintf(parsed->port, sizeof(parsed->port), "%s", colon + 1); + } + snprintf(parsed->host, sizeof(parsed->host), "%s", authority); + } + + if (!parsed->host[0]) + return 0; + if (!parsed->port[0]) + snprintf(parsed->port, sizeof(parsed->port), "%s", parsed->tls ? "443" : "80"); + if (slash) + snprintf(parsed->base_path, sizeof(parsed->base_path), "%s", slash); + return 1; +} + +static int h2_set_call_timeouts(chttpx_socket_t fd) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + DWORD timeout_ms = CHTTPX_H2_CALL_TIMEOUT_SEC * 1000U; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) + return cHTTPX_ERR_UNAVAILABLE; +#else + struct timeval timeout = {.tv_sec = CHTTPX_H2_CALL_TIMEOUT_SEC, .tv_usec = 0}; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) + return cHTTPX_ERR_UNAVAILABLE; +#endif + return cHTTPX_OK; +} + +static int h2_connect(const chttpx_h2_url_t* remote, chttpx_socket_t* connected) +{ + if (!remote || !connected) + return cHTTPX_ERR_INVALID_ARGUMENT; + + struct addrinfo hints; + struct addrinfo* result = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo(remote->host, remote->port, &hints, &result) != 0) + return cHTTPX_ERR_UNAVAILABLE; + + int final_result = cHTTPX_ERR_UNAVAILABLE; + for (struct addrinfo* current = result; current; current = current->ai_next) + { + chttpx_socket_t fd = socket(current->ai_family, current->ai_socktype, current->ai_protocol); +#ifdef CHTTPX_PLATFORM_WINDOWS + if (fd == INVALID_SOCKET) +#else + if (fd < 0) +#endif + continue; + + if (h2_set_call_timeouts(fd) != cHTTPX_OK) + { + chttpx_close(fd); + continue; + } + + if (connect(fd, current->ai_addr, (int)current->ai_addrlen) == 0) + { + *connected = fd; + final_result = cHTTPX_OK; + break; + } + chttpx_close(fd); + } + + freeaddrinfo(result); + return final_result; +} + +static ssize_t h2_client_send(nghttp2_session* session, + const uint8_t* data, + size_t length, + int flags, + void* user_data) +{ + (void)session; + (void)flags; + chttpx_h2_client_t* client = user_data; + if (!client) + return NGHTTP2_ERR_CALLBACK_FAILURE; + return _chttpx_io_send_all(client->fd, client->tls_session, data, length) == cHTTPX_OK + ? (ssize_t)length + : NGHTTP2_ERR_CALLBACK_FAILURE; +} + +static int h2_client_header(nghttp2_session* session, + const nghttp2_frame* frame, + const uint8_t* name, + size_t namelen, + const uint8_t* value, + size_t valuelen, + uint8_t flags, + void* user_data) +{ + (void)session; + (void)flags; + chttpx_h2_client_t* client = user_data; + if (!client || !frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_RESPONSE) + return 0; + + if (namelen == 7 && memcmp(name, ":status", 7) == 0) + { + char status[4] = {0}; + if (valuelen != 3) + return NGHTTP2_ERR_CALLBACK_FAILURE; + memcpy(status, value, 3); + client->status = atoi(status); + } + else if (namelen == 12 && memcmp(name, "content-type", 12) == 0) + { + size_t copy = valuelen < sizeof(client->content_type) - 1 ? valuelen : sizeof(client->content_type) - 1; + memcpy(client->content_type, value, copy); + client->content_type[copy] = '\0'; + } + return 0; +} + +static int h2_client_data(nghttp2_session* session, + uint8_t flags, + int32_t stream_id, + const uint8_t* data, + size_t len, + void* user_data) +{ + (void)session; + (void)flags; + chttpx_h2_client_t* client = user_data; + if (!client || stream_id != client->stream_id) + return 0; + + if (client->body_size > MAX_BUFFER_BODY || len > MAX_BUFFER_BODY - client->body_size) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + size_t required = client->body_size + len + 1; + if (required > client->body_capacity) + { + size_t capacity = client->body_capacity ? client->body_capacity : 4096; + while (capacity < required) + { + if (capacity > MAX_BUFFER_BODY / 2) + { + capacity = MAX_BUFFER_BODY + 1; + break; + } + capacity *= 2; + } + unsigned char* resized = realloc(client->body, capacity); + if (!resized) + return NGHTTP2_ERR_CALLBACK_FAILURE; + client->body = resized; + client->body_capacity = capacity; + } + + if (len) + memcpy(client->body + client->body_size, data, len); + client->body_size += len; + client->body[client->body_size] = '\0'; + return 0; +} + +static int h2_client_stream_close(nghttp2_session* session, + int32_t stream_id, + uint32_t error_code, + void* user_data) +{ + (void)session; + (void)error_code; + chttpx_h2_client_t* client = user_data; + if (client && stream_id == client->stream_id) + client->done = true; + return 0; +} + +static ssize_t h2_client_body_read(nghttp2_session* session, + int32_t stream_id, + uint8_t* buf, + size_t length, + uint32_t* data_flags, + nghttp2_data_source* source, + void* user_data) +{ + (void)session; + (void)stream_id; + (void)user_data; + chttpx_h2_client_body_t* body = source ? source->ptr : NULL; + if (!body || !data_flags) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + size_t remaining = body->size - body->offset; + size_t take = remaining < length ? remaining : length; + if (take) + memcpy(buf, body->body + body->offset, take); + body->offset += take; + if (body->offset >= body->size) + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + return (ssize_t)take; +} + +int _chttpx_http2_call(chttpx_request_t* source, + const char* base_url, + const chttpx_tls_client_config_t* tls_config, + const char* method, + const char* path, + const void* body, + size_t body_size, + const char* content_type, + chttpx_response_t* res) +{ + if (!source || !base_url || !method || !path || !res || (body_size && !body)) + return cHTTPX_ERR_INVALID_ARGUMENT; + + chttpx_h2_url_t remote; + if (!h2_parse_url(base_url, &remote)) + return cHTTPX_ERR_PROTOCOL; + +#ifdef CHTTPX_PLATFORM_WINDOWS + chttpx_socket_t fd = INVALID_SOCKET; +#else + chttpx_socket_t fd = -1; +#endif + int result = h2_connect(&remote, &fd); + if (result != cHTTPX_OK) + return result; + + void* tls_context = NULL; + void* tls_session = NULL; + if (remote.tls) + { + result = _chttpx_tls_client_connect(fd, remote.host, tls_config, &tls_context, &tls_session); + if (result != cHTTPX_OK) + { + chttpx_close(fd); + return result; + } + } + + chttpx_h2_client_t client = { + .fd = fd, + .tls_session = tls_session, + }; + + nghttp2_session_callbacks* callbacks = NULL; + if (nghttp2_session_callbacks_new(&callbacks) != 0) + { + result = cHTTPX_ERR_MEMORY; + goto done; + } + + nghttp2_session_callbacks_set_send_callback(callbacks, h2_client_send); + nghttp2_session_callbacks_set_on_header_callback(callbacks, h2_client_header); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, h2_client_data); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, h2_client_stream_close); + + if (nghttp2_session_client_new(&client.session, callbacks, &client) != 0) + { + nghttp2_session_callbacks_del(callbacks); + result = cHTTPX_ERR_MEMORY; + goto done; + } + nghttp2_session_callbacks_del(callbacks); + + if (nghttp2_submit_settings(client.session, NGHTTP2_FLAG_NONE, NULL, 0) != 0) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + + char full_path[CHTTPX_MAX_PATH]; + if (remote.base_path[0] && strcmp(remote.base_path, "/") != 0) + { + int n = snprintf(full_path, sizeof(full_path), "%s%s%s", + remote.base_path, + remote.base_path[strlen(remote.base_path) - 1] == '/' || path[0] == '/' ? "" : "/", + path); + if (n < 0 || (size_t)n >= sizeof(full_path)) + { + result = cHTTPX_ERR_LIMIT; + goto done; + } + } + else + snprintf(full_path, sizeof(full_path), "%s", path); + + char authority[CHTTPX_H2_MAX_AUTHORITY + 24]; + bool default_port = (remote.tls && strcmp(remote.port, "443") == 0) || (!remote.tls && strcmp(remote.port, "80") == 0); + if (default_port) + snprintf(authority, sizeof(authority), "%s", remote.host); + else + snprintf(authority, sizeof(authority), "%s:%s", remote.host, remote.port); + + char content_length[32]; + snprintf(content_length, sizeof(content_length), "%zu", body_size); + + const char* selected_content_type = content_type && *content_type + ? content_type + : (source->content_type[0] ? source->content_type : cHTTPX_CTYPE_JSON); + const char* authorization = cHTTPX_HeaderGet(source, "Authorization"); + + nghttp2_nv headers[10]; + size_t count = 0; + headers[count++] = h2_nv(":method", method); + headers[count++] = h2_nv(":scheme", remote.tls ? "https" : "http"); + headers[count++] = h2_nv(":authority", authority); + headers[count++] = h2_nv(":path", full_path); + headers[count++] = h2_nv("content-type", selected_content_type); + headers[count++] = h2_nv("content-length", content_length); + if (source->request_id[0]) + headers[count++] = h2_nv("x-request-id", source->request_id); + if (source->language[0]) + headers[count++] = h2_nv("accept-language", source->language); + if (authorization && *authorization) + headers[count++] = h2_nv("authorization", authorization); + + chttpx_h2_client_body_t request_body = { + .body = body, + .size = body_size, + }; + nghttp2_data_provider provider = { + .source = {.ptr = &request_body}, + .read_callback = h2_client_body_read, + }; + + client.stream_id = nghttp2_submit_request(client.session, + NULL, + headers, + count, + body_size ? &provider : NULL, + NULL); + if (client.stream_id < 0 || nghttp2_session_send(client.session) != 0) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + + unsigned char buffer[BUFFER_SIZE]; + while (!client.done) + { + int received = _chttpx_io_recv(fd, tls_session, buffer, sizeof(buffer)); + if (received <= 0) + { + result = received == cHTTPX_ERR_TIMEOUT ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_IO; + goto done; + } + + size_t offset = 0; + while (offset < (size_t)received) + { + ssize_t consumed = nghttp2_session_mem_recv(client.session, buffer + offset, (size_t)received - offset); + if (consumed <= 0) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + offset += (size_t)consumed; + } + + if (nghttp2_session_send(client.session) != 0) + { + result = cHTTPX_ERR_IO; + goto done; + } + } + + if (client.status < 100 || client.status > 599) + { + result = cHTTPX_ERR_PROTOCOL; + goto done; + } + + *res = cHTTPX_ResBinary((uint16_t)client.status, + client.content_type[0] ? client.content_type : cHTTPX_CTYPE_OCTET, + client.body, + client.body_size); + if (!res->status) + { + result = cHTTPX_ERR_MEMORY; + goto done; + } + result = cHTTPX_OK; + +done: + if (client.session) + nghttp2_session_del(client.session); + free(client.body); + _chttpx_tls_client_close(tls_context, tls_session); + chttpx_close(fd); + return result; +} From 4b62a78834fd81d3a0d7f346add82a9e82f8eee6 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:30:58 +0500 Subject: [PATCH 06/48] fix: send valid HTTP/2 server settings --- src/cHTTPX_http2.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index 6d60f1a..253784c 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -594,7 +594,6 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* nghttp2_settings_entry settings[] = { {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 128}, - {NGHTTP2_SETTINGS_ENABLE_PUSH, 0}, }; if (nghttp2_submit_settings(connection.session, NGHTTP2_FLAG_NONE, settings, CHTTPX_ARRAY_LEN(settings)) != 0 || nghttp2_session_send(connection.session) != 0) From 9ba3d848a484f8cb3bd01c0f90074bd68471e83a Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:31:15 +0500 Subject: [PATCH 07/48] refactor: route socket handling through HTTP/2 transport --- src/cHTTPX_response.c | 75 +++---------------------------------------- 1 file changed, 4 insertions(+), 71 deletions(-) diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index bd79915..ed952eb 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -32,6 +32,7 @@ #include "cHTTPX_queries.h" #include "cHTTPX_crosspltm.h" #include "cHTTPX_tls.h" +#include "cHTTPX_http2.h" #include "cHTTPX_metrics.h" #include @@ -414,7 +415,7 @@ static int append_response_header(char* buffer, size_t capacity, size_t* length, * @param res httpx_response_t structure containing status, content type, and body. * @param client_fd File descriptor of the connected client socket. * - * This function formats the HTTP response headers and body according to HTTP/1.1. + * This function serializes the application response for the HTTP/2 transport. */ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, char** output, size_t* output_size) { @@ -448,7 +449,7 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c size_t length = 0; const char* allowed_origin = server && server->cors.enabled ? allowed_origin_cors(server, cHTTPX_HeaderGet(req, "Origin")) : NULL; - if (!append_response_header(header, capacity, &length, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\n", res.status, cHTTPX_StatusReason((uint16_t)res.status), res.content_type ? res.content_type : cHTTPX_CTYPE_OCTET, res.body_size)) + if (!append_response_header(header, capacity, &length, "HTTP/2 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n", res.status, cHTTPX_StatusReason((uint16_t)res.status), res.content_type ? res.content_type : cHTTPX_CTYPE_OCTET, res.body_size)) goto limit_error; const char* etag = generate_etag(res.body, res.body_size); @@ -993,75 +994,7 @@ void* chttpx_handle(void* arg) return NULL; } - chttpx_request_t* req = NULL; - char buf[BUFFER_SIZE]; - ssize_t received = read_req(server, client_sock, tls_session, buf, BUFFER_SIZE); - if (received == -2) - { - _chttpx_metrics_parser_failure(server); - static const char too_large[] = "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - _chttpx_io_send_all(client_sock, tls_session, too_large, sizeof(too_large) - 1); - goto cleanup_connection; - } - if (received <= 0) - { - if (received == cHTTPX_ERR_TIMEOUT) - _chttpx_metrics_timeout_failure(server); - else if (received == cHTTPX_ERR_TLS) - { - _chttpx_metrics_connection_rejected(server); - _chttpx_tls_log_error(server, "-", "TLS request-header read failed"); - } - goto cleanup_connection; - } - - req = parse_req_buffer(server, client_sock, tls_session, buf, (size_t)received); - if (!req) - { - _chttpx_metrics_parser_failure(server); - goto cleanup_connection; - } - - if (req->_parse_status) - { - _chttpx_metrics_parser_failure(server); - const char* parse_message = req->_parse_status == cHTTPX_StatusPayloadTooLarge - ? "payload too large" - : (req->_parse_status == cHTTPX_StatusInternalServerError ? "internal server error" : "invalid request"); - chttpx_response_t parse_error = cHTTPX_ResError((uint16_t)req->_parse_status, parse_message); - send_response(req, parse_error); - cHTTPX_ResponseCleanup(&parse_error); - goto cleanup_request; - } - - if (is_cors_preflight(req)) - goto cleanup_request; - - chttpx_response_t res = {0}; - if (_chttpx_dispatch(server, req, &res) == cHTTPX_OK) - { - send_response(req, res); - cHTTPX_ResponseCleanup(&res); - } - -cleanup_request: - cHTTPX_RequestCleanup(req); - chttpx_free_req_cookie(req); - - free(req->method); - free(req->path); - free(req->body); - - for (size_t i = 0; i < req->query_count; i++) - { - free(req->query[i].name); - free(req->query[i].value); - } - - free(req->query); - free(req); - -cleanup_connection: + _chttpx_http2_serve(server, client_sock, tls_session); _chttpx_tls_session_close(tls_session); chttpx_close(client_sock); return NULL; From 0684d9ebb95acf9c740831c2880b97a6aa0df258 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:31:18 +0500 Subject: [PATCH 08/48] refactor: expose internal HTTP/2 dispatch adapter --- src/cHTTPX_response.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cHTTPX_response.h b/src/cHTTPX_response.h index 34bcb02..bf2fd03 100644 --- a/src/cHTTPX_response.h +++ b/src/cHTTPX_response.h @@ -72,6 +72,11 @@ extern "C" /* Internal route dispatcher shared by socket requests and cHTTPX_Call(). */ int _chttpx_dispatch(struct chttpx_serv* server, chttpx_request_t* req, chttpx_response_t* res); + /* Internal adapter used by the HTTP/2 transport to reuse the public request/handler API. */ + int _chttpx_execute_prefetched(struct chttpx_serv* server, chttpx_socket_t client_fd, void* tls_session, + char* headers, size_t header_size, unsigned char* body, size_t body_size, + FILE* body_stream, size_t content_length, char** output, size_t* output_size); + /** * Create a JSON HTTP response with formatted content. * From 0e5be3f21e3923feb287340057c6cb935ebc07a2 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:31:36 +0500 Subject: [PATCH 09/48] feat: negotiate HTTP/2 with TLS ALPN --- src/cHTTPX_tls.c | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index c4b6479..6f6e9d8 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -2,7 +2,7 @@ * Optional TLS transport backend for libchttpx. * * Define CHTTPX_ENABLE_TLS and link OpenSSL (ssl + crypto) to enable it. - * Plain HTTP builds compile this file without any OpenSSL dependency. + * Cleartext HTTP/2 builds compile this file without any OpenSSL dependency. */ #include "cHTTPX_tls.h" @@ -18,6 +18,39 @@ #include #include #include + +static int chttpx_h2_alpn_select(SSL* ssl, const unsigned char** out, unsigned char* outlen, + const unsigned char* in, unsigned int inlen, void* arg) +{ + (void)ssl; + (void)arg; + const unsigned char* cursor = in; + const unsigned char* end = in + inlen; + + while (cursor < end) + { + unsigned int length = *cursor++; + if ((size_t)(end - cursor) < length) + break; + if (length == 2 && cursor[0] == 'h' && cursor[1] == '2') + { + *out = cursor; + *outlen = 2; + return SSL_TLSEXT_ERR_OK; + } + cursor += length; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} + +static int chttpx_tls_selected_h2(SSL* ssl) +{ + const unsigned char* protocol = NULL; + unsigned int length = 0; + SSL_get0_alpn_selected(ssl, &protocol, &length); + return length == 2 && protocol && protocol[0] == 'h' && protocol[1] == '2'; +} #endif void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const char* prefix) @@ -98,6 +131,7 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION); + SSL_CTX_set_alpn_select_cb(ctx, chttpx_h2_alpn_select, NULL); if (SSL_CTX_use_certificate_chain_file(ctx, server->tls.cert_file) != 1 || SSL_CTX_use_PrivateKey_file(ctx, server->tls.key_file, SSL_FILETYPE_PEM) != 1 || @@ -178,7 +212,7 @@ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** return cHTTPX_ERR_TLS; } - if (SSL_set_fd(ssl, (int)client_fd) != 1 || SSL_accept(ssl) != 1) + if (SSL_set_fd(ssl, (int)client_fd) != 1 || SSL_accept(ssl) != 1 || !chttpx_tls_selected_h2(ssl)) { _chttpx_tls_log_error(server, "-", "TLS handshake failed"); SSL_free(ssl); @@ -224,7 +258,7 @@ int _chttpx_tls_accept_step(void* session) #else int result = SSL_accept((SSL*)session); if (result == 1) - return cHTTPX_OK; + return chttpx_tls_selected_h2((SSL*)session) ? cHTTPX_OK : cHTTPX_ERR_TLS; int error = SSL_get_error((SSL*)session, result); if (error == SSL_ERROR_WANT_READ) return CHTTPX_IO_WANT_READ; @@ -328,7 +362,11 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, goto error; } - if (SSL_set_fd(ssl, (int)socket_fd) != 1 || SSL_connect(ssl) != 1) + static const unsigned char h2_alpn[] = {2, 'h', '2'}; + if (SSL_set_alpn_protos(ssl, h2_alpn, sizeof(h2_alpn)) != 0) + goto error; + + if (SSL_set_fd(ssl, (int)socket_fd) != 1 || SSL_connect(ssl) != 1 || !chttpx_tls_selected_h2(ssl)) goto error; if (selected.verify_peer && SSL_get_verify_result(ssl) != X509_V_OK) From 445458f053d593a4ed3b0abd0deb413838e1e62f Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:32:28 +0500 Subject: [PATCH 10/48] refactor: run accepted connections as HTTP/2 sessions --- src/cHTTPX_runtime.c | 162 ++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 94 deletions(-) diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c index 2669d27..f4ca581 100644 --- a/src/cHTTPX_runtime.c +++ b/src/cHTTPX_runtime.c @@ -5,6 +5,7 @@ #include "cHTTPX_http.h" #include "cHTTPX_metrics.h" #include "cHTTPX_tls.h" +#include "cHTTPX_http2.h" #include "cHTTPX_utils.h" #include @@ -13,6 +14,9 @@ #include #include #include +#ifdef CHTTPX_PLATFORM_POSIX +#include +#endif #define CHTTPX_RUNTIME_EVENTS 256 #define CHTTPX_CHUNK_LINE_MAX 128 @@ -94,7 +98,6 @@ typedef struct chttpx_runtime chttpx_connection_t* connections; } chttpx_runtime_t; -int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size); static bool runtime_is_stopping(chttpx_runtime_t* runtime) { @@ -110,6 +113,38 @@ static bool socket_valid(chttpx_socket_t fd) #endif } +static int connection_set_blocking(chttpx_connection_t* connection) +{ + if (!connection) + return -1; + +#ifdef CHTTPX_PLATFORM_WINDOWS + u_long mode = 0; + if (ioctlsocket(connection->fd, FIONBIO, &mode) != 0) + return -1; + + DWORD read_timeout_ms = (DWORD)connection->server->read_timeout_sec * 1000U; + DWORD write_timeout_ms = (DWORD)connection->server->write_timeout_sec * 1000U; + if (setsockopt(connection->fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&read_timeout_ms, sizeof(read_timeout_ms)) != 0 || + setsockopt(connection->fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&write_timeout_ms, sizeof(write_timeout_ms)) != 0) + return -1; +#else + int flags = fcntl(connection->fd, F_GETFL, 0); + if (flags < 0 || fcntl(connection->fd, F_SETFL, flags & ~O_NONBLOCK) != 0) + return -1; + + struct timeval timeout = {.tv_usec = 0}; + timeout.tv_sec = connection->server->read_timeout_sec; + if (setsockopt(connection->fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0) + return -1; + timeout.tv_sec = connection->server->write_timeout_sec; + if (setsockopt(connection->fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) + return -1; +#endif + + return 0; +} + static uint64_t monotonic_ms(void) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -181,29 +216,10 @@ static void runtime_worker_execute(void* job, void* context) chttpx_connection_t* connection = job; chttpx_runtime_t* runtime = context; - unsigned char* body = connection->body; - FILE* stream = connection->body_stream; - connection->body = NULL; - connection->body_stream = NULL; - if (runtime_is_stopping(runtime)) - { - free(body); - - if (stream) - fclose(stream); - connection->worker_result = cHTTPX_ERR_STATE; - } else - { - connection->worker_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session, connection->headers, connection->header_end, body, connection->body_size, stream, connection->content_length, &connection->write_buffer, &connection->write_size); - } - - free(connection->headers); - connection->headers = NULL; - connection->header_size = 0; - connection->header_capacity = 0; + connection->worker_result = _chttpx_http2_serve(connection->server, connection->fd, connection->tls_session); completion_push(runtime, connection); } @@ -591,45 +607,22 @@ static bool connection_body_complete(chttpx_connection_t* connection) static int connection_error_response(chttpx_runtime_t* runtime, chttpx_connection_t* connection, int status) { - const char* reason = cHTTPX_StatusReason((uint16_t)status); - char buffer[256]; - int size = snprintf(buffer, sizeof(buffer), "HTTP/1.1 %d %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", status, reason); - if (size <= 0 || (size_t)size >= sizeof(buffer)) - { - connection_close(runtime, connection); - return 0; - } - connection->write_buffer = malloc((size_t)size); - if (!connection->write_buffer) - { - connection_close(runtime, connection); - return 0; - } - memcpy(connection->write_buffer, buffer, (size_t)size); - connection->write_size = (size_t)size; - connection->write_offset = 0; - connection->state = CHTTPX_CONN_WRITING; - connection_touch(connection); - if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) - { - connection_close(runtime, connection); - return 0; - } - return 1; + (void)status; + connection_close(runtime, connection); + return 0; } static int connection_submit(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { - if (connection->body_stream) + _chttpx_event_del(runtime->event_loop, connection->fd); + if (connection_set_blocking(connection) != 0) { - if (fflush(connection->body_stream) != 0 || fseek(connection->body_stream, 0, SEEK_SET) != 0) - { - connection_error_response(runtime, connection, cHTTPX_StatusInternalServerError); - return 0; - } + connection_close(runtime, connection); + return 0; } - _chttpx_event_del(runtime->event_loop, connection->fd); + connection->state = CHTTPX_CONN_PROCESSING; + connection_touch(connection); if (!_chttpx_worker_submit(runtime->worker_pool, connection)) { connection_close(runtime, connection); @@ -746,16 +739,7 @@ static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* c { int result = _chttpx_tls_accept_step(connection->tls_session); if (result == cHTTPX_OK) - { - connection->state = CHTTPX_CONN_READING_HEADERS; - connection_touch(connection); - if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) - { - connection_close(runtime, connection); - return 0; - } - return 1; - } + return connection_submit(runtime, connection); if (result == CHTTPX_IO_WANT_READ) { if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) @@ -870,16 +854,7 @@ static void drain_completions(chttpx_runtime_t* runtime) { chttpx_connection_t* next = connection->completion_next; connection->completion_next = NULL; - if (runtime_is_stopping(runtime) || connection->worker_result != cHTTPX_OK || !connection->write_buffer) - connection_close(runtime, connection); - else - { - connection->state = CHTTPX_CONN_WRITING; - connection->write_offset = 0; - connection_touch(connection); - if (_chttpx_event_add(runtime->event_loop, connection->fd, CHTTPX_EVENT_WRITE, connection) != 0) - connection_close(runtime, connection); - } + connection_close(runtime, connection); connection = next; } } @@ -907,11 +882,6 @@ static void accept_connections(chttpx_runtime_t* runtime) if (runtime_is_stopping(runtime) || __atomic_load_n(&server->current_clients, __ATOMIC_SEQ_CST) >= server->max_clients) { - if (!server->tls.enabled) - { - static const char busy[] = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - send(fd, busy, (int)(sizeof(busy) - 1), 0); - } _chttpx_metrics_connection_rejected(server); chttpx_close(fd); continue; @@ -950,26 +920,30 @@ static void accept_connections(chttpx_runtime_t* runtime) continue; } + if (!server->tls.enabled) + { + connection_submit(runtime, connection); + continue; + } + uint32_t interest = CHTTPX_EVENT_READ; - if (server->tls.enabled) + int step = _chttpx_tls_accept_step(connection->tls_session); + if (step == cHTTPX_OK) { - int step = _chttpx_tls_accept_step(connection->tls_session); - if (step == cHTTPX_OK) - connection->state = CHTTPX_CONN_READING_HEADERS; - else if (step == CHTTPX_IO_WANT_READ) - interest = CHTTPX_EVENT_READ; - else if (step == CHTTPX_IO_WANT_WRITE) - interest = CHTTPX_EVENT_WRITE; - else - { - _chttpx_metrics_connection_rejected(server); - _chttpx_tls_log_error(server, "-", "TLS handshake failed"); - connection_close(runtime, connection); - continue; - } + connection_submit(runtime, connection); + continue; } + if (step == CHTTPX_IO_WANT_READ) + interest = CHTTPX_EVENT_READ; + else if (step == CHTTPX_IO_WANT_WRITE) + interest = CHTTPX_EVENT_WRITE; else - connection->state = CHTTPX_CONN_READING_HEADERS; + { + _chttpx_metrics_connection_rejected(server); + _chttpx_tls_log_error(server, "-", "TLS handshake failed"); + connection_close(runtime, connection); + continue; + } if (_chttpx_event_add(runtime->event_loop, fd, interest, connection) != 0) { From b4febdcb123d3973607f3b39e69e728a1eda1771 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:32:41 +0500 Subject: [PATCH 11/48] refactor: use HTTP/2 for remote service calls --- src/cHTTPX_app.c | 206 +---------------------------------------------- 1 file changed, 4 insertions(+), 202 deletions(-) diff --git a/src/cHTTPX_app.c b/src/cHTTPX_app.c index 877c3da..4d59234 100644 --- a/src/cHTTPX_app.c +++ b/src/cHTTPX_app.c @@ -12,6 +12,7 @@ #include "cHTTPX_queries.h" #include "cHTTPX_utils.h" #include "cHTTPX_tls.h" +#include "cHTTPX_http2.h" #include #include @@ -801,208 +802,9 @@ static int remote_call(chttpx_request_t* source, const chttpx_app_remote_t* targ if (!source || !target) return cHTTPX_ERR_INVALID_ARGUMENT; - chttpx_remote_url_t remote; - if (!parse_remote_url(target->base_url, &remote)) - return cHTTPX_ERR_PROTOCOL; - -#ifdef CHTTPX_PLATFORM_WINDOWS - chttpx_socket_t socket_fd = INVALID_SOCKET; -#else - chttpx_socket_t socket_fd = -1; -#endif - - int result = connect_remote(&remote, &socket_fd); - if (result != cHTTPX_OK) - return result; - - void* tls_ctx = NULL; - void* tls_session = NULL; - char* response = NULL; - - if (remote.tls) - { - result = _chttpx_tls_client_connect(socket_fd, remote.host, &target->tls, &tls_ctx, &tls_session); - if (result != cHTTPX_OK) - { - log_remote_tls_error(source, "remote TLS handshake or certificate verification failed"); - goto done; - } - } - - char full_path[CHTTPX_MAX_PATH]; - if (snprintf(full_path, sizeof(full_path), "%s%s", remote.base_path, path) >= (int)sizeof(full_path)) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - - char host_header[320]; - int host_header_size = strchr(remote.host, ':') - ? snprintf(host_header, sizeof(host_header), "[%s]:%s", remote.host, remote.port) - : snprintf(host_header, sizeof(host_header), "%s:%s", remote.host, remote.port); - if (host_header_size < 0 || (size_t)host_header_size >= sizeof(host_header)) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - - const char* selected_content_type = - content_type && *content_type ? content_type : (source->content_type[0] ? source->content_type : cHTTPX_CTYPE_JSON); - const char* authorization = cHTTPX_HeaderGet(source, "Authorization"); - - char header[8192]; - int header_size = snprintf(header, sizeof(header), - "%s %s HTTP/1.1\r\n" - "Host: %s\r\n" - "Connection: close\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "X-Request-ID: %s\r\n" - "Accept-Language: %s\r\n", - method, full_path, host_header, selected_content_type, body_size, - source->request_id, source->language); - - if (header_size < 0 || (size_t)header_size >= sizeof(header)) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - - size_t used = (size_t)header_size; - if (authorization && *authorization) - { - int added = snprintf(header + used, sizeof(header) - used, "Authorization: %s\r\n", authorization); - if (added < 0 || (size_t)added >= sizeof(header) - used) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - used += (size_t)added; - } - - if (used + 2 > sizeof(header)) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - - memcpy(header + used, "\r\n", 2); - used += 2; - - int send_result = _chttpx_io_send_all(socket_fd, tls_session, header, used); - if (send_result == cHTTPX_OK && body_size) - send_result = _chttpx_io_send_all(socket_fd, tls_session, body, body_size); - - if (send_result != cHTTPX_OK) - { - if (remote.tls) - { - log_remote_tls_error(source, "remote TLS write failed"); - result = cHTTPX_ERR_TLS; - } - else - { - int error = socket_last_error(); - result = socket_error_is_timeout(error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; - } - goto done; - } - - size_t limit = source->_server->max_body_size ? source->_server->max_body_size + 64 * 1024 : 10 * 1024 * 1024; - size_t capacity = 8192; - if (capacity > limit) - capacity = limit; - - response = malloc(capacity + 1); - if (!response) - { - result = cHTTPX_ERR_MEMORY; - goto done; - } - - size_t total = 0; - for (;;) - { - if (total == capacity) - { - if (capacity >= limit) - { - result = cHTTPX_ERR_LIMIT; - goto done; - } - - size_t next = capacity * 2; - if (next > limit) - next = limit; - - char* resized = realloc(response, next + 1); - if (!resized) - { - result = cHTTPX_ERR_MEMORY; - goto done; - } - - response = resized; - capacity = next; - } - - int received = _chttpx_io_recv(socket_fd, tls_session, response + total, capacity - total); - if (received < 0) - { - if (remote.tls) - { - log_remote_tls_error(source, "remote TLS read failed"); - result = cHTTPX_ERR_TLS; - } - else - { - int error = socket_last_error(); - result = socket_error_is_timeout(error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; - } - goto done; - } - - if (received == 0) - { - if (total == 0) - { - result = cHTTPX_ERR_UNAVAILABLE; - goto done; - } - break; - } - - total += (size_t)received; - } - - response[total] = '\0'; - - int status = 0; - if (sscanf(response, "HTTP/%*s %d", &status) != 1 || status < 100 || status > 599) - { - result = cHTTPX_ERR_PROTOCOL; - goto done; - } - - char* delimiter = chttpx_memmem(response, total, "\r\n\r\n", 4); - if (!delimiter) - { - result = cHTTPX_ERR_PROTOCOL; - goto done; - } - - size_t header_bytes = (size_t)(delimiter - response) + 4; - char* body_start = response + header_bytes; - size_t response_body_size = total - header_bytes; - - const char* response_type = remote_response_content_type(find_remote_content_type(response)); - *res = cHTTPX_ResBinary((uint16_t)status, response_type, (const unsigned char*)body_start, response_body_size); - result = res->status ? cHTTPX_OK : cHTTPX_ERR_MEMORY; - -done: - free(response); - _chttpx_tls_client_close(tls_ctx, tls_session); - chttpx_close(socket_fd); + int result = _chttpx_http2_call(source, target->base_url, &target->tls, method, path, body, body_size, content_type, res); + if (result == cHTTPX_ERR_TLS) + log_remote_tls_error(source, "remote HTTP/2 TLS handshake, ALPN, or certificate verification failed"); return result; } From 0671f66a8abea659f8528ee58afa473de04fae57 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:14 +0500 Subject: [PATCH 12/48] build: link nghttp2 for HTTP/2 transport --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 189c517..7af7b81 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ TAR = $(RELEASE_DIR).tar.gz CC = gcc CFLAGS = -Wall -Wextra -Wpedantic -O2 -Iinclude -Isrc -# Native TLS is opt-in so plain HTTP builds keep zero OpenSSL dependency. +# Native TLS is opt-in so cleartext HTTP/2 builds keep zero OpenSSL dependency. TLS ?= 0 TLS_CFLAGS = TLS_LDFLAGS = @@ -30,8 +30,8 @@ PKGDIR ?= /pkg/usr/local WIN_LIB_DIR = tools -LIN_LDFLAGS = -lcjson -lz $(TLS_LDFLAGS) -WIN_LDFLAGS = -lws2_32 -lz $(TLS_LDFLAGS) +LIN_LDFLAGS = -lcjson -lz -lnghttp2 $(TLS_LDFLAGS) +WIN_LDFLAGS = -lws2_32 -lz -lnghttp2 $(TLS_LDFLAGS) TEST_TARGET = $(BINDIR)/test_core TEST_SERVER_TARGET = $(BINDIR)/test_server TEST_SANITIZE_TARGET = $(BINDIR)/test_core_sanitize From da03070754c4254ebed83568b4c7f4d4c6f50b01 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:16 +0500 Subject: [PATCH 13/48] build: install nghttp2 dependencies in container --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index f6231d2..e8ce96e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update \ build-essential \ libcjson-dev \ zlib1g-dev \ + libnghttp2-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /src @@ -20,6 +21,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ libcjson1 \ zlib1g \ + libnghttp2-14 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /pkg/usr/local/lib/libchttpx.so /usr/local/lib/ From e2249d7bdb898ae60ebfdaafca47c5526ef37feb Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:18 +0500 Subject: [PATCH 14/48] ci: install nghttp2 dependencies --- .github/workflows/ci.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d20e93..2d04999 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev + sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev - name: Build binary run: make lin @@ -92,7 +92,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev + sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev - name: Run ASan and UBSan run: make test-sanitize @@ -111,7 +111,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y clang make libcjson-dev zlib1g-dev + sudo apt install -y clang make libcjson-dev zlib1g-dev libnghttp2-dev - name: Run fuzz smoke test run: make test-fuzz @@ -127,7 +127,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev + sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev - name: Build release archive run: make lin-lib @@ -152,7 +152,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev libssl-dev openssl + sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev libnghttp2-dev libssl-dev openssl - name: Build TLS-enabled shared library run: make TLS=1 libchttpx.so @@ -174,7 +174,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y gcc make libcjson-dev zlib1g-dev libssl-dev + sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev libnghttp2-dev libssl-dev - name: Build shared library run: make libchttpx.so @@ -210,6 +210,7 @@ jobs: mingw-w64-x86_64-gcc mingw-w64-x86_64-make mingw-w64-x86_64-zlib + mingw-w64-x86_64-nghttp2 - name: Build run: mingw32-make win SHELL=cmd From def6170e9b67e5034eedbb6fac4efbbc61f7c3e1 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:20 +0500 Subject: [PATCH 15/48] packaging: build HTTP/2 packages with nghttp2 --- .github/actions/build-linux-packages/action.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/build-linux-packages/action.yml b/.github/actions/build-linux-packages/action.yml index 9342dd1..9cd88c8 100644 --- a/.github/actions/build-linux-packages/action.yml +++ b/.github/actions/build-linux-packages/action.yml @@ -32,7 +32,7 @@ runs: bash -euc ' export DEBIAN_FRONTEND=noninteractive apt-get update - apt-get install -y --no-install-recommends build-essential libcjson-dev zlib1g-dev + apt-get install -y --no-install-recommends build-essential libcjson-dev zlib1g-dev libnghttp2-dev make clean make libchttpx.so cp libchttpx.so dist/deb/libchttpx.so @@ -47,7 +47,7 @@ runs: -w /work \ fedora:42 \ bash -euc ' - dnf install -y gcc make cjson-devel zlib-devel + dnf install -y gcc make cjson-devel zlib-devel libnghttp2-devel make clean make libchttpx.so cp libchttpx.so dist/rpm/libchttpx.so @@ -62,7 +62,7 @@ runs: -w /work \ archlinux:base-devel \ bash -euc ' - pacman -Sy --noconfirm cjson zlib + pacman -Sy --noconfirm cjson zlib libnghttp2 make clean make libchttpx.so cp libchttpx.so dist/archlinux/libchttpx.so @@ -77,7 +77,7 @@ runs: -w /work \ alpine:3.22 \ sh -euc ' - apk add --no-cache build-base cjson-dev zlib-dev + apk add --no-cache build-base cjson-dev zlib-dev nghttp2-dev make clean make libchttpx.so cp libchttpx.so dist/apk/libchttpx.so @@ -106,10 +106,10 @@ runs: "includedir=${prefix}/include", "", "Name: libchttpx", - "Description: A compact cross-platform HTTP/1.1 server library for C/C++.", + "Description: A compact cross-platform HTTP/2 server library for C/C++.", f"Version: {version}", "Libs: -L${libdir} -lchttpx", - "Libs.private: -lz", + "Libs.private: -lz -lnghttp2", "Cflags: -I${includedir}/libchttpx", "", ]) From 3d78db79de4a83380bf56f5bdf2aa9b9f4dcb70b Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:22 +0500 Subject: [PATCH 16/48] packaging: declare nghttp2 runtime dependency --- packaging/nfpm.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml index b8263b3..763f6f2 100644 --- a/packaging/nfpm.yaml +++ b/packaging/nfpm.yaml @@ -11,7 +11,7 @@ vendor: netcorelink homepage: https://github.com/netcorelink/libchttpx license: BSD-3-Clause description: | - Development package for libchttpx, a compact cross-platform HTTP/1.1 + Development package for libchttpx, a compact cross-platform HTTP/2 server library for C/C++ with routing, middleware, JSON, uploads, request-scoped memory, CORS, cookies, i18n, logging and graceful shutdown. @@ -77,15 +77,19 @@ overrides: depends: - libcjson1 - zlib1g + - libnghttp2-14 rpm: depends: - cjson - zlib + - libnghttp2 archlinux: depends: - cjson - zlib + - libnghttp2 apk: depends: - cjson - zlib + - nghttp2-libs From dfc65000a1e0619830ab0e54b8c54673cbbe8d20 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:25 +0500 Subject: [PATCH 17/48] fix: include stdio for internal dispatch adapter --- src/cHTTPX_response.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cHTTPX_response.h b/src/cHTTPX_response.h index bf2fd03..2eefbc9 100644 --- a/src/cHTTPX_response.h +++ b/src/cHTTPX_response.h @@ -16,6 +16,7 @@ extern "C" #include "cHTTPX_request.h" #include +#include struct chttpx_serv; From 5ac95eb967f74c52f3824ed9ddf5fa585c47ac67 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:27 +0500 Subject: [PATCH 18/48] packaging: expose nghttp2 linker dependency --- libchttpx.pc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libchttpx.pc b/libchttpx.pc index e9e0692..e4be4b1 100644 --- a/libchttpx.pc +++ b/libchttpx.pc @@ -6,5 +6,5 @@ Name: libchttpx Description: A powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers. Version: 1.4.1 Libs: -L${libdir} -lchttpx -Libs.private: -lz +Libs.private: -lz -lnghttp2 Cflags: -I${includedir}/libchttpx \ No newline at end of file From 82c23d550d1ba0ac7ae656f37452a424ae08b618 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:33:29 +0500 Subject: [PATCH 19/48] packaging: expose nghttp2 linker dependency for TLS builds From 4c7b2b93859b83503a333435c71e43521b66b6ec Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:34:36 +0500 Subject: [PATCH 20/48] feat: preserve HTTP/2 request and response headers --- src/cHTTPX_http2.c | 63 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index 253784c..bf174a2 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -54,6 +54,8 @@ typedef struct bool done; int status; char content_type[512]; + chttpx_header_t headers[MAX_HEADERS]; + size_t headers_count; unsigned char* body; size_t body_size; size_t body_capacity; @@ -800,11 +802,21 @@ static int h2_client_header(nghttp2_session* session, memcpy(status, value, 3); client->status = atoi(status); } - else if (namelen == 12 && memcmp(name, "content-type", 12) == 0) + else if (namelen > 0 && name[0] != ':' && client->headers_count < MAX_HEADERS && + namelen < MAX_HEADER_NAME && valuelen < MAX_HEADER_VALUE) { - size_t copy = valuelen < sizeof(client->content_type) - 1 ? valuelen : sizeof(client->content_type) - 1; - memcpy(client->content_type, value, copy); - client->content_type[copy] = '\0'; + chttpx_header_t* header = &client->headers[client->headers_count++]; + memcpy(header->name, name, namelen); + header->name[namelen] = '\0'; + memcpy(header->value, value, valuelen); + header->value[valuelen] = '\0'; + + if (namelen == 12 && memcmp(name, "content-type", 12) == 0) + { + size_t copy = valuelen < sizeof(client->content_type) - 1 ? valuelen : sizeof(client->content_type) - 1; + memcpy(client->content_type, value, copy); + client->content_type[copy] = '\0'; + } } return 0; } @@ -988,9 +1000,9 @@ int _chttpx_http2_call(chttpx_request_t* source, const char* selected_content_type = content_type && *content_type ? content_type : (source->content_type[0] ? source->content_type : cHTTPX_CTYPE_JSON); - const char* authorization = cHTTPX_HeaderGet(source, "Authorization"); - nghttp2_nv headers[10]; + nghttp2_nv headers[MAX_HEADERS + 8]; + char lowercase_names[MAX_HEADERS][MAX_HEADER_NAME]; size_t count = 0; headers[count++] = h2_nv(":method", method); headers[count++] = h2_nv(":scheme", remote.tls ? "https" : "http"); @@ -998,12 +1010,32 @@ int _chttpx_http2_call(chttpx_request_t* source, headers[count++] = h2_nv(":path", full_path); headers[count++] = h2_nv("content-type", selected_content_type); headers[count++] = h2_nv("content-length", content_length); - if (source->request_id[0]) + + if (source->request_id[0] && !cHTTPX_HeaderGet(source, "X-Request-ID")) headers[count++] = h2_nv("x-request-id", source->request_id); - if (source->language[0]) + if (source->language[0] && !cHTTPX_HeaderGet(source, "Accept-Language")) headers[count++] = h2_nv("accept-language", source->language); - if (authorization && *authorization) - headers[count++] = h2_nv("authorization", authorization); + + size_t copied_names = 0; + for (size_t i = 0; i < source->headers_count && count < CHTTPX_ARRAY_LEN(headers); i++) + { + const char* name = source->headers[i].name; + const char* value = source->headers[i].value; + if (!name[0] || name[0] == ':' || h2_forbidden_header(name) || + strcasecmp(name, "host") == 0 || + strcasecmp(name, "content-length") == 0 || + strcasecmp(name, "content-type") == 0) + continue; + + size_t name_len = strlen(name); + if (name_len >= MAX_HEADER_NAME || copied_names >= MAX_HEADERS) + continue; + for (size_t j = 0; j < name_len; j++) + lowercase_names[copied_names][j] = (char)tolower((unsigned char)name[j]); + lowercase_names[copied_names][name_len] = '\0'; + headers[count++] = h2_nv(lowercase_names[copied_names], value); + copied_names++; + } chttpx_h2_client_body_t request_body = { .body = body, @@ -1070,6 +1102,17 @@ int _chttpx_http2_call(chttpx_request_t* source, result = cHTTPX_ERR_MEMORY; goto done; } + + for (size_t i = 0; i < client.headers_count; i++) + { + if (cHTTPX_HeaderAdd(res, client.headers[i].name, client.headers[i].value) != 0) + { + cHTTPX_ResponseCleanup(res); + memset(res, 0, sizeof(*res)); + result = cHTTPX_ERR_LIMIT; + goto done; + } + } result = cHTTPX_OK; done: From 0d4956efd88a84f65cd39be600729c9f5d34ba6a Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:34:47 +0500 Subject: [PATCH 21/48] fix: keep idle sockets out of HTTP/2 worker pool --- src/cHTTPX_runtime.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c index f4ca581..640fcff 100644 --- a/src/cHTTPX_runtime.c +++ b/src/cHTTPX_runtime.c @@ -24,6 +24,7 @@ typedef enum { CHTTPX_CONN_TLS, + CHTTPX_CONN_HTTP2_READY, CHTTPX_CONN_READING_HEADERS, CHTTPX_CONN_READING_BODY, CHTTPX_CONN_PROCESSING, @@ -739,7 +740,16 @@ static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* c { int result = _chttpx_tls_accept_step(connection->tls_session); if (result == cHTTPX_OK) - return connection_submit(runtime, connection); + { + connection->state = CHTTPX_CONN_HTTP2_READY; + connection_touch(connection); + if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) + { + connection_close(runtime, connection); + return 0; + } + return 1; + } if (result == CHTTPX_IO_WANT_READ) { if (_chttpx_event_mod(runtime->event_loop, connection->fd, CHTTPX_EVENT_READ, connection) != 0) @@ -922,7 +932,9 @@ static void accept_connections(chttpx_runtime_t* runtime) if (!server->tls.enabled) { - connection_submit(runtime, connection); + connection->state = CHTTPX_CONN_HTTP2_READY; + if (_chttpx_event_add(runtime->event_loop, fd, CHTTPX_EVENT_READ, connection) != 0) + connection_close(runtime, connection); continue; } @@ -930,7 +942,9 @@ static void accept_connections(chttpx_runtime_t* runtime) int step = _chttpx_tls_accept_step(connection->tls_session); if (step == cHTTPX_OK) { - connection_submit(runtime, connection); + connection->state = CHTTPX_CONN_HTTP2_READY; + if (_chttpx_event_add(runtime->event_loop, fd, CHTTPX_EVENT_READ, connection) != 0) + connection_close(runtime, connection); continue; } if (step == CHTTPX_IO_WANT_READ) @@ -1077,6 +1091,8 @@ void _chttpx_runtime_listen(chttpx_serv_t* server) int alive = 1; if (connection->state == CHTTPX_CONN_TLS) alive = connection_tls_step(runtime, connection); + else if (connection->state == CHTTPX_CONN_HTTP2_READY) + alive = connection_submit(runtime, connection); else if (connection->state == CHTTPX_CONN_READING_HEADERS || connection->state == CHTTPX_CONN_READING_BODY) alive = connection_read_ready(runtime, connection); else if (connection->state == CHTTPX_CONN_WRITING) From 124307afda5980db2d21afaf484fa37b782c151d Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:36:04 +0500 Subject: [PATCH 22/48] test: run server integration suite over HTTP/2 --- tests/test_server.c | 184 +++++++++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 72 deletions(-) diff --git a/tests/test_server.c b/tests/test_server.c index 9deca49..0ec777d 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -1,4 +1,5 @@ #include "libchttpx.h" +#include "cHTTPX_http2.h" #include #include @@ -103,53 +104,86 @@ static void custom_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) static void exchange_family(uint16_t port, int family, const char* request, char* response, size_t response_size) { - chttpx_socket_t socket_fd = socket(family, SOCK_STREAM, 0); -#ifdef CHTTPX_PLATFORM_WINDOWS - assert(socket_fd != INVALID_SOCKET); -#else - assert(socket_fd >= 0); -#endif - - struct sockaddr_storage storage = {0}; - socklen_t address_size = 0; - - if (family == AF_INET) + assert(request && response && response_size > 0); + + const char* line_end = strstr(request, "\r\n"); + const char* header_end = strstr(request, "\r\n\r\n"); + assert(line_end && header_end); + + char first_line[CHTTPX_MAX_PATH + 64]; + size_t first_line_size = (size_t)(line_end - request); + assert(first_line_size < sizeof(first_line)); + memcpy(first_line, request, first_line_size); + first_line[first_line_size] = '\0'; + + char method[16]; + char path[CHTTPX_MAX_PATH]; + char protocol[16]; + assert(sscanf(first_line, "%15s %4095s %15s", method, path, protocol) == 3); + assert(strcmp(protocol, "HTTP/2") == 0); + + chttpx_request_t source = {0}; + const char* cursor = line_end + 2; + while (cursor < header_end) { - struct sockaddr_in* address = (struct sockaddr_in*)&storage; - address->sin_family = AF_INET; - address->sin_port = htons(port); - address->sin_addr.s_addr = htonl(INADDR_LOOPBACK); - address_size = sizeof(*address); + const char* next = strstr(cursor, "\r\n"); + assert(next && next <= header_end); + if (next == cursor) + break; + + const char* colon = memchr(cursor, ':', (size_t)(next - cursor)); + assert(colon); + size_t name_size = (size_t)(colon - cursor); + const char* value = colon + 1; + while (value < next && (*value == ' ' || *value == '\t')) + value++; + size_t value_size = (size_t)(next - value); + assert(source.headers_count < MAX_HEADERS); + assert(name_size < MAX_HEADER_NAME && value_size < MAX_HEADER_VALUE); + + chttpx_header_t* header = &source.headers[source.headers_count++]; + memcpy(header->name, cursor, name_size); + header->name[name_size] = '\0'; + memcpy(header->value, value, value_size); + header->value[value_size] = '\0'; + + if (strcasecmp(header->name, "Content-Type") == 0) + snprintf(source.content_type, sizeof(source.content_type), "%s", header->value); + cursor = next + 2; } + + const unsigned char* body = (const unsigned char*)header_end + 4; + size_t body_size = strlen((const char*)body); + char base_url[160]; + if (family == AF_INET6) + snprintf(base_url, sizeof(base_url), "http://[::1]:%u", port); else - { - struct sockaddr_in6* address = (struct sockaddr_in6*)&storage; - address->sin6_family = AF_INET6; - address->sin6_port = htons(port); - assert(inet_pton(AF_INET6, "::1", &address->sin6_addr) == 1); - address_size = sizeof(*address); - } + snprintf(base_url, sizeof(base_url), "http://127.0.0.1:%u", port); - assert(connect(socket_fd, (struct sockaddr*)&storage, address_size) == 0); - assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == cHTTPX_OK); + chttpx_response_t result = {0}; + assert(_chttpx_http2_call(&source, base_url, NULL, method, path, body, body_size, + source.content_type[0] ? source.content_type : NULL, &result) == cHTTPX_OK); -#ifdef CHTTPX_PLATFORM_WINDOWS - shutdown(socket_fd, SD_SEND); -#else - shutdown(socket_fd, SHUT_WR); -#endif + size_t used = 0; + int written = snprintf(response, response_size, "HTTP/2 %d %s\r\n", result.status, cHTTPX_StatusReason((uint16_t)result.status)); + assert(written > 0 && (size_t)written < response_size); + used = (size_t)written; - size_t total = 0; - while (total + 1 < response_size) + for (size_t i = 0; i < result.headers_count; i++) { - int received = recv(socket_fd, response + total, response_size - total - 1, 0); - if (received <= 0) - break; - total += (size_t)received; + written = snprintf(response + used, response_size - used, "%s: %s\r\n", result.headers[i].name, result.headers[i].value); + assert(written >= 0 && (size_t)written < response_size - used); + used += (size_t)written; } - response[total] = '\0'; - chttpx_close(socket_fd); + assert(used + 2 + result.body_size < response_size); + memcpy(response + used, "\r\n", 2); + used += 2; + if (result.body_size) + memcpy(response + used, result.body, result.body_size); + used += result.body_size; + response[used] = '\0'; + cHTTPX_ResponseCleanup(&result); } static void exchange(uint16_t port, const char* request, char* response, size_t response_size) @@ -157,6 +191,11 @@ static void exchange(uint16_t port, const char* request, char* response, size_t exchange_family(port, AF_INET, request, response, response_size); } +static void exchange_ipv6(uint16_t port, const char* request, char* response, size_t response_size) +{ + exchange_family(port, AF_INET6, request, response, response_size); +} + static chttpx_socket_t open_idle_connection(uint16_t port) { chttpx_socket_t socket_fd = socket(AF_INET, SOCK_STREAM, 0); @@ -280,145 +319,146 @@ int main(void) struct timespec concurrency_start; struct timespec concurrency_end; clock_gettime(CLOCK_MONOTONIC, &concurrency_start); - exchange(public_port, "GET /ip HTTP/1.1\r\nHost: localhost\r\n\r\n", response, sizeof(response)); + exchange(public_port, "GET /ip HTTP/2\r\nHost: localhost\r\n\r\n", response, sizeof(response)); clock_gettime(CLOCK_MONOTONIC, &concurrency_end); double concurrency_seconds = (double)(concurrency_end.tv_sec - concurrency_start.tv_sec) + (double)(concurrency_end.tv_nsec - concurrency_start.tv_nsec) / 1000000000.0; - assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); + assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(concurrency_seconds < 1.0); for (size_t i = 0; i < CHTTPX_ARRAY_LEN(idle_connections); i++) chttpx_close(idle_connections[i]); exchange_ipv6(public_port, - "GET /ip HTTP/1.1\r\n" + "GET /ip HTTP/2\r\n" "Host: localhost\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); + assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(strcmp(observed_client_ip, "::1") == 0); exchange(public_port, - "GET /ip HTTP/1.1\r\n" + "GET /ip HTTP/2\r\n" "Host: localhost\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); + assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(strcmp(observed_client_ip, "127.0.0.1") == 0); exchange(public_port, - "OPTIONS /body HTTP/1.1\r\n" + "OPTIONS /body HTTP/2\r\n" "Host: localhost\r\n" "Origin: https://example.com\r\n" "Access-Control-Request-Method: POST\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); - assert(strstr(response, "Access-Control-Allow-Origin: https://example.com") != NULL); + assert(strstr(response, "HTTP/2 204 No Content") != NULL); + assert(strstr(response, "access-control-allow-origin: https://example.com") != NULL); assert(__atomic_load_n(&options_handler_calls, __ATOMIC_SEQ_CST) == 0); exchange(public_port, - "OPTIONS /body HTTP/1.1\r\n" + "OPTIONS /body HTTP/2\r\n" "Host: localhost\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 204 No Content") != NULL); + assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(__atomic_load_n(&options_handler_calls, __ATOMIC_SEQ_CST) == 1); exchange(public_port, - "GET /empty HTTP/1.1\r\n" + "GET /empty HTTP/2\r\n" "Host: localhost\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 500 Internal Server Error") != NULL); - assert(strstr(response, "Connection: close") != NULL); + assert(strstr(response, "HTTP/2 500 Internal Server Error") != NULL); + assert(strstr(response, "connection:") == NULL); exchange(public_port, - "POST /body HTTP/1.1\r\n" + "POST /body HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: text/plain\r\n" - "Transfer-Encoding: chunked\r\n" + "Content-Length: 5\r\n" "Accept-Language: en;q=0.2, ru-RU;q=0.9\r\n" "X-Request-ID: integration-123\r\n" "\r\n" - "5\r\nhello\r\n0\r\n\r\n", + "hello", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 200 OK") != NULL); + assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(observed_body, "hello") == 0); assert(strcmp(observed_language, "ru") == 0); assert(strcmp(observed_request_id, "integration-123") == 0); exchange(public_port, - "POST /proxy HTTP/1.1\r\n" + "POST /proxy HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: application/json\r\n" "Content-Length: 18\r\n" "\r\n" "{\"plan\":\"premium\"}", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 200 OK") != NULL); + assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strstr(response, "\"server\":\"internal\"") != NULL); assert(strcmp(internal_observed_body, "{\"plan\":\"premium\"}") == 0); exchange(public_port, - "POST /proxy-remote HTTP/1.1\r\n" + "POST /proxy-remote HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: application/json\r\n" "Content-Length: 18\r\n" "\r\n" "{\"plan\":\"premium\"}", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 200 OK") != NULL); + assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strstr(response, "\"server\":\"internal\"") != NULL); assert(last_remote_call_result == cHTTPX_OK); exchange(public_port, - "POST /proxy-remote-error HTTP/1.1\r\n" + "POST /proxy-remote-error HTTP/2\r\n" "Host: localhost\r\n" "Content-Length: 0\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 500 Internal Server Error") != NULL); + assert(strstr(response, "HTTP/2 500 Internal Server Error") != NULL); assert(last_remote_call_result == cHTTPX_OK); exchange(public_port, - "POST /proxy-unavailable HTTP/1.1\r\n" + "POST /proxy-unavailable HTTP/2\r\n" "Host: localhost\r\n" "Content-Length: 0\r\n" "\r\n", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 503 Service Unavailable") != NULL); + assert(strstr(response, "HTTP/2 503 Service Unavailable") != NULL); assert(last_remote_call_result == cHTTPX_ERR_UNAVAILABLE); exchange(public_port, - "POST /proxy-custom HTTP/1.1\r\n" + "POST /proxy-custom HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: application/json\r\n" "Content-Length: 18\r\n" "\r\n" "{\"plan\":\"premium\"}", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 200 OK") != NULL); + assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(internal_observed_body, "{\"custom\":true}") == 0); exchange(internal_port, - "POST /process HTTP/1.1\r\n" + "POST /process HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: text/plain\r\n" "Content-Length: 4\r\n" "\r\n" "ping", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 200 OK") != NULL); + assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(internal_observed_body, "ping") == 0); exchange(public_port, - "POST /body HTTP/1.1\r\n" + "POST /body HTTP/2\r\n" "Host: localhost\r\n" "Content-Type: application/json\r\n" - "Content-Length: 100\r\n" - "\r\n", + "Content-Length: 40\r\n" + "\r\n" + "0123456789012345678901234567890123456789", response, sizeof(response)); - assert(strstr(response, "HTTP/1.1 413 Payload Too Large") != NULL); + assert(strstr(response, "HTTP/2 413 Payload Too Large") != NULL); cHTTPX_AppShutdown(&app); cHTTPX_AppShutdown(&remote_app); From b7e0eec76244de8beb0a495866e341d2e35edc1f Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:36:26 +0500 Subject: [PATCH 23/48] test: exercise metrics over HTTP/2 --- tests/test_metrics.c | 58 +++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/tests/test_metrics.c b/tests/test_metrics.c index 9f7a552..166a3a4 100644 --- a/tests/test_metrics.c +++ b/tests/test_metrics.c @@ -1,4 +1,5 @@ #include "libchttpx.h" +#include "cHTTPX_http2.h" #include #include @@ -65,42 +66,23 @@ static http_response_t exchange(uint16_t port, const char* path) http_response_t response; memset(&response, 0, sizeof(response)); - int fd = socket(AF_INET, SOCK_STREAM, 0); - assert(fd >= 0); - - struct sockaddr_in address; - memset(&address, 0, sizeof(address)); - address.sin_family = AF_INET; - address.sin_port = htons(port); - address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - assert(connect(fd, (struct sockaddr*)&address, sizeof(address)) == 0); - - char request[1024]; - int length = snprintf(request, - sizeof(request), - "GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", - path); - assert(length > 0 && (size_t)length < sizeof(request)); - assert(cHTTPX_SendAll(fd, request, (size_t)length) == cHTTPX_OK); - shutdown(fd, SHUT_WR); - - while (response.size + 1 < sizeof(response.bytes)) - { - int received = recv(fd, - response.bytes + response.size, - sizeof(response.bytes) - response.size - 1, - 0); - if (received <= 0) - break; - response.size += (size_t)received; - } + char base_url[128]; + snprintf(base_url, sizeof(base_url), "http://127.0.0.1:%u", port); + + chttpx_request_t source = {0}; + chttpx_response_t result = {0}; + assert(_chttpx_http2_call(&source, base_url, NULL, cHTTPX_MethodGet, path, NULL, 0, NULL, &result) == cHTTPX_OK); + + int written = snprintf(response.bytes, sizeof(response.bytes), "HTTP/2 %d %s\r\n\r\n", + result.status, cHTTPX_StatusReason((uint16_t)result.status)); + assert(written > 0 && (size_t)written < sizeof(response.bytes)); + response.header_size = (size_t)written; + assert(response.header_size + result.body_size < sizeof(response.bytes)); + if (result.body_size) + memcpy(response.bytes + response.header_size, result.body, result.body_size); + response.size = response.header_size + result.body_size; response.bytes[response.size] = '\0'; - close(fd); - - char* delimiter = strstr(response.bytes, "\r\n\r\n"); - assert(delimiter); - response.header_size = (size_t)(delimiter - response.bytes) + 4; + cHTTPX_ResponseCleanup(&result); return response; } @@ -112,7 +94,7 @@ static void* worker(void* data) char path[64]; snprintf(path, sizeof(path), "/users/%d", i); http_response_t response = exchange(ctx->port, path); - assert(strncmp(response.bytes, "HTTP/1.1 200 OK", 15) == 0); + assert(strncmp(response.bytes, "HTTP/2 200 OK", 15) == 0); } return NULL; } @@ -199,7 +181,7 @@ int main(void) char path[64]; snprintf(path, sizeof(path), "/route/%zu", i); http_response_t response = exchange(server->port, path); - assert(strncmp(response.bytes, "HTTP/1.1 200 OK", 15) == 0); + assert(strncmp(response.bytes, "HTTP/2 200 OK", 15) == 0); } __atomic_store_n(&snapshot_ctx.stop, 1, __ATOMIC_RELEASE); @@ -225,7 +207,7 @@ int main(void) assert(runtime_metrics.rejected_jobs_total == 0); http_response_t scrape = exchange(server->port, "/metrics"); - assert(strncmp(scrape.bytes, "HTTP/1.1 200 OK", 15) == 0); + assert(strncmp(scrape.bytes, "HTTP/2 200 OK", 15) == 0); const char* body = response_body(&scrape); assert(strstr(body, "# TYPE libchttpx_requests_total counter")); From 8bd40e1cd42372db06597836376b7efb803fb07e Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:36:45 +0500 Subject: [PATCH 24/48] test: exercise compression over HTTP/2 --- tests/test_compression.c | 156 ++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 61 deletions(-) diff --git a/tests/test_compression.c b/tests/test_compression.c index 8b8522d..bf93ced 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -1,4 +1,5 @@ #include "libchttpx.h" +#include "cHTTPX_http2.h" #include #include @@ -101,45 +102,78 @@ static test_response_t exchange(uint16_t port, const char* request) test_response_t response; memset(&response, 0, sizeof(response)); - chttpx_socket_t socket_fd = socket(AF_INET, SOCK_STREAM, 0); -#ifdef CHTTPX_PLATFORM_WINDOWS - assert(socket_fd != INVALID_SOCKET); -#else - assert(socket_fd >= 0); -#endif - - struct sockaddr_in address; - memset(&address, 0, sizeof(address)); - address.sin_family = AF_INET; - address.sin_port = htons(port); - address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - assert(connect(socket_fd, (struct sockaddr*)&address, sizeof(address)) == 0); - assert(cHTTPX_SendAll(socket_fd, request, strlen(request)) == cHTTPX_OK); - -#ifdef CHTTPX_PLATFORM_WINDOWS - shutdown(socket_fd, SD_SEND); -#else - shutdown(socket_fd, SHUT_WR); -#endif - - while (response.size < sizeof(response.bytes)) + const char* line_end = strstr(request, "\r\n"); + const char* header_end = strstr(request, "\r\n\r\n"); + assert(line_end && header_end); + + char first_line[CHTTPX_MAX_PATH + 64]; + size_t first_line_size = (size_t)(line_end - request); + assert(first_line_size < sizeof(first_line)); + memcpy(first_line, request, first_line_size); + first_line[first_line_size] = '\0'; + + char method[16]; + char path[CHTTPX_MAX_PATH]; + char protocol[16]; + assert(sscanf(first_line, "%15s %4095s %15s", method, path, protocol) == 3); + assert(strcmp(protocol, "HTTP/2") == 0); + + chttpx_request_t source = {0}; + const char* cursor = line_end + 2; + while (cursor < header_end) { - int received = recv(socket_fd, response.bytes + response.size, sizeof(response.bytes) - response.size, 0); - if (received <= 0) + const char* next = strstr(cursor, "\r\n"); + assert(next && next <= header_end); + if (next == cursor) break; - response.size += (size_t)received; + + const char* colon = memchr(cursor, ':', (size_t)(next - cursor)); + assert(colon); + size_t name_size = (size_t)(colon - cursor); + const char* value = colon + 1; + while (value < next && (*value == ' ' || *value == '\t')) + value++; + size_t value_size = (size_t)(next - value); + assert(source.headers_count < MAX_HEADERS); + assert(name_size < MAX_HEADER_NAME && value_size < MAX_HEADER_VALUE); + + chttpx_header_t* header = &source.headers[source.headers_count++]; + memcpy(header->name, cursor, name_size); + header->name[name_size] = '\0'; + memcpy(header->value, value, value_size); + header->value[value_size] = '\0'; + cursor = next + 2; } - chttpx_close(socket_fd); + char base_url[128]; + snprintf(base_url, sizeof(base_url), "http://127.0.0.1:%u", port); + chttpx_response_t result = {0}; + assert(_chttpx_http2_call(&source, base_url, NULL, method, path, NULL, 0, NULL, &result) == cHTTPX_OK); - const unsigned char* delimiter = find_bytes(response.bytes, response.size, "\r\n\r\n"); - assert(delimiter); + size_t used = 0; + int written = snprintf(response.headers, sizeof(response.headers), "HTTP/2 %d %s\r\n", + result.status, cHTTPX_StatusReason((uint16_t)result.status)); + assert(written > 0 && (size_t)written < sizeof(response.headers)); + used = (size_t)written; - response.header_size = (size_t)(delimiter - response.bytes) + 4; - assert(response.header_size < sizeof(response.headers)); - memcpy(response.headers, response.bytes, response.header_size); - response.headers[response.header_size] = '\0'; + for (size_t i = 0; i < result.headers_count; i++) + { + written = snprintf(response.headers + used, sizeof(response.headers) - used, "%s: %s\r\n", + result.headers[i].name, result.headers[i].value); + assert(written >= 0 && (size_t)written < sizeof(response.headers) - used); + used += (size_t)written; + } + assert(used + 2 < sizeof(response.headers)); + memcpy(response.headers + used, "\r\n", 3); + used += 2; + + response.header_size = used; + assert(response.header_size + result.body_size <= sizeof(response.bytes)); + memcpy(response.bytes, response.headers, response.header_size); + if (result.body_size) + memcpy(response.bytes + response.header_size, result.body, result.body_size); + response.size = response.header_size + result.body_size; + cHTTPX_ResponseCleanup(&result); return response; } @@ -166,17 +200,17 @@ static void assert_no_header(const test_response_t* response, const char* text) static void assert_identity_large(const test_response_t* response) { - assert_header(response, "HTTP/1.1 200 OK"); - assert_no_header(response, "Content-Encoding:"); + assert_header(response, "HTTP/2 200 OK"); + assert_no_header(response, "content-encoding:"); assert(response_body_size(response) == sizeof(large_text)); assert(memcmp(response_body(response), large_text, sizeof(large_text)) == 0); } static void assert_gzip_large(const test_response_t* response) { - assert_header(response, "HTTP/1.1 200 OK"); - assert_header(response, "Content-Encoding: gzip"); - assert_header(response, "Vary: Accept-Encoding"); + assert_header(response, "HTTP/2 200 OK"); + assert_header(response, "content-encoding: gzip"); + assert_header(response, "vary: Accept-Encoding"); assert(response_body_size(response) < sizeof(large_text)); unsigned char decompressed[LARGE_BODY_SIZE + 64]; @@ -248,74 +282,74 @@ int main(void) test_response_t response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_gzip_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0.5, identity;q=1\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0.5, identity;q=1\r\n\r\n"); assert_identity_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: br;q=1, gzip;q=0.8, identity;q=0.2\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: br;q=1, gzip;q=0.8, identity;q=0.2\r\n\r\n"); assert_gzip_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: GZIP ; q=1.0, identity;q=0\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: GZIP ; q=1.0, identity;q=0\r\n\r\n"); assert_gzip_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: *;q=1, identity;q=0\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: *;q=1, identity;q=0\r\n\r\n"); assert_gzip_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0\r\n\r\n"); assert_identity_large(&response); response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0, identity;q=0\r\n\r\n"); - assert_header(&response, "HTTP/1.1 406 Not Acceptable"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0, identity;q=0\r\n\r\n"); + assert_header(&response, "HTTP/2 406 Not Acceptable"); assert(response_body_size(&response) == 0); response = exchange( server->port, - "GET /small HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); - assert_header(&response, "HTTP/1.1 200 OK"); - assert_no_header(&response, "Content-Encoding:"); + "GET /small HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + assert_header(&response, "HTTP/2 200 OK"); + assert_no_header(&response, "content-encoding:"); response = exchange( server->port, - "GET /binary HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); - assert_header(&response, "HTTP/1.1 200 OK"); - assert_no_header(&response, "Content-Encoding:"); + "GET /binary HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + assert_header(&response, "HTTP/2 200 OK"); + assert_no_header(&response, "content-encoding:"); assert(response_body_size(&response) == sizeof(binary_body)); response = exchange( server->port, - "GET /response-disabled HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + "GET /response-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_identity_large(&response); response = exchange( server->port, - "GET /route-disabled HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + "GET /route-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_identity_large(&response); response = exchange( server->port, - "GET /encoded HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); - assert_header(&response, "Content-Encoding: br"); - assert_no_header(&response, "Content-Encoding: gzip"); + "GET /encoded HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + assert_header(&response, "content-encoding: br"); + assert_no_header(&response, "content-encoding: gzip"); response = exchange( server->port, - "GET /no-transform HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); - assert_header(&response, "Cache-Control: public, no-transform"); - assert_no_header(&response, "Content-Encoding:"); + "GET /no-transform HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + assert_header(&response, "cache-control: public, no-transform"); + assert_no_header(&response, "content-encoding:"); chttpx_compression_provider_t failing = { .encoding = "gzip", @@ -330,7 +364,7 @@ int main(void) response = exchange( server->port, - "GET /large HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip;q=1, identity;q=0.5\r\n\r\n"); + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=1, identity;q=0.5\r\n\r\n"); assert_identity_large(&response); cHTTPX_AppShutdown(&app); From 888842923f92c2fadaefe965ab1da90206cf5e27 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:37:29 +0500 Subject: [PATCH 25/48] fix: return HTTP/2 limits cleanly and stabilize response metadata --- src/cHTTPX_http2.c | 64 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index bf174a2..bfca13d 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -13,6 +13,10 @@ #include #include #include +#ifdef CHTTPX_PLATFORM_POSIX +#include +#include +#endif #define CHTTPX_H2_PROTOCOL "HTTP/2" #define CHTTPX_H2_CALL_TIMEOUT_SEC 30 @@ -31,6 +35,7 @@ typedef struct chttpx_h2_stream size_t body_capacity; size_t body_limit; bool responded; + int error_status; char* response; size_t response_size; size_t response_body_offset; @@ -122,7 +127,10 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, if (namelen == 7 && memcmp(name, ":method", 7) == 0) { if (valuelen >= sizeof(stream->method)) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + { + stream->error_status = cHTTPX_StatusBadRequest; + return 0; + } memcpy(stream->method, value, valuelen); stream->method[valuelen] = '\0'; return 0; @@ -131,7 +139,10 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, if (namelen == 5 && memcmp(name, ":path", 5) == 0) { if (valuelen >= sizeof(stream->path)) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + { + stream->error_status = cHTTPX_StatusURITooLong; + return 0; + } memcpy(stream->path, value, valuelen); stream->path[valuelen] = '\0'; return 0; @@ -140,7 +151,10 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, if (namelen == 10 && memcmp(name, ":authority", 10) == 0) { if (valuelen >= sizeof(stream->authority)) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + { + stream->error_status = cHTTPX_StatusRequestHeaderFieldsTooLarge; + return 0; + } memcpy(stream->authority, value, valuelen); stream->authority[valuelen] = '\0'; return 0; @@ -150,7 +164,10 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, return 0; if (stream->headers_count >= MAX_HEADERS || namelen >= MAX_HEADER_NAME || valuelen >= MAX_HEADER_VALUE) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + { + stream->error_status = cHTTPX_StatusRequestHeaderFieldsTooLarge; + return 0; + } chttpx_header_t* header = &stream->headers[stream->headers_count++]; memcpy(header->name, name, namelen); @@ -186,11 +203,17 @@ static int h2_append_body(chttpx_h2_server_t* connection, chttpx_h2_stream_t* st if (!stream || (len && !data)) return NGHTTP2_ERR_CALLBACK_FAILURE; + if (stream->error_status) + return 0; + if (stream->body_limit == 0) stream->body_limit = h2_body_limit(connection, stream); if (stream->body_size > stream->body_limit || len > stream->body_limit - stream->body_size) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + { + stream->error_status = cHTTPX_StatusPayloadTooLarge; + return 0; + } size_t required = stream->body_size + len + 1; if (required > stream->body_capacity) @@ -440,6 +463,9 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t if (!connection || !stream || stream->responded) return 0; + if (stream->error_status) + return h2_submit_text_response(connection, stream, stream->error_status, cHTTPX_CTYPE_TEXT); + char* headers = NULL; size_t header_size = 0; int build_result = h2_build_request_text(connection, stream, &headers, &header_size); @@ -763,6 +789,32 @@ static int h2_connect(const chttpx_h2_url_t* remote, chttpx_socket_t* connected) return final_result; } +static const char* h2_stable_content_type(const char* content_type) +{ + if (!content_type || !*content_type) + return cHTTPX_CTYPE_OCTET; +#define CHTTPX_H2_MATCH_CTYPE(value) if (strcasecmp(content_type, value) == 0) return value + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_HTML); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_TEXT); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_XML); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_CSS); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_CSV); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_JSON); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_FORM); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_MULTI); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_OCTET); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_JS); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_PNG); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_JPEG); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_GIF); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_WEBP); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_SVG); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_MP3); + CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_MP4); +#undef CHTTPX_H2_MATCH_CTYPE + return cHTTPX_CTYPE_OCTET; +} + static ssize_t h2_client_send(nghttp2_session* session, const uint8_t* data, size_t length, @@ -1094,7 +1146,7 @@ int _chttpx_http2_call(chttpx_request_t* source, } *res = cHTTPX_ResBinary((uint16_t)client.status, - client.content_type[0] ? client.content_type : cHTTPX_CTYPE_OCTET, + h2_stable_content_type(client.content_type), client.body, client.body_size); if (!res->status) From bba13a89a100a40c0bcdd146f6e20b6167ab821d Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:38:09 +0500 Subject: [PATCH 26/48] refactor: disable legacy connection-level WebSocket upgrade on HTTP/2 --- src/cHTTPX_websocket.c | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/cHTTPX_websocket.c b/src/cHTTPX_websocket.c index 4bddc0f..0dd5fc3 100644 --- a/src/cHTTPX_websocket.c +++ b/src/cHTTPX_websocket.c @@ -26,26 +26,15 @@ int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key) { - char accept_key[128] = {0}; - char buffer[256]; - - char key_concat[128]; - snprintf(key_concat, sizeof(key_concat), "%s258EAFA5-E914-47DA-95CA-C5AB0DC85B11", sec_wsocket_key); - - // unsigned char hash[20]; - // sha1((unsigned char*)key_concat, strlen(key_concat), hash); - - // base64_encode(hash, 20, accept_key); - - int n = snprintf(buffer, sizeof(buffer), - "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: %s\r\n\r\n", - accept_key); - send(client_socket, buffer, n, 0); - - return 0; + (void)client_socket; + (void)sec_wsocket_key; + + /* + * The legacy raw-socket upgrade API cannot identify an HTTP/2 stream. + * Keep the symbol for source compatibility and fail explicitly instead + * of emitting an invalid connection-level upgrade response. + */ + return cHTTPX_ERR_UNAVAILABLE; } // static wsocket_read_frame(chttpx_socket_t client_fd, wsocket_frame_t* out) From c47d755b95506e8a0d5be15c72b193456aee967d Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:38:11 +0500 Subject: [PATCH 27/48] docs: describe request protocol without legacy version --- src/cHTTPX_request.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h index f6b1400..04372d3 100644 --- a/src/cHTTPX_request.h +++ b/src/cHTTPX_request.h @@ -189,7 +189,7 @@ extern "C" /* User-Agent */ char user_agent[512]; - /* HTTP/1.1 HTTP/2 ... */ + /* HTTP protocol negotiated for this request. */ char protocol[16]; /* Client IP REQuest */ From 8a9d95ac9a4674ce7977401a5a8d8176c7e49686 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:38:13 +0500 Subject: [PATCH 28/48] docs: describe public request protocol as negotiated --- include/libchttpx.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/libchttpx.h b/include/libchttpx.h index 4703756..9bf3426 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -513,7 +513,7 @@ extern "C" /* User-Agent */ char user_agent[512]; - /* HTTP/1.1 HTTP/2 ... */ + /* HTTP protocol negotiated for this request. */ char protocol[16]; /* Client IP REQuest */ From 3dd61a6fd9aa4ef110786d21cb6e55eb6ce355e9 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:38:16 +0500 Subject: [PATCH 29/48] test: remove legacy protocol fixtures --- tests/test_core.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_core.c b/tests/test_core.c index 8d1724e..b45a741 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -179,7 +179,7 @@ static void test_multipart(void) static void test_header_boundary(void) { chttpx_request_t req = {0}; - char request[] = "POST /body HTTP/1.1\r\n" + char request[] = "POST /body HTTP/2\r\n" "Host: localhost\r\n" "Content-Length: 13\r\n\r\n" "Fake: header\r\n"; @@ -205,7 +205,7 @@ static void test_request_framing(void) strcpy(req.headers[1].name, "Content-Length"); strcpy(req.headers[1].value, "6"); req.headers_count = 2; - char request[] = "POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello"; + char request[] = "POST / HTTP/2\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello"; _parse_req_body(&req, 0, request, strlen(request)); assert(req._parse_status == cHTTPX_StatusBadRequest); @@ -235,7 +235,7 @@ static void test_streamed_raw_chunked_upload(void) strcpy(req.headers[1].value, "chunked"); req.headers_count = 2; - char request[] = "POST /upload HTTP/1.1\r\n" + char request[] = "POST /upload HTTP/2\r\n" "Content-Type: application/octet-stream\r\n" "Transfer-Encoding: chunked\r\n\r\n" "8\r\nRAW-DATA\r\n0\r\n\r\n"; @@ -281,7 +281,7 @@ static void test_streamed_multipart(void) char request[2048]; int body_size = (int)strlen(body); int request_size = snprintf(request, sizeof(request), - "POST /upload HTTP/1.1\r\nContent-Type: multipart/form-data; boundary=StreamBoundary\r\n" + "POST /upload HTTP/2\r\nContent-Type: multipart/form-data; boundary=StreamBoundary\r\n" "Content-Length: %d\r\n\r\n%s", body_size, body); assert(request_size > 0 && (size_t)request_size < sizeof(request)); @@ -314,7 +314,7 @@ static void test_streamed_multipart(void) assert(chunked_body_size > 0 && (size_t)chunked_body_size < sizeof(chunked_body)); request_size = snprintf(request, sizeof(request), - "POST /upload HTTP/1.1\r\nContent-Type: multipart/form-data; boundary=StreamBoundary\r\n" + "POST /upload HTTP/2\r\nContent-Type: multipart/form-data; boundary=StreamBoundary\r\n" "Transfer-Encoding: chunked\r\n\r\n%s", chunked_body); assert(request_size > 0 && (size_t)request_size < sizeof(request)); From 3d5f28a3aa8ddce84f750e17922775e17f53681f Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:03 +0500 Subject: [PATCH 30/48] docs: describe HTTP/2 transport --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f33b116..d50c75f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # libchttpx -`libchttpx` is a compact cross-platform HTTP/1.1 server library for C. It provides an App-based runtime, routing, middleware, request parsing, JSON binding and responses, uploads, request-scoped memory, CORS, cookies, i18n, logging, rate limiting, and graceful shutdown while keeping a direct C-style API. +`libchttpx` is a compact cross-platform HTTP/2 server library for C. It provides an App-based runtime, routing, middleware, request parsing, JSON binding and responses, uploads, request-scoped memory, CORS, cookies, i18n, logging, rate limiting, and graceful shutdown while keeping a direct C-style API. The library is designed so handlers contain application logic instead of repetitive HTTP plumbing. From 99a9615d924f67ef18f29e5594d825ea3406e200 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:05 +0500 Subject: [PATCH 31/48] docs: describe HTTP/2 transport in Russian --- README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_RU.md b/README_RU.md index 0d62dac..23ae597 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,6 @@ # libchttpx -`libchttpx` — компактная кроссплатформенная HTTP/1.1-библиотека для C. Она предоставляет App-runtime, routing, middleware, разбор запросов, JSON binding/response, uploads, request-scoped память, CORS, cookies, i18n, logging, rate limiting и graceful shutdown, сохраняя простой C-style API. +`libchttpx` — компактная кроссплатформенная HTTP/2-библиотека для C. Она предоставляет App-runtime, routing, middleware, разбор запросов, JSON binding/response, uploads, request-scoped память, CORS, cookies, i18n, logging, rate limiting и graceful shutdown, сохраняя простой C-style API. Идея библиотеки: handler должен содержать бизнес-логику приложения, а не повторяющийся HTTP boilerplate. From 6f68ea5bf326bf841a3f5992392dd6026c3e2cc4 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:23 +0500 Subject: [PATCH 32/48] docs: update server HTTP/2 model --- docs/server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server/README.md b/docs/server/README.md index 34becd8..2ea17c3 100644 --- a/docs/server/README.md +++ b/docs/server/README.md @@ -151,4 +151,4 @@ The worker count is intentionally not part of `chttpx_config_t` and cannot be ch ## HTTP model -Current server behavior is HTTP/1.1 with one request per connection and explicit `Connection: close`. Fixed `Content-Length` and chunked request bodies are supported. +Current server behavior is HTTP/2. Cleartext servers use h2c prior knowledge; TLS servers negotiate `h2` with ALPN. Multiple HTTP/2 streams may share one connection, while the public App/router/handler API remains unchanged. From aa4bb35870698bd4c68cccdbbc16b33724ff5869 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:25 +0500 Subject: [PATCH 33/48] docs: update Russian server HTTP/2 model --- docs/server/README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server/README_RU.md b/docs/server/README_RU.md index 8da1e50..71ec194 100644 --- a/docs/server/README_RU.md +++ b/docs/server/README_RU.md @@ -139,4 +139,4 @@ Routes/middleware настраивайте до `AppStart/AppRun`. Во врем ## HTTP model -Текущая реализация работает по HTTP/1.1 с одним request на connection и явным `Connection: close`. Поддерживаются `Content-Length` и chunked request bodies. +Текущая реализация работает по HTTP/2. Для соединений без TLS используется h2c prior knowledge, а TLS-серверы согласовывают `h2` через ALPN. Несколько HTTP/2 stream могут использовать одно соединение, при этом публичный App/router/handler API не меняется. From e1edfee94a08bd749a6ee07ba510261015d16610 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:27 +0500 Subject: [PATCH 34/48] docs: update CORS example to HTTP/2 --- docs/cors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cors/README.md b/docs/cors/README.md index 95d6ee9..ac1649c 100644 --- a/docs/cors/README.md +++ b/docs/cors/README.md @@ -45,7 +45,7 @@ The server copies CORS configuration into its own state. A CORS preflight looks like: ```http -OPTIONS /api/v2/users HTTP/1.1 +OPTIONS /api/v2/users HTTP/2 Origin: https://example.com Access-Control-Request-Method: POST ``` From 8a5d490965ca3fabed2168bc0553b0c7a0f512d8 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:29 +0500 Subject: [PATCH 35/48] docs: update Russian CORS example to HTTP/2 --- docs/cors/README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cors/README_RU.md b/docs/cors/README_RU.md index 637d70c..810fc8c 100644 --- a/docs/cors/README_RU.md +++ b/docs/cors/README_RU.md @@ -42,7 +42,7 @@ CORS config копируется в server state. ## Preflight ```http -OPTIONS /api/v2/users HTTP/1.1 +OPTIONS /api/v2/users HTTP/2 Origin: https://example.com Access-Control-Request-Method: POST ``` From e14fcb81e2866ceb5d3ca2fe8c529b4d3cb69b35 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:39:31 +0500 Subject: [PATCH 36/48] docs: advertise HTTP/2 on documentation site --- docs/site/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/site/index.html b/docs/site/index.html index 2109e65..c2da6bc 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -4,7 +4,7 @@ libchttpx — HTTP servers in C - + @@ -37,7 +37,7 @@

A fast and simple HTTP server library for C

Build HTTP servers in C, the modern way

- libchttpx is a compact HTTP/1.1 server library with routing, middleware, + libchttpx is a compact HTTP/2 server library with routing, middleware, JSON helpers, uploads, TLS, compression, built-in metrics and graceful shutdown.

From 531d79fa37b89048dad600988eca849b504e6b6b Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:41:37 +0500 Subject: [PATCH 37/48] docs: document nghttp2 build dependency --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d50c75f..574b948 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Pull the published runtime image: docker pull noneandundefined/libchttpx:latest ``` -The image contains the installed shared library, headers, pkg-config metadata, and the cJSON runtime. +The image contains the installed shared library, headers, pkg-config metadata, and the cJSON, zlib, and nghttp2 runtimes. ```dockerfile FROM noneandundefined/libchttpx:latest @@ -126,11 +126,11 @@ CMD ["/usr/local/bin/my-server"] ### Build from source on Linux -Requirements: GCC, Make, pkg-config, cJSON development files, and zlib development files. +Requirements: GCC, Make, pkg-config, cJSON development files, zlib development files, and nghttp2 development files. ```bash sudo apt update -sudo apt install -y build-essential pkg-config libcjson-dev zlib1g-dev +sudo apt install -y build-essential pkg-config libcjson-dev zlib1g-dev libnghttp2-dev git clone https://github.com/netcorelink/libchttpx.git cd libchttpx @@ -166,7 +166,7 @@ TLS remains independently optional: `make TLS=1 libchttpx.so`. See [Response com ### Build from source on Windows -Use MinGW/GCC. The Windows build uses the bundled `lib/cjson` source and requires zlib (for MSYS2/MinGW64: `mingw-w64-x86_64-zlib`). +Use MinGW/GCC. The Windows build uses the bundled `lib/cjson` source and requires zlib and nghttp2 (for MSYS2/MinGW64: `mingw-w64-x86_64-zlib` and `mingw-w64-x86_64-nghttp2`). ```powershell git clone https://github.com/netcorelink/libchttpx.git From 460f2f4035cd4560164b7701d5b38bccc8a43da0 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:41:39 +0500 Subject: [PATCH 38/48] docs: document nghttp2 dependency in Russian --- README_RU.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 23ae597..dcbd52d 100644 --- a/README_RU.md +++ b/README_RU.md @@ -113,7 +113,7 @@ iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install docker pull noneandundefined/libchttpx:latest ``` -Image содержит установленную shared library, headers, pkg-config metadata и runtime cJSON. +Image содержит установленную shared library, headers, pkg-config metadata и runtime-зависимости cJSON, zlib и nghttp2. ```dockerfile FROM noneandundefined/libchttpx:latest @@ -126,7 +126,7 @@ CMD ["/usr/local/bin/my-server"] ```bash sudo apt update -sudo apt install -y build-essential pkg-config libcjson-dev zlib1g-dev +sudo apt install -y build-essential pkg-config libcjson-dev zlib1g-dev libnghttp2-dev git clone https://github.com/netcorelink/libchttpx.git cd libchttpx @@ -162,7 +162,7 @@ TLS остаётся отдельной опцией сборки: `make TLS=1 l ### Самостоятельная сборка на Windows -Используется MinGW/GCC. Для Windows cJSON уже находится в `lib/cjson`, а zlib должен быть установлен (для MSYS2/MinGW64: `mingw-w64-x86_64-zlib`). +Используется MinGW/GCC. Для Windows cJSON уже находится в `lib/cjson`, а zlib и nghttp2 должны быть установлены (для MSYS2/MinGW64: `mingw-w64-x86_64-zlib` и `mingw-w64-x86_64-nghttp2`). ```powershell git clone https://github.com/netcorelink/libchttpx.git From c1ce1d9b1cc4d512ccf1e9b12024052f7364a68b Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:42:06 +0500 Subject: [PATCH 39/48] test: remove duplicate HTTP/2 IPv6 helper --- tests/test_server.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_server.c b/tests/test_server.c index 0ec777d..a8b9808 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -213,11 +213,6 @@ static chttpx_socket_t open_idle_connection(uint16_t port) } -static void exchange_ipv6(uint16_t port, const char* request, char* response, size_t response_size) -{ - exchange_family(port, AF_INET6, request, response, response_size); -} - static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) From be6735050b08da9797efa0ef96a355bf3a30ab4a Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:45:41 +0500 Subject: [PATCH 40/48] fix: avoid duplicate HTTP/2 representation headers in proxied responses --- src/cHTTPX_http2.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index bfca13d..73168a2 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -854,20 +854,32 @@ static int h2_client_header(nghttp2_session* session, memcpy(status, value, 3); client->status = atoi(status); } - else if (namelen > 0 && name[0] != ':' && client->headers_count < MAX_HEADERS && - namelen < MAX_HEADER_NAME && valuelen < MAX_HEADER_VALUE) + else if (namelen > 0 && name[0] != ':') { - chttpx_header_t* header = &client->headers[client->headers_count++]; - memcpy(header->name, name, namelen); - header->name[namelen] = '\0'; - memcpy(header->value, value, valuelen); - header->value[valuelen] = '\0'; - if (namelen == 12 && memcmp(name, "content-type", 12) == 0) { size_t copy = valuelen < sizeof(client->content_type) - 1 ? valuelen : sizeof(client->content_type) - 1; memcpy(client->content_type, value, copy); client->content_type[copy] = '\0'; + return 0; + } + + /* + * chttpx_response_t owns representation metadata separately from the + * generic header array. Copying Content-Length here would make a + * proxied response emit two content-length fields when it is serialized + * again by the receiving server. + */ + if (namelen == 14 && memcmp(name, "content-length", 14) == 0) + return 0; + + if (client->headers_count < MAX_HEADERS && namelen < MAX_HEADER_NAME && valuelen < MAX_HEADER_VALUE) + { + chttpx_header_t* header = &client->headers[client->headers_count++]; + memcpy(header->name, name, namelen); + header->name[namelen] = '\0'; + memcpy(header->value, value, valuelen); + header->value[valuelen] = '\0'; } } return 0; From b65e2686dd4c03756e6e49d851a84d056faba6c2 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:47:57 +0500 Subject: [PATCH 41/48] test: use HTTP/2 status line length in metrics assertions --- tests/test_metrics.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_metrics.c b/tests/test_metrics.c index 166a3a4..54a2574 100644 --- a/tests/test_metrics.c +++ b/tests/test_metrics.c @@ -94,7 +94,7 @@ static void* worker(void* data) char path[64]; snprintf(path, sizeof(path), "/users/%d", i); http_response_t response = exchange(ctx->port, path); - assert(strncmp(response.bytes, "HTTP/2 200 OK", 15) == 0); + assert(strncmp(response.bytes, "HTTP/2 200 OK", strlen("HTTP/2 200 OK")) == 0); } return NULL; } @@ -181,7 +181,7 @@ int main(void) char path[64]; snprintf(path, sizeof(path), "/route/%zu", i); http_response_t response = exchange(server->port, path); - assert(strncmp(response.bytes, "HTTP/2 200 OK", 15) == 0); + assert(strncmp(response.bytes, "HTTP/2 200 OK", strlen("HTTP/2 200 OK")) == 0); } __atomic_store_n(&snapshot_ctx.stop, 1, __ATOMIC_RELEASE); @@ -207,7 +207,7 @@ int main(void) assert(runtime_metrics.rejected_jobs_total == 0); http_response_t scrape = exchange(server->port, "/metrics"); - assert(strncmp(scrape.bytes, "HTTP/2 200 OK", 15) == 0); + assert(strncmp(scrape.bytes, "HTTP/2 200 OK", strlen("HTTP/2 200 OK")) == 0); const char* body = response_body(&scrape); assert(strstr(body, "# TYPE libchttpx_requests_total counter")); From 70389be1896979777e909d8cdbefc99f33bc090d Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 01:51:10 +0500 Subject: [PATCH 42/48] fix: synchronize HTTP/2 shutdown flag access --- src/cHTTPX_http2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index 73168a2..03a1291 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -633,7 +633,7 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* unsigned char buffer[BUFFER_SIZE]; int result = cHTTPX_OK; - while (!server->shutdown_requested && (nghttp2_session_want_read(connection.session) || nghttp2_session_want_write(connection.session))) + while (!__atomic_load_n(&server->shutdown_requested, __ATOMIC_ACQUIRE) && (nghttp2_session_want_read(connection.session) || nghttp2_session_want_write(connection.session))) { int received = _chttpx_io_recv(client_fd, tls_session, buffer, sizeof(buffer)); if (received == 0) From f176ced05a8d08b0b1cb5aab2963caeb108bdee0 Mon Sep 17 00:00:00 2001 From: noneandundefined Date: Fri, 25 Sep 2026 02:45:57 +0500 Subject: [PATCH 43/48] refactor: refactor arch. code in full project --- docs/site/README.md | 8 +- docs/site/about.html | 109 ++-- docs/site/app.js | 294 ++++++++-- docs/site/blog.html | 97 ++-- docs/site/docs.html | 254 +++----- docs/site/examples.html | 144 ++--- docs/site/index.html | 208 +++---- docs/site/module.html | 84 +-- docs/site/module.js | 66 +-- docs/site/styles.css | 1116 +++++++++++++++++------------------- docs/site/support.html | 106 ++-- example/basic.c | 2 + example/compression.c | 3 + example/json.c | 22 +- example/local_call.c | 11 +- example/metrics.c | 5 +- example/middleware.c | 3 + example/multiple_servers.c | 3 + example/remote_call.c | 10 +- example/tls.c | 2 + example/upload.c | 11 +- include/libchttpx.h | 80 +-- src/cHTTPX_app.c | 196 +++++++ src/cHTTPX_app.h | 6 + src/cHTTPX_body.c | 102 +++- src/cHTTPX_body.h | 19 +- src/cHTTPX_compression.c | 204 ++++++- src/cHTTPX_compression.h | 7 +- src/cHTTPX_cookies.c | 33 +- src/cHTTPX_cookies.h | 48 +- src/cHTTPX_cors.h | 14 +- src/cHTTPX_crosspltm.h | 33 +- src/cHTTPX_event.c | 268 +++++++++ src/cHTTPX_event.h | 33 ++ src/cHTTPX_headers.c | 39 +- src/cHTTPX_headers.h | 30 +- src/cHTTPX_http.h | 1 - src/cHTTPX_http2.c | 410 +++++++++---- src/cHTTPX_http2.h | 32 +- src/cHTTPX_i18n.c | 33 +- src/cHTTPX_i18n.h | 50 +- src/cHTTPX_json.c | 65 +++ src/cHTTPX_json.h | 121 +++- src/cHTTPX_media.c | 165 +++++- src/cHTTPX_media.h | 9 +- src/cHTTPX_metrics.c | 95 ++- src/cHTTPX_metrics.h | 7 +- src/cHTTPX_middlewares.c | 56 +- src/cHTTPX_middlewares.h | 8 +- src/cHTTPX_params.c | 40 +- src/cHTTPX_params.h | 34 +- src/cHTTPX_queries.c | 77 ++- src/cHTTPX_queries.h | 71 ++- src/cHTTPX_request.c | 37 ++ src/cHTTPX_request.h | 31 +- src/cHTTPX_response.c | 138 +++++ src/cHTTPX_response.h | 15 +- src/cHTTPX_runtime.c | 242 ++++++++ src/cHTTPX_runtime.h | 32 ++ src/cHTTPX_serv.c | 155 +++++ src/cHTTPX_serv.h | 33 ++ src/cHTTPX_tls.c | 139 ++++- src/cHTTPX_tls.h | 70 ++- src/cHTTPX_utils.h | 44 ++ src/cHTTPX_websocket.c | 19 +- src/cHTTPX_websocket.h | 36 +- src/cHTTPX_worker.c | 94 ++- src/cHTTPX_worker.h | 31 +- tests/fuzz_headers.c | 7 + tests/test_compression.c | 90 ++- tests/test_core.c | 18 +- tests/test_metrics.c | 9 + tests/test_server.c | 111 ++-- tests/test_tls.c | 2 + 74 files changed, 4421 insertions(+), 1876 deletions(-) diff --git a/docs/site/README.md b/docs/site/README.md index c8ca670..c3f364a 100644 --- a/docs/site/README.md +++ b/docs/site/README.md @@ -2,6 +2,9 @@ Static project website for GitHub Pages. +The documentation layout follows a Mongoose-style docs UI: sticky header, +collapsible left sidebar with search, chevron breadcrumbs, and dark code blocks. + ## Files - `index.html` — landing page @@ -12,9 +15,8 @@ Static project website for GitHub Pages. - `blog.html` — project updates and release links - `support.html` — community, commercial and sponsorship support - `about.html` — project information -- `styles.css` — shared styles -- `app.js` — mobile navigation, copy buttons, docs filter and GitHub repository stats -- `styles.css` — shared styles and DM Sans typography +- `styles.css` — shared documentation layout (sidebar, breadcrumbs, dark code) +- `app.js` — navigation, sidebar, copy buttons, syntax highlighting and GitHub stats The site has no framework. GitHub Pages has a small build step that recreates `docs/site/content` from the canonical module READMEs under `docs/`, so website documentation cannot drift from repository documentation. diff --git a/docs/site/about.html b/docs/site/about.html index 69e832a..eaa50a1 100644 --- a/docs/site/about.html +++ b/docs/site/about.html @@ -10,48 +10,45 @@ -
-
-
-

About

-

A small C library focused on practical HTTP server work

-

- libchttpx is maintained by netcorelink and developed in public on GitHub. -

-
-
+
+ +
+
+ -
-
-
-

Project goal

-

- Keep the API direct and C-like while providing the pieces commonly needed by real backend services: - routing, middleware, request parsing, JSON, uploads, CORS, cookies, logging, rate limiting and graceful shutdown. -

- -
+

About

+

+ libchttpx is a small C library focused on practical HTTP server work. + It is maintained by netcorelink and developed in public on GitHub. +

+

Project goal

+

+ Keep the API direct and C-like while providing the pieces commonly needed + by real backend services: routing, middleware, request parsing, JSON, + uploads, CORS, cookies, logging, rate limiting and graceful shutdown. +

  • Written primarily in C
  • Cross-platform Linux and Windows support
  • @@ -60,32 +57,20 @@

    Project goal

  • Examples included in the repository
  • Documentation versioned with the code
-
-
-
-

Maintained in public

-

- Bugs, changes and releases are tracked on GitHub. This website is intentionally a simple front door to the project, - while the repository remains the source of truth. +

+ Bugs, changes and releases are tracked on GitHub. This website is a + front door to the project; the repository remains the source of truth.

-
-

Source

Read the implementation and follow development.

GitHub ↗
-

Issues

Report a bug or propose a feature.

Issues ↗
- -
-
-
-
- - +

+ Source · + Issues · + Releases · + License +

+ +
+ diff --git a/docs/site/app.js b/docs/site/app.js index 3d4d35b..3460630 100644 --- a/docs/site/app.js +++ b/docs/site/app.js @@ -4,17 +4,72 @@ var REPO = "netcorelink/libchttpx"; var API = "https://api.github.com/repos/" + REPO; + window.CHTTPX_DOCS = [ + { + title: "Getting started", + items: [ + { href: "docs.html", label: "Overview", page: "docs" }, + { href: "docs.html#install", label: "Installation", page: "docs" }, + { href: "docs.html#first-server", label: "First server", page: "docs" }, + { href: "docs.html#compile", label: "Compile", page: "docs" } + ] + }, + { + title: "Modules", + items: [ + { href: "module.html?name=app", id: "app", label: "App runtime" }, + { href: "module.html?name=server", id: "server", label: "Server" }, + { href: "module.html?name=tls", id: "tls", label: "TLS / HTTPS" }, + { href: "module.html?name=compression", id: "compression", label: "Compression" }, + { href: "module.html?name=metrics", id: "metrics", label: "Metrics" }, + { href: "module.html?name=routing", id: "routing", label: "Routing" }, + { href: "module.html?name=middleware", id: "middleware", label: "Middleware" }, + { href: "module.html?name=request", id: "request", label: "Request" }, + { href: "module.html?name=responses", id: "responses", label: "Responses" }, + { href: "module.html?name=json", id: "json", label: "JSON" }, + { href: "module.html?name=memory", id: "memory", label: "Memory" }, + { href: "module.html?name=uploads", id: "uploads", label: "Uploads" }, + { href: "module.html?name=cors", id: "cors", label: "CORS" }, + { href: "module.html?name=cookies", id: "cookies", label: "Cookies" }, + { href: "module.html?name=i18n", id: "i18n", label: "Request IDs & i18n" }, + { href: "module.html?name=logging", id: "logging", label: "Logging" }, + { href: "module.html?name=rate-limiting", id: "rate-limiting", label: "Rate limiting" }, + { href: "module.html?name=websocket", id: "websocket", label: "WebSocket" } + ] + }, + { + title: "More", + items: [ + { href: "examples.html", label: "Examples", page: "examples" }, + { href: "blog.html", label: "Updates", page: "blog" }, + { href: "support.html", label: "Support", page: "support" }, + { href: "about.html", label: "About", page: "about" } + ] + } + ]; + + var currentPage = window.location.pathname.split("/").pop() || "index.html"; + var currentModule = new URLSearchParams(window.location.search).get("name") || ""; + var menuButton = document.querySelector("[data-menu-button]"); var navLinks = document.querySelector("[data-nav-links]"); + var sidebar = document.querySelector("[data-docs-sidebar]"); - if (menuButton && navLinks) { + if (menuButton) { menuButton.addEventListener("click", function () { - var open = navLinks.classList.toggle("open"); - menuButton.setAttribute("aria-expanded", String(open)); + if (window.matchMedia("(max-width: 900px)").matches && sidebar) { + var open = sidebar.classList.toggle("open"); + menuButton.setAttribute("aria-expanded", String(open)); + if (navLinks) navLinks.classList.remove("open"); + return; + } + if (navLinks) { + var shown = navLinks.classList.toggle("open"); + menuButton.setAttribute("aria-expanded", String(shown)); + } }); } - var currentPage = window.location.pathname.split("/").pop() || "index.html"; document.querySelectorAll("[data-nav-link]").forEach(function (link) { var href = link.getAttribute("href"); if (href === currentPage || (currentPage === "" && href === "index.html")) { @@ -22,16 +77,170 @@ } }); - document.querySelectorAll("[data-copy-target]").forEach(function (button) { - button.addEventListener("click", function () { - var targetId = button.getAttribute("data-copy-target"); - var target = document.getElementById(targetId); - if (!target) { - return; + function linkActive(item) { + if (item.id) return currentPage === "module.html" && item.id === currentModule; + if (item.page === "docs") { + if (currentPage !== "docs.html") return false; + var hash = item.href.indexOf("#") >= 0 ? "#" + item.href.split("#")[1] : ""; + if (hash) return window.location.hash === hash; + return !window.location.hash || window.location.hash === "#"; + } + if (item.page) return currentPage === item.page + ".html"; + return item.href === currentPage; + } + + function groupHasActive(group) { + return group.items.some(function (item) { + return linkActive(item); + }); + } + + window.renderDocsSidebar = function (extraChildren) { + var root = document.querySelector("[data-docs-sidebar]"); + if (!root) return; + + root.innerHTML = ""; + + var search = document.createElement("input"); + search.type = "search"; + search.className = "sidebar-search"; + search.placeholder = "Search... (/)"; + search.setAttribute("aria-label", "Search documentation"); + search.setAttribute("data-sidebar-search", ""); + root.appendChild(search); + + window.CHTTPX_DOCS.forEach(function (group, index) { + var block = document.createElement("div"); + block.className = "nav-group"; + if (groupHasActive(group) || index === 0) block.classList.add("open"); + + var toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "nav-group-toggle"; + toggle.setAttribute("aria-expanded", block.classList.contains("open") ? "true" : "false"); + + var label = document.createElement("span"); + label.textContent = group.title; + toggle.appendChild(label); + + var icon = document.createElement("span"); + icon.className = "nav-toggle-icon"; + icon.setAttribute("aria-hidden", "true"); + toggle.appendChild(icon); + + toggle.addEventListener("click", function () { + block.classList.toggle("open"); + toggle.setAttribute("aria-expanded", block.classList.contains("open") ? "true" : "false"); + }); + block.appendChild(toggle); + + var items = document.createElement("div"); + items.className = "nav-group-items"; + + group.items.forEach(function (item) { + var link = document.createElement("a"); + link.href = item.href; + link.textContent = item.label; + link.setAttribute("data-nav-item", ""); + if (linkActive(item)) link.classList.add("active"); + items.appendChild(link); + + if (extraChildren && extraChildren.length && item.id === currentModule) { + extraChildren.forEach(function (child) { + var nested = document.createElement("a"); + nested.href = child.href; + nested.className = "nav-child"; + nested.setAttribute("data-nav-item", ""); + nested.textContent = child.label; + items.appendChild(nested); + }); + } + }); + + block.appendChild(items); + root.appendChild(block); + }); + + search.addEventListener("input", function () { + var query = search.value.trim().toLowerCase(); + root.querySelectorAll(".nav-group").forEach(function (group) { + var matches = 0; + group.querySelectorAll("[data-nav-item]").forEach(function (item) { + var show = !query || item.textContent.toLowerCase().includes(query); + item.classList.toggle("hidden", !show); + if (show) matches += 1; + }); + group.classList.toggle("hidden", query && matches === 0); + if (query && matches > 0) { + group.classList.add("open"); + var toggleBtn = group.querySelector(".nav-group-toggle"); + if (toggleBtn) toggleBtn.setAttribute("aria-expanded", "true"); + } + }); + }); + }; + + if (!window.__chttpxSlashBound) { + window.__chttpxSlashBound = true; + document.addEventListener("keydown", function (event) { + var search = document.querySelector("[data-sidebar-search]"); + if (!search) return; + if (event.key === "/" && document.activeElement !== search && + document.activeElement.tagName !== "INPUT" && + document.activeElement.tagName !== "TEXTAREA") { + event.preventDefault(); + search.focus(); + } + }); + } + + if (sidebar && currentPage !== "module.html") { + window.renderDocsSidebar(); + } + + function escapeHtml(text) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function highlightC(source) { + var escaped = escapeHtml(source); + var parts = escaped.split(/(\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^&])*?"|'(?:\\.|[^&])*?')/g); + return parts.map(function (part, index) { + if (index % 2 === 1) { + if (part.indexOf("//") === 0 || part.indexOf("/*") === 0) { + return '' + part + ""; + } + return '' + part + ""; } + return part + .replace(/\b(auto|break|case|const|continue|default|do|else|enum|extern|for|goto|if|inline|return|sizeof|static|struct|switch|typedef|union|volatile|while|bool|true|false|NULL)\b/g, '$1') + .replace(/\b(void|char|short|int|long|float|double|unsigned|signed|size_t|ssize_t|uint8_t|uint16_t|uint32_t|uint64_t|int8_t|int16_t|int32_t|int64_t|bool)\b/g, '$1') + .replace(/\b(0x[0-9a-fA-F]+|\d+)\b/g, '$1'); + }).join(""); + } + + document.querySelectorAll("pre code").forEach(function (block) { + if (block.querySelector("span")) return; + block.innerHTML = highlightC(block.textContent); + }); - var text = target.innerText; - navigator.clipboard.writeText(text).then(function () { + window.highlightCodeBlocks = function (root) { + (root || document).querySelectorAll("pre code").forEach(function (block) { + if (block.querySelector("span")) return; + block.innerHTML = highlightC(block.textContent); + }); + }; + + document.querySelectorAll("[data-copy-target]").forEach(function (button) { + button.addEventListener("click", function () { + var target = document.getElementById(button.getAttribute("data-copy-target")); + if (!target) return; + navigator.clipboard.writeText(target.innerText).then(function () { var oldText = button.textContent; button.textContent = "Copied"; window.setTimeout(function () { @@ -48,8 +257,7 @@ filter.addEventListener("input", function () { var query = filter.value.trim().toLowerCase(); document.querySelectorAll("[data-doc-item]").forEach(function (item) { - var text = item.textContent.toLowerCase(); - item.classList.toggle("hidden", query && !text.includes(query)); + item.classList.toggle("hidden", query && !item.textContent.toLowerCase().includes(query)); }); }); } @@ -60,37 +268,35 @@ }); } - fetch(API, { headers: { Accept: "application/vnd.github+json" } }) - .then(function (response) { - if (!response.ok) { - throw new Error("GitHub API error"); - } - return response.json(); - }) - .then(function (repo) { - setStat("stars", repo.stargazers_count.toLocaleString()); - setStat("forks", repo.forks_count.toLocaleString()); - setStat("issues", repo.open_issues_count.toLocaleString()); - }) - .catch(function () { - setStat("stars", "—"); - setStat("forks", "—"); - setStat("issues", "—"); - }); + if (document.querySelector("[data-repo-stat]")) { + fetch(API, { headers: { Accept: "application/vnd.github+json" } }) + .then(function (response) { + if (!response.ok) throw new Error("GitHub API error"); + return response.json(); + }) + .then(function (repo) { + setStat("stars", repo.stargazers_count.toLocaleString()); + setStat("forks", repo.forks_count.toLocaleString()); + setStat("issues", repo.open_issues_count.toLocaleString()); + }) + .catch(function () { + setStat("stars", "—"); + setStat("forks", "—"); + setStat("issues", "—"); + }); - fetch(API + "/releases/latest", { headers: { Accept: "application/vnd.github+json" } }) - .then(function (response) { - if (!response.ok) { - throw new Error("No release"); - } - return response.json(); - }) - .then(function (release) { - setStat("release", release.tag_name || "Latest"); - }) - .catch(function () { - setStat("release", "—"); - }); + fetch(API + "/releases/latest", { headers: { Accept: "application/vnd.github+json" } }) + .then(function (response) { + if (!response.ok) throw new Error("No release"); + return response.json(); + }) + .then(function (release) { + setStat("release", release.tag_name || "Latest"); + }) + .catch(function () { + setStat("release", "—"); + }); + } document.querySelectorAll("[data-year]").forEach(function (node) { node.textContent = new Date().getFullYear(); diff --git a/docs/site/blog.html b/docs/site/blog.html index 923d896..f05e2ce 100644 --- a/docs/site/blog.html +++ b/docs/site/blog.html @@ -10,67 +10,52 @@ -
-
-
-

Project updates

-

Releases, changes and development notes

-

- The project does not duplicate release history on the website. Canonical release notes stay on GitHub. -

-
-
+
+ +
+
+ -
-
-
- Releases -

GitHub Releases

-

Download published versions and read the notes attached to each release.

- Open releases ↗ -
+

Project updates

+

+ The project does not duplicate release history on the website. + Canonical release notes stay on GitHub. +

-
- History -

ChangeLog

-

A compact history of important project changes kept together with the source code.

- Read ChangeLog ↗ -
+

GitHub Releases

+

Download published versions and read the notes attached to each release.

+

Open releases

-
- Development -

Commits

-

For the latest development activity, use the repository commit history.

- View commits ↗ -
-
-
-
+

ChangeLog

+

A compact history of important project changes kept together with the source code.

+

Read ChangeLog

- +

Commits

+

For the latest development activity, use the repository commit history.

+

View commits

+ +
+ diff --git a/docs/site/docs.html b/docs/site/docs.html index 6ed990e..bc54d6b 100644 --- a/docs/site/docs.html +++ b/docs/site/docs.html @@ -10,90 +10,65 @@ -
-
-
-

Documentation

-

Start small, add features when you need them

-

- A short guide to installation, the first server and the full libchttpx module documentation. +

+ + +
+
+ + +

Documentation

+

+ libchttpx is a compact HTTP/2 server library for C. This page covers + installation and the first server. Module details live in the sidebar.

-
-
- -
-
- - -
-
-

Installation

-

For Debian and Ubuntu on amd64, add the libchttpx APT repository once and install the development package normally.

-
-
- Debian / Ubuntu - -
-
curl -fsSL https://netcorelink.github.io/libchttpx/apt/libchttpx.sources \
+
+        

Installation

+

For Debian and Ubuntu on amd64, add the libchttpx APT repository once and install the development package.

+
+
+ +
+
curl -fsSL https://netcorelink.github.io/libchttpx/apt/libchttpx.sources \
   | sudo tee /etc/apt/sources.list.d/libchttpx.sources >/dev/null
 
 sudo apt update
 sudo apt install libchttpx-dev
-
- -

After that, future versions are installed through the normal APT update flow:

-
-
- Update - -
-
sudo apt update
+        
+ +

After that, future versions are installed through the normal APT update flow:

+
sudo apt update
 sudo apt upgrade
-
- -

To remove libchttpx:

-
-
- Remove - -
-
sudo apt remove libchttpx-dev
-
- -

The APT repository currently targets Debian/Ubuntu on amd64 and uses Trusted: yes, so there is no separate GPG-key setup. Package signing can be added later.

- -

Build from source

-

If you do not want to use the APT repository, the existing source installation remains available.

-
-
- Source build - -
-
sudo apt update
+
+        

To remove libchttpx:

+
sudo apt remove libchttpx-dev
+ +

The APT repository currently targets Debian/Ubuntu on amd64 and uses Trusted: yes, so there is no separate GPG-key setup. Package signing can be added later.

+ +

Build from source

+

If you do not want to use the APT repository, the existing source installation remains available.

+
sudo apt update
 sudo apt install -y build-essential pkg-config libcjson-dev zlib1g-dev
 
 git clone https://github.com/netcorelink/libchttpx.git
@@ -101,21 +76,16 @@ 

Build from source

make lin-lib sudo make lib-install PREFIX=/usr/local DESTDIR=
-
-
- -
-

Your first server

-

Create an app, configure a server, add a route, then run the app.

-
-
- server.c - -
-
#include <libchttpx.h>
-
-static void health(chttpx_request_t* req,
-                   chttpx_response_t* res)
+
+        

First server

+

Create an app, configure a server, add a route, then run the app.

+
+
+ +
+
#include <libchttpx.h>
+
+static void health(chttpx_request_t* req, chttpx_response_t* res)
 {
     (void)req;
     *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "healthy");
@@ -130,81 +100,47 @@ 

Your first server

chttpx_config_t config = cHTTPX_DefaultConfig(); config.port = 8080; - chttpx_serv_t* server = - cHTTPX_AppServer(&app, "main", &config); - - chttpx_router_t router = - cHTTPX_RoutePathPrefix(server, ""); - + chttpx_serv_t* server = cHTTPX_AppServer(&app, "main", &config); + chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); cHTTPX_Get(&router, "/health", health); int result = cHTTPX_AppRun(&app); cHTTPX_AppShutdown(&app); return result == cHTTPX_OK ? 0 : 1; }
-
-
- -
-

Modules

-

Filter the list and open the full module documentation directly on this site.

- - - -
- -
-

Compile your application

-

After installation, use pkg-config to get include and linker flags.

-
-
- Terminal - -
-
gcc server.c -o server $(pkg-config --cflags --libs libchttpx)
+        
+ +

Compile

+

After installation, use pkg-config to get include and linker flags.

+
gcc server.c -o server $(pkg-config --cflags --libs libchttpx)
 ./server
-
-
- -
-

Full documentation

-

All module documentation is available on this website. Start with the App runtime and move through the modules as needed.

- -
+ +

Modules

+

Filter the list and open the full module documentation.

+ + + - - -
- - + + + diff --git a/docs/site/examples.html b/docs/site/examples.html index ff45600..6b090c9 100644 --- a/docs/site/examples.html +++ b/docs/site/examples.html @@ -10,41 +10,43 @@ -
-
-
-

Examples

-

Copy a small example and start building

-

These examples use API calls that already exist in the repository.

-
-
- -
-
-
- Routing -

Health endpoint

-

Create an app, register a GET route and run the server.

-
-
basic.c
-
#include <libchttpx.h>
+  
+ +
+
+ + +

Examples

+

Copy a small example and start building. These snippets use API calls that already exist in the repository.

+ +

Health endpoint

+

Create an app, register a GET route and run the server.

+
+
+ +
+
#include <libchttpx.h>
 
 static void health(chttpx_request_t* req, chttpx_response_t* res)
 {
@@ -69,26 +71,22 @@ 

Health endpoint

cHTTPX_AppShutdown(&app); return result == cHTTPX_OK ? 0 : 1; }
-
- Open source example ↗ -
+
+

Open source example

-
- Middleware -

Bearer authentication

-

Protect a route group with a small middleware function.

-
-
middleware.c
-
static chttpx_middleware_result_t authenticate(
+        

Bearer authentication

+

Protect a route group with a small middleware function.

+
+
+ +
+
static chttpx_middleware_result_t authenticate(
     chttpx_request_t* req,
     chttpx_response_t* res)
 {
     if (!cHTTPX_BearerToken(req))
     {
-        *res = cHTTPX_ResError(
-            cHTTPX_StatusUnauthorized,
-            "bearer token required"
-        );
+        *res = cHTTPX_ResError(cHTTPX_StatusUnauthorized, "bearer token required");
         return out;
     }
 
@@ -98,28 +96,19 @@ 

Bearer authentication

chttpx_router_t api = cHTTPX_RoutePathPrefix(server, "/api"); chttpx_router_t private_api = cHTTPX_RouteGroup(&api, ""); cHTTPX_RouterUse(&private_api, authenticate);
-
- Open source example ↗ -
+
+

Open source example

-
- JSON -

Bind and validate JSON

-

Describe fields once, validate the request and build a JSON response.

-
-
json.c
-
create_user_t payload = {0};
+        

Bind and validate JSON

+

Describe fields once, validate the request and build a JSON response.

+
+
+ +
+
create_user_t payload = {0};
 
 chttpx_validation_t fields[] = {
-    cHTTPX_StringField(
-        "email",
-        &payload.email,
-        true,
-        3,
-        254,
-        cHTTPX_TRIM | cHTTPX_LOWERCASE,
-        NULL
-    )
+    cHTTPX_StringField("email", &payload.email, true, 3, 254, cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL)
 };
 
 if (!cHTTPX_BindJSON(req, res, fields, CHTTPX_ARRAY_LEN(fields)))
@@ -128,24 +117,11 @@ 

Bind and validate JSON

chttpx_json_t* json = cHTTPX_JsonObject(req); cHTTPX_JsonString(json, "email", payload.email); *res = cHTTPX_ResJsonObject(cHTTPX_StatusCreated, json);
-
- Open source example ↗ -
- - -
-
-
- - +

Open source example

+

See all examples on GitHub

+ + + diff --git a/docs/site/index.html b/docs/site/index.html index c2da6bc..67ee385 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -10,59 +10,50 @@ -
-
-
-
-

A fast and simple HTTP server library for C

-

Build HTTP servers in C, the modern way

-

- libchttpx is a compact HTTP/2 server library with routing, middleware, - JSON helpers, uploads, TLS, compression, built-in metrics and graceful shutdown. -

- - - -
Open source · BSD 3-Clause · C/C++ friendly
+
+
+
+

HTTP/2 server library for C

+

Build HTTP servers in C

+

+ A compact library with routing, middleware, JSON helpers, uploads, + TLS, compression, metrics and graceful shutdown. +

+ +

Open source · BSD 3-Clause · Linux and Windows

+
-
-
- example.c - -
-
#include <libchttpx.h>
+      
+
+ +
+
#include <libchttpx.h>
 
-static void health(chttpx_request_t* req,
-                   chttpx_response_t* res)
+static void health(chttpx_request_t* req, chttpx_response_t* res)
 {
     (void)req;
-    *res = cHTTPX_ResMessage(
-        cHTTPX_StatusOK, "healthy"
-    );
+    *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "healthy");
 }
 
 int main(void)
@@ -73,51 +64,39 @@ 

Build HTTP servers in C, the modern way

chttpx_config_t config = cHTTPX_DefaultConfig(); config.port = 8080; - chttpx_serv_t* server = - cHTTPX_AppServer(&app, "main", &config); - - chttpx_router_t router = - cHTTPX_RoutePathPrefix(server, ""); - + chttpx_serv_t* server = cHTTPX_AppServer(&app, "main", &config); + chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); cHTTPX_Get(&router, "/health", health); return cHTTPX_AppRun(&app); }
-
-
-
-
-
⚡
-

High performance

-

Small C runtime with direct control over server behavior.

-
-
-
◎
-

Easy to use

-

Routing, responses and helpers without repetitive HTTP plumbing.

-
-
-
◇
-

Flexible

-

Middleware, TLS, gzip compression, metrics, JSON, uploads and custom server limits.

-
-
-
▤
-

Documented

-

Module-by-module documentation and runnable examples.

-
-
-
⌘
-

Open source

-

Read the code, contribute, fork it and build on top of it.

-
-
+
+
+

High performance

+

Small C runtime with direct control over server behavior.

+
+
+

Easy to use

+

Routing, responses and helpers without repetitive HTTP plumbing.

+
+
+

Flexible

+

Middleware, TLS, gzip, metrics, JSON, uploads and custom limits.

+
+
+

Documented

+

Module-by-module documentation and runnable examples.

+
+
+

Open source

+

Read the code, contribute, fork it and build on top of it.

+
-
+
— GitHub stars @@ -137,61 +116,30 @@

Open source

-
-
-
-

Why libchttpx?

-

Keep HTTP code small and readable

-

- The library is designed so handlers contain application logic instead of - repetitive request parsing, response building and server boilerplate. -

- Read the documentation → -
- -
    -
  • Embedded and internal HTTP services
  • -
  • Microservices and backend APIs
  • -
  • Route groups and middleware pipelines
  • -
  • JSON validation and response builders
  • -
  • Multipart forms and file uploads
  • -
  • Linux and Windows development
  • -
-
-
- -
-
-

Support the project

-

Use it for free. Pay only when you need help.

+
+
+

Keep HTTP code small

- libchttpx stays open source. Commercial help, integration work and sponsorship - are separate ways to support continued development. + Handlers contain application logic instead of repetitive request + parsing, response building and server boilerplate.

- -
-
-

♥ Sponsor development

-

Interested in funding features or long-term maintenance?

- Sponsor options -
-
-

◌ Report an issue

-

Found a bug or have a feature request? Open it publicly on GitHub.

- GitHub Issues ↗ -
-
-

▢ Professional support

-

Need help with integration, debugging, review or a custom feature?

- Contact -
+
+
    +
  • Embedded and internal HTTP services
  • +
  • Microservices and backend APIs
  • +
  • Route groups and middleware pipelines
  • +
  • JSON validation and response builders
  • +
  • Multipart forms and file uploads
  • +
  • Linux and Windows development
  • +
diff --git a/docs/site/module.html b/docs/site/module.html index 8fa7a7b..100701d 100644 --- a/docs/site/module.html +++ b/docs/site/module.html @@ -12,60 +12,38 @@ -
-
-
-
-
- - + +
+ - \ No newline at end of file + diff --git a/docs/site/module.js b/docs/site/module.js index d691cd7..09f3422 100644 --- a/docs/site/module.js +++ b/docs/site/module.js @@ -1,56 +1,46 @@ (function () { "use strict"; - var modules = [ - ["app", "App runtime"], - ["server", "Server"], - ["tls", "TLS / HTTPS"], - ["compression", "Compression"], - ["metrics", "Metrics"], - ["routing", "Routing"], - ["middleware", "Middleware"], - ["request", "Request"], - ["responses", "Responses"], - ["json", "JSON"], - ["memory", "Memory"], - ["uploads", "Uploads"], - ["cors", "CORS"], - ["cookies", "Cookies"], - ["i18n", "Request IDs & i18n"], - ["logging", "Logging"], - ["rate-limiting", "Rate limiting"], - ["websocket", "WebSocket"] - ]; - - var allowed = {}; - modules.forEach(function (item) { allowed[item[0]] = item[1]; }); + var modules = {}; + if (window.CHTTPX_DOCS) { + window.CHTTPX_DOCS.forEach(function (group) { + group.items.forEach(function (item) { + if (item.id) modules[item.id] = item.label; + }); + }); + } var params = new URLSearchParams(window.location.search); var name = params.get("name") || "app"; - if (!allowed[name]) name = "app"; + if (!modules[name]) name = "app"; var container = document.querySelector("[data-module-content]"); var breadcrumb = document.querySelector("[data-module-breadcrumb]"); - var nav = document.querySelector("[data-module-nav]"); - document.title = allowed[name] + " — libchttpx"; - if (breadcrumb) breadcrumb.textContent = allowed[name]; + document.title = modules[name] + " — libchttpx"; + if (breadcrumb) breadcrumb.textContent = modules[name]; - if (nav) { - modules.forEach(function (item) { - var link = document.createElement("a"); - link.href = "module.html?name=" + encodeURIComponent(item[0]); - link.textContent = item[1]; - if (item[0] === name) link.classList.add("active"); - nav.appendChild(link); + function slugify(text) { + return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + } + + function headingChildren(root) { + var children = []; + root.querySelectorAll("h2").forEach(function (heading) { + if (!heading.id) heading.id = slugify(heading.textContent); + children.push({ + href: "#" + heading.id, + label: heading.textContent + }); }); + return children; } function rewriteLinks(root) { root.querySelectorAll("a[href]").forEach(function (link) { var href = link.getAttribute("href") || ""; var match = href.match(/^\.\.\/([^/]+)\/README\.md(?:#(.*))?$/); - if (match && allowed[match[1]]) { + if (match && modules[match[1]]) { link.href = "module.html?name=" + encodeURIComponent(match[1]) + (match[2] ? "#" + match[2] : ""); return; } @@ -65,6 +55,8 @@ }); } + if (window.renderDocsSidebar) window.renderDocsSidebar(); + fetch("content/" + encodeURIComponent(name) + ".md") .then(function (response) { if (!response.ok) throw new Error("Documentation file not found"); @@ -74,6 +66,8 @@ if (!window.marked) throw new Error("Markdown renderer failed to load"); container.innerHTML = window.marked.parse(markdown, { gfm: true, breaks: false }); rewriteLinks(container); + if (window.highlightCodeBlocks) window.highlightCodeBlocks(container); + if (window.renderDocsSidebar) window.renderDocsSidebar(headingChildren(container)); if (window.location.hash) { window.requestAnimationFrame(function () { var target = document.querySelector(window.location.hash); @@ -84,4 +78,4 @@ .catch(function () { container.innerHTML = '
Could not load this documentation. Return to documentation.
'; }); -})(); \ No newline at end of file +})(); diff --git a/docs/site/styles.css b/docs/site/styles.css index 989af28..b4539d0 100644 --- a/docs/site/styles.css +++ b/docs/site/styles.css @@ -1,18 +1,21 @@ -@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); :root { --bg: #ffffff; - --surface: #f7f9fc; - --surface-strong: #eef3f9; - --text: #101828; - --muted: #667085; - --border: #e4e7ec; - --accent: #2563eb; - --accent-hover: #1d4ed8; - --success: #16803a; - --shadow: 0 14px 40px rgba(16, 24, 40, 0.08); - --radius: 14px; - --container: 1180px; + --text: #1f2328; + --muted: #656d76; + --border: #e6e8eb; + --crumb: #edf2f7; + --crumb-active: #dce6f0; + --nav-active: #e8f0fe; + --accent: #d32f2f; + --accent-hover: #b71c1c; + --link: #0969da; + --code-bg: #1e1e1e; + --code-text: #e8e8e8; + --header-h: 52px; + --sidebar-w: 260px; + --content: 760px; } * { @@ -27,8 +30,9 @@ body { margin: 0; background: var(--bg); color: var(--text); - font-family: "DM Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.6; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + font-size: 16px; + line-height: 1.65; } a { @@ -45,817 +49,743 @@ img { max-width: 100%; } -.container { - width: min(calc(100% - 40px), var(--container)); - margin: 0 auto; -} - .site-header { position: sticky; top: 0; - z-index: 20; - background: rgba(255, 255, 255, 0.94); + z-index: 40; + display: flex; + align-items: center; + justify-content: space-between; + height: var(--header-h); + padding: 0 22px; + background: #fff; border-bottom: 1px solid var(--border); - backdrop-filter: blur(10px); } -.nav { - min-height: 70px; - display: flex; - align-items: center; - gap: 30px; +.site-header .site-menu { + position: absolute; } .brand { display: inline-flex; align-items: center; - font-size: 21px; - font-weight: 800; - letter-spacing: -0.03em; - white-space: nowrap; -} - -.nav-links { - display: flex; - align-items: center; - gap: 24px; - margin-right: auto; -} - -.nav-links a { - color: #475467; - font-size: 14px; + gap: 9px; + font-size: 17px; font-weight: 600; + letter-spacing: -0.03em; } -.nav-links a:hover, -.nav-links a.active { - color: var(--text); -} - -.nav-actions { - display: flex; - gap: 10px; - align-items: center; +.brand-mark { + display: block; + width: 20px; + height: 20px; + flex: 0 0 20px; } .menu-button { - display: none; - width: 40px; - height: 40px; - border: 1px solid var(--border); - border-radius: 9px; - background: #fff; - cursor: pointer; -} - -.btn { display: inline-flex; - min-height: 44px; - align-items: center; + flex-direction: column; justify-content: center; - gap: 8px; - padding: 0 17px; - border: 1px solid var(--border); - border-radius: 9px; - background: #fff; - color: var(--text); - font-weight: 700; - font-size: 14px; + gap: 5px; + width: 36px; + height: 36px; + padding: 8px; + border: 0; + background: transparent; cursor: pointer; - transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease; } -.btn:hover { - transform: translateY(-1px); - border-color: #c7d0dc; +.menu-button span { + display: block; + height: 1.5px; + background: #333; } -.btn-primary { - border-color: var(--accent); - background: var(--accent); - color: #fff; +.site-menu { + display: none; + position: absolute; + top: var(--header-h); + right: 12px; + min-width: 200px; + padding: 8px; + border: 1px solid var(--border); + background: #fff; + z-index: 50; } -.btn-primary:hover { - border-color: var(--accent-hover); - background: var(--accent-hover); +.site-menu.open { + display: block; } -.btn-small { - min-height: 36px; - padding: 0 13px; - font-size: 13px; +.site-menu a { + display: block; + padding: 8px 10px; + color: #444; + font-size: 14px; } -.hero { - padding: 70px 0 56px; +.site-menu a:hover, +.site-menu a.active { + background: #f4f4f4; + color: #111; } -.hero-grid { +.docs-frame { display: grid; - grid-template-columns: 1fr 1fr; - gap: 48px; - align-items: center; -} - -.eyebrow { - margin: 0 0 16px; - color: #64748b; - font-size: 12px; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -h1, -h2, -h3 { - margin-top: 0; - line-height: 1.12; - letter-spacing: -0.025em; - font-weight: 700; -} - -h1 { - max-width: 720px; - margin-bottom: 20px; - font-size: clamp(42px, 6vw, 68px); -} - -h2 { - margin-bottom: 15px; - font-size: clamp(28px, 4vw, 38px); + grid-template-columns: var(--sidebar-w) minmax(0, 1fr); + min-height: calc(100vh - var(--header-h)); } -h3 { - margin-bottom: 8px; - font-size: 19px; +.docs-sidebar { + position: sticky; + top: var(--header-h); + height: calc(100vh - var(--header-h)); + overflow: auto; + padding: 14px 12px 40px; + border-right: 1px solid var(--border); + background: var(--bg); } -.lead { - max-width: 650px; - margin: 0; - color: var(--muted); - font-size: 18px; +.sidebar-search { + width: 100%; + margin: 0 0 14px; + padding: 7px 10px; + border: 1px solid var(--border); + border-radius: 4px; + background: #fff; + color: var(--text); + font-size: 12px; + letter-spacing: 0.02em; + outline: none; } -.hero-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin: 28px 0 17px; +.sidebar-search::placeholder { + color: #9aa0a6; + text-transform: uppercase; + letter-spacing: 0.06em; } -.meta-line { - color: #667085; - font-size: 13px; +.sidebar-search:focus { + border-color: #b8c4d0; } -.code-card { - overflow: hidden; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #fbfcfe; - box-shadow: var(--shadow); +.nav-group { + margin-bottom: 2px; } -.code-card-header { - min-height: 46px; +.nav-group-toggle { display: flex; + width: 100%; align-items: center; justify-content: space-between; - padding: 0 16px; - border-bottom: 1px solid var(--border); - color: #475467; - background: #f8fafc; - font-size: 13px; - font-weight: 700; -} - -.code-card pre { + gap: 8px; margin: 0; - padding: 20px; - overflow: auto; - color: #25304a; - font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - tab-size: 4; -} - -.copy-button { + padding: 6px 8px; border: 0; background: transparent; - color: #667085; + color: #111; + font-size: 13px; + font-weight: 600; + text-align: left; cursor: pointer; - font-size: 12px; - font-weight: 700; } -.copy-button:hover { - color: var(--text); +.nav-group-toggle:hover { + background: #f6f8fa; } -.feature-grid, -.card-grid, -.stats-grid { - display: grid; - gap: 1px; - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - background: var(--border); +.nav-toggle-icon { + display: inline-grid; + width: 14px; + height: 14px; + place-items: center; + border: 1px solid #c9d1d9; + border-radius: 2px; + color: #57606a; + font-size: 11px; + font-weight: 700; + line-height: 1; + flex: 0 0 14px; } -.feature-grid { - grid-template-columns: repeat(5, 1fr); +.nav-group-items { + display: none; + padding: 0 0 6px; } -.feature { - min-height: 176px; - padding: 28px 22px; - text-align: center; - background: #fff; +.nav-group.open .nav-group-items { + display: block; } -.feature-icon { - display: grid; - width: 42px; - height: 42px; - margin: 0 auto 15px; - place-items: center; - border-radius: 10px; - background: var(--surface); - font-size: 20px; +.nav-group.open .nav-toggle-icon::before { + content: "−"; } -.feature p, -.card p, -.section-copy, -.page-lead { - color: var(--muted); +.nav-group:not(.open) .nav-toggle-icon::before { + content: "+"; } -.feature p, -.card p { - margin: 0; - font-size: 14px; +.docs-sidebar a { + display: block; + padding: 4px 8px 4px 12px; + color: #57606a; + font-size: 13px; + line-height: 1.45; + border-radius: 3px; } -.stats-wrap { - padding: 28px 0; - border-top: 1px solid var(--border); - border-bottom: 1px solid var(--border); - background: #fbfcfe; +.docs-sidebar a:hover { + color: #111; + background: #f6f8fa; } -.stats-grid { - grid-template-columns: repeat(4, 1fr); - border: 0; - border-radius: 0; +.docs-sidebar a.active { + color: #0b57d0; + font-weight: 600; + background: var(--nav-active); } -.stat { - min-height: 76px; - padding: 8px 22px; - text-align: center; - background: #fbfcfe; +.docs-sidebar .nav-child { + padding-left: 24px; + font-size: 12.5px; } -.stat + .stat { - border-left: 1px solid var(--border); +.docs-main { + min-width: 0; } -.stat-value { - display: block; - font-size: 24px; - font-weight: 800; - letter-spacing: -0.03em; +.docs-article { + width: min(100% - 56px, var(--content)); + margin: 0 auto; + padding: 20px 0 72px; } -.stat-label { - color: var(--muted); +.crumbs { + display: flex; + flex-wrap: wrap; + align-items: stretch; + margin: 0 0 28px; font-size: 13px; } -.section { - padding: 64px 0; +.crumbs a, +.crumbs span.crumb { + position: relative; + display: inline-flex; + align-items: center; + height: 30px; + margin-right: 2px; + padding: 0 16px 0 22px; + background: var(--crumb); + color: #4b5563; + white-space: nowrap; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%, 10px 50%); } -.section-muted { - background: #fbfcfe; - border-top: 1px solid var(--border); - border-bottom: 1px solid var(--border); +.crumbs a:first-child, +.crumbs span.crumb:first-child { + padding-left: 14px; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%); } -.two-column { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 48px; - align-items: start; +.crumbs .crumb-current { + background: var(--crumb-active); + color: #111; + font-weight: 600; } -.check-list { - margin: 0; - padding: 25px; - list-style: none; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--surface); +.crumbs a:hover { + color: #111; + filter: brightness(0.98); } -.check-list li { - position: relative; - padding: 8px 0 8px 27px; +h1, +h2, +h3, +h4 { + color: #111; + line-height: 1.25; + letter-spacing: -0.02em; + font-weight: 600; } -.check-list li::before { - position: absolute; - left: 0; - color: var(--success); - content: "✓"; - font-weight: 900; +h1 { + margin: 0 0 14px; + font-size: clamp(28px, 4vw, 36px); } -.card-grid { - grid-template-columns: repeat(3, 1fr); - border: 0; - background: transparent; - gap: 16px; +h2 { + margin: 36px 0 12px; + font-size: 22px; + scroll-margin-top: 76px; } -.card { - padding: 24px; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #fff; +h3 { + margin: 26px 0 10px; + font-size: 17px; + scroll-margin-top: 76px; } -.card .btn { - margin-top: 18px; +.lead, +.page-lead, +.section-copy, +.docs-article > p, +.doc-article p, +.docs-content p { + color: #444; } -.page-hero { - padding: 58px 0 32px; +.lead, +.page-lead { + max-width: 680px; + margin: 0 0 22px; } -.page-hero h1 { - max-width: 850px; - margin-bottom: 15px; - font-size: clamp(38px, 5vw, 56px); +pre, +.code-card pre, +.doc-article pre, +.docs-article pre { + margin: 16px 0 22px; + padding: 16px 18px; + overflow: auto; + border: 0; + border-radius: 8px; + background: var(--code-bg); + color: var(--code-text); + font: 13px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } -.page-lead { - max-width: 760px; - margin: 0; - font-size: 17px; +code, +.inline-code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } -.docs-layout { - display: grid; - grid-template-columns: 260px minmax(0, 1fr); - gap: 34px; - align-items: start; +p code, +li code, +td code, +.inline-code { + padding: 1px 5px; + border-radius: 4px; + background: #f2f3f5; + color: #333; + font-size: 0.9em; } -.sidebar { - position: sticky; - top: 92px; - padding: 20px; - border: 1px solid var(--border); - border-radius: 12px; - background: #fff; +pre code, +.doc-article pre code, +.docs-article pre code { + padding: 0; + background: transparent; + color: inherit; + font-size: inherit; } -.sidebar strong { - display: block; - margin-bottom: 10px; -} +.token-kw { color: #c792ea; } +.token-type { color: #82aaff; } +.token-str { color: #c3e88d; } +.token-cm { color: #9e9e9e; } +.token-num { color: #f78c6c; } +.token-fn { color: #82aaff; } -.sidebar a { +pre code { display: block; - padding: 7px 0; - color: #667085; - font-size: 14px; + min-width: max-content; } -.sidebar a:hover { - color: var(--accent); +.code-card { + position: relative; } -.docs-content section { - margin-bottom: 50px; - scroll-margin-top: 100px; +.code-card-header { + position: absolute; + top: 8px; + right: 12px; + z-index: 1; + margin: 0; } -.docs-content p { +.copy-button { + border: 0; + background: transparent; color: var(--muted); + cursor: pointer; + font-size: 12px; + font-weight: 600; } -.inline-code, -code { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +.copy-button:hover { + color: #111; } -.inline-code { - padding: 2px 6px; - border-radius: 5px; - background: var(--surface-strong); - color: #344054; - font-size: 0.92em; +.code-card .copy-button { + color: #bbb; } -.module-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; +.code-card .copy-button:hover { + color: #fff; } -.module-link { - display: block; - padding: 18px; - border: 1px solid var(--border); - border-radius: 10px; - background: #fff; +pre { + position: relative; } -.module-link:hover { - border-color: #b9c4d2; - box-shadow: 0 8px 24px rgba(16, 24, 40, 0.05); +.home { + width: min(calc(100% - 48px), 1080px); + margin: 0 auto; + padding: 48px 0 80px; } -.module-link span { - display: block; - margin-top: 5px; - color: var(--muted); - font-size: 13px; -} - -.search-box { - width: 100%; - margin-bottom: 18px; - padding: 11px 13px; - border: 1px solid var(--border); - border-radius: 9px; - outline: none; -} - -.search-box:focus { - border-color: #94a3b8; -} - -.example-stack, -.post-list { +.hero-grid { display: grid; - gap: 22px; -} - -.example-card, -.post { - padding: 25px; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #fff; -} - -.example-card .code-card { - margin-top: 18px; - box-shadow: none; + grid-template-columns: 1fr 1.05fr; + gap: 48px; + align-items: start; } -.tag { - display: inline-flex; - padding: 4px 9px; - border-radius: 999px; - background: var(--surface-strong); - color: #475467; +.eyebrow { + margin: 0 0 10px; + color: var(--muted); font-size: 12px; font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; } -.notice { - padding: 17px 19px; - border: 1px solid #bfdbfe; - border-radius: 10px; - background: #eff6ff; - color: #1e3a8a; -} - -.site-footer { - padding: 28px 0; - border-top: 1px solid var(--border); -} - -.footer-row { +.hero-actions, +.meta-row { display: flex; - justify-content: space-between; - gap: 30px; - align-items: center; + flex-wrap: wrap; + gap: 10px; + margin: 22px 0 0; } +.meta-line, +.muted, .footer-copy { color: var(--muted); font-size: 13px; } -.footer-links { - display: flex; - flex-wrap: wrap; - gap: 16px; - color: #475467; - font-size: 13px; +.btn { + display: inline-flex; + min-height: 38px; + align-items: center; + justify-content: center; + padding: 0 14px; + border: 1px solid #ddd; + border-radius: 6px; + background: #fff; + color: #222; + font-size: 14px; font-weight: 600; + cursor: pointer; } -.muted { - color: var(--muted); +.btn:hover { + border-color: #bbb; } -.hidden { - display: none !important; +.btn-primary { + border-color: var(--accent); + background: var(--accent); + color: #fff; } -@media (max-width: 900px) { - .menu-button { - display: inline-grid; - place-items: center; - } - - .nav { - flex-wrap: wrap; - gap: 12px; - padding: 12px 0; - } - - .nav-links { - order: 3; - width: 100%; - display: none; - flex-direction: column; - align-items: flex-start; - gap: 4px; - padding: 10px 0; - } - - .nav-links.open { - display: flex; - } - - .nav-actions { - margin-left: auto; - } - - .hero-grid, - .two-column, - .docs-layout { - grid-template-columns: 1fr; - } - - .sidebar { - position: static; - } - - .feature-grid { - grid-template-columns: repeat(2, 1fr); - } - - .feature:last-child { - grid-column: 1 / -1; - } - - .stats-grid, - .card-grid { - grid-template-columns: repeat(2, 1fr); - } +.btn-primary:hover { + border-color: var(--accent-hover); + background: var(--accent-hover); +} - .stat:nth-child(3) { - border-left: 0; - } +.btn-small { + min-height: 32px; + padding: 0 11px; + font-size: 13px; } -@media (max-width: 620px) { - .container { - width: min(calc(100% - 28px), var(--container)); - } +.feature-grid, +.card-grid, +.stats-grid, +.module-grid { + display: grid; + gap: 0; +} - .nav-actions .btn:not(.btn-primary) { - display: none; - } +.feature-grid { + grid-template-columns: repeat(5, 1fr); + margin: 48px 0 0; + border-top: 1px solid var(--border); +} - .hero { - padding-top: 44px; - } +.feature, +.stat, +.card, +.module-link, +.example-card, +.post { + padding: 22px 18px 22px 0; +} - h1 { - font-size: 42px; - } +.feature + .feature, +.stat + .stat { + padding-left: 18px; +} - .feature-grid, - .stats-grid, - .card-grid, - .module-grid { - grid-template-columns: 1fr; - } +.feature h3, +.card h3 { + margin: 0 0 6px; + font-size: 15px; +} - .feature:last-child { - grid-column: auto; - } +.feature p, +.card p, +.module-link span { + margin: 0; + color: var(--muted); + font-size: 13.5px; +} - .stat + .stat { - border-left: 0; - border-top: 1px solid var(--border); - } +.stats-wrap { + margin-top: 28px; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} - .footer-row { - align-items: flex-start; - flex-direction: column; - } +.stats-grid { + grid-template-columns: repeat(4, 1fr); +} - .hero-actions .btn { - width: 100%; - } +.stat-value { + display: block; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; } +.stat-label { + color: var(--muted); + font-size: 13px; +} -/* Documentation article pages */ -.module-shell { +.two-column { display: grid; - grid-template-columns: 230px minmax(0, 1fr); - gap: 38px; - align-items: start; + grid-template-columns: 1fr 1fr; + gap: 48px; + margin-top: 48px; } -.module-nav { - position: sticky; - top: 92px; - padding: 18px; - border: 1px solid var(--border); - border-radius: 12px; - background: #fff; +.check-list { + margin: 0; + padding: 0; + list-style: none; } -.module-nav-title { - margin: 0 0 10px; - font-size: 13px; - font-weight: 700; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.08em; +.check-list li { + padding: 7px 0 7px 18px; + border-bottom: 1px solid var(--border); } -.module-nav a { - display: block; - padding: 7px 9px; - border-radius: 7px; - color: #667085; - font-size: 14px; - font-weight: 500; +.check-list li::before { + content: "–"; + margin-right: 8px; + color: #999; } -.module-nav a:hover, -.module-nav a.active { - background: var(--surface); - color: var(--text); +.card-grid { + grid-template-columns: repeat(3, 1fr); + gap: 28px; + margin-top: 28px; } -.doc-article { - min-width: 0; - max-width: 860px; +.card, +.example-card, +.post, +.module-link { + border-bottom: 1px solid var(--border); + background: transparent; } -.doc-article > h1:first-child { - margin-bottom: 16px; - font-size: clamp(36px, 5vw, 52px); +.module-grid { + grid-template-columns: 1fr 1fr; + gap: 0 28px; } -.doc-article h2 { - margin-top: 44px; - margin-bottom: 14px; - padding-top: 8px; - font-size: 27px; - scroll-margin-top: 96px; +.module-link { + display: block; + padding: 14px 0; } -.doc-article h3 { - margin-top: 30px; - font-size: 20px; - scroll-margin-top: 96px; +.module-link:hover strong { + color: var(--accent); } -.doc-article p, -.doc-article li { - color: #475467; - font-size: 16px; +.search-box { + width: 100%; + margin: 8px 0 12px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 6px; + outline: none; } -.doc-article ul, -.doc-article ol { - padding-left: 24px; +.search-box:focus { + border-color: #bbb; } -.doc-article li + li { - margin-top: 6px; +.notice { + margin: 18px 0; + padding: 12px 14px; + border-left: 3px solid var(--accent); + background: #fff6f6; + color: #5c2a2a; } -.doc-article a { - color: var(--accent); - text-decoration: underline; - text-decoration-color: #bfdbfe; - text-underline-offset: 3px; +.doc-article ul, +.docs-article ul, +.doc-article ol, +.docs-article ol { + padding-left: 22px; } -.doc-article blockquote { - margin: 20px 0; - padding: 14px 18px; - border-left: 3px solid var(--accent); - background: #f8fafc; - color: #475467; +.doc-article li + li, +.docs-article li + li { + margin-top: 4px; } -.doc-article pre { - margin: 18px 0 24px; - padding: 18px 20px; - overflow: auto; - border: 1px solid var(--border); - border-radius: 10px; - background: #f8fafc; - color: #25304a; - font: 13px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +.doc-article a, +.docs-article a { + color: var(--link); } -.doc-article code { - padding: 2px 5px; - border-radius: 5px; - background: #f1f5f9; - font-size: 0.9em; +.doc-article a:hover, +.docs-article a:hover { + text-decoration: underline; } -.doc-article pre code { - padding: 0; - background: transparent; - font-size: inherit; +.doc-article blockquote, +.docs-article blockquote { + margin: 18px 0; + padding: 0 0 0 14px; + border-left: 3px solid #ddd; + color: #555; } -.doc-article table { +.doc-article table, +.docs-article table { width: 100%; - margin: 18px 0 26px; + margin: 16px 0 24px; border-collapse: collapse; - overflow: hidden; - border: 1px solid var(--border); - border-radius: 10px; } .doc-article th, -.doc-article td { - padding: 11px 13px; +.docs-article th, +.doc-article td, +.docs-article td { + padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; -} - -.doc-article th { - background: #f8fafc; font-size: 14px; } -.doc-article td { - color: #475467; - font-size: 14px; +.doc-article th, +.docs-article th { + color: #111; } .doc-loading, .doc-error { - padding: 26px; - border: 1px solid var(--border); - border-radius: 12px; + padding: 18px 0; color: var(--muted); - background: #fff; } .doc-error { - border-color: #fecaca; - background: #fff7f7; - color: #991b1b; + color: #9b1c1c; } -.breadcrumbs { +.site-footer { + padding: 20px 22px 28px; + border-top: 1px solid var(--border); +} + +.footer-row { display: flex; - gap: 7px; + justify-content: space-between; + gap: 20px; align-items: center; - margin-bottom: 18px; - color: var(--muted); +} + +.footer-links { + display: flex; + flex-wrap: wrap; + gap: 16px; + color: #666; font-size: 13px; } -.breadcrumbs a:hover { - color: var(--text); +.tag { + color: var(--muted); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.hidden { + display: none !important; +} + +.page-hero, +.section { + width: min(calc(100% - 48px), 1080px); + margin: 0 auto; +} + +.page-hero { + padding: 36px 0 8px; +} + +.section { + padding: 12px 0 56px; } @media (max-width: 900px) { - .module-shell { + .docs-frame { + grid-template-columns: 1fr; + } + + .docs-sidebar { + display: none; + position: fixed; + top: var(--header-h); + left: 0; + width: min(86vw, 300px); + height: calc(100vh - var(--header-h)); + z-index: 30; + background: #fff; + border-right: 1px solid var(--border); + box-shadow: none; + } + + .docs-sidebar.open { + display: block; + } + + .docs-article { + width: min(100% - 32px, var(--content)); + } + + .hero-grid, + .two-column, + .feature-grid, + .stats-grid, + .card-grid, + .module-grid { grid-template-columns: 1fr; } - .module-nav { - position: static; + .feature + .feature, + .stat + .stat { + padding-left: 0; + border-top: 1px solid var(--border); } } diff --git a/docs/site/support.html b/docs/site/support.html index 1761c78..1a7b574 100644 --- a/docs/site/support.html +++ b/docs/site/support.html @@ -10,64 +10,53 @@ -
-
-
-

Support

-

Open source for everyone, paid help when you need it

-

- Use libchttpx freely. If your project needs direct help, custom work or sponsorship, there are separate support paths. +

+ +
+
+ + +

Support

+

+ Use libchttpx freely. If your project needs direct help, custom work + or sponsorship, there are separate support paths.

-
-
-
-
-
-

Community support

-

Report reproducible bugs and request features publicly so fixes can benefit everyone.

- Open an issue ↗ -
+

Community support

+

Report reproducible bugs and request features publicly so fixes can benefit everyone.

+

Open an issue

-
-

Professional support

-

Integration help, debugging, API design review, migration and custom functionality for your project.

- Contact by email -
+

Professional support

+

Integration help, debugging, API design review, migration and custom functionality for your project.

+

Contact by email

- -
-
+ +

For feature funding, maintenance sponsorship or company support, contact the maintainer directly.

+

Become a sponsor

-
-
-
-

What can be paid?

-

- The library itself remains open source. Payment is for work around the project, not for access to the public code. -

-
+

What can be paid?

+

The library itself remains open source. Payment is for work around the project, not for access to the public code.

  • Integration into an existing C/C++ service
  • Custom features and extensions
  • @@ -76,17 +65,8 @@

    What can be paid?

  • Priority support for a commercial project
  • Sponsored maintenance or roadmap work
-
-
-
- - + + + diff --git a/example/basic.c b/example/basic.c index 373f042..5a5617c 100644 --- a/example/basic.c +++ b/example/basic.c @@ -1,11 +1,13 @@ #include +/** Liveness check; responds with plain text "healthy". */ static void health(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "healthy"); } +/** Minimal single-server example on port 8080. */ int main(void) { chttpx_app_t app; diff --git a/example/compression.c b/example/compression.c index 2ad3049..8e24f9c 100644 --- a/example/compression.c +++ b/example/compression.c @@ -15,12 +15,14 @@ static const char page[] = "

This intentionally repetitive response demonstrates gzip negotiation.

" ""; +/** Serves a large HTML page eligible for gzip compression. */ static void page_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResHtml(cHTTPX_StatusOK, "%s", page); } +/** Health endpoint with compression explicitly disabled on the response. */ static void health_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -28,6 +30,7 @@ static void health_handler(chttpx_request_t* req, chttpx_response_t* res) cHTTPX_ResponseCompression(res, false); } +/** Demonstrates server-wide gzip and per-route/response opt-out. */ int main(void) { chttpx_app_t app; diff --git a/example/json.c b/example/json.c index f7533d3..5b2f183 100644 --- a/example/json.c +++ b/example/json.c @@ -6,29 +6,14 @@ typedef struct char* name; } create_user_t; +/** Binds JSON body fields and returns a created-user JSON object. */ static void create_user(chttpx_request_t* req, chttpx_response_t* res) { create_user_t payload = {0}; chttpx_validation_t fields[] = { - cHTTPX_StringField( - "email", - &payload.email, - true, - 3, - 254, - cHTTPX_TRIM | cHTTPX_LOWERCASE, - NULL - ), - cHTTPX_StringField( - "name", - &payload.name, - true, - 1, - 64, - cHTTPX_TRIM, - NULL - ), + cHTTPX_StringField("email", &payload.email, true, 3, 254, cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL), + cHTTPX_StringField("name", &payload.name, true, 1, 64, cHTTPX_TRIM, NULL), }; if (!cHTTPX_BindJSON(req, res, fields, CHTTPX_ARRAY_LEN(fields))) @@ -41,6 +26,7 @@ static void create_user(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResJsonObject(cHTTPX_StatusCreated, json); } +/** POST /api/users with JSON validation and binding. */ int main(void) { chttpx_app_t app; diff --git a/example/local_call.c b/example/local_call.c index 5e23ae4..e6c4090 100644 --- a/example/local_call.c +++ b/example/local_call.c @@ -1,25 +1,22 @@ #include +/** Internal processing endpoint invoked via cHTTPX_Call. */ static void internal_process(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"source\":\"internal\"}"); } +/** Proxies the request to the in-process "internal" server. */ static void public_proxy(chttpx_request_t* req, chttpx_response_t* res) { - int result = cHTTPX_Call( - req, - "internal", - cHTTPX_MethodPost, - "/process", - res - ); + int result = cHTTPX_Call(req, "internal", cHTTPX_MethodPost, "/process", res); if (result != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "internal call failed"); } +/** Two App servers: public (8080) forwards to internal (9090) without TCP. */ int main(void) { chttpx_app_t app; diff --git a/example/metrics.c b/example/metrics.c index 84c55ba..14e5bd7 100644 --- a/example/metrics.c +++ b/example/metrics.c @@ -2,12 +2,14 @@ #include +/** Simple handler used to generate request metrics. */ static void hello(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "hello"); } +/** Enables server metrics and exposes GET /metrics for Prometheus scraping. */ int main(void) { chttpx_app_t app; @@ -26,8 +28,7 @@ int main(void) } chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); - if (!cHTTPX_Get(&router, "/hello", hello) || - cHTTPX_MetricsRoute(&router, "/metrics") != cHTTPX_OK) + if (!cHTTPX_Get(&router, "/hello", hello) || cHTTPX_MetricsRoute(&router, "/metrics") != cHTTPX_OK) { cHTTPX_AppShutdown(&app); return 1; diff --git a/example/middleware.c b/example/middleware.c index d65a033..89bc517 100644 --- a/example/middleware.c +++ b/example/middleware.c @@ -1,5 +1,6 @@ #include +/** Requires a Bearer token; stops the chain with 401 when absent. */ static chttpx_middleware_result_t authenticate(chttpx_request_t* req, chttpx_response_t* res) { if (!cHTTPX_BearerToken(req)) @@ -11,12 +12,14 @@ static chttpx_middleware_result_t authenticate(chttpx_request_t* req, chttpx_res return next; } +/** Protected route returning a minimal authenticated JSON payload. */ static void me(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"authenticated\":true}"); } +/** Router-scoped authentication middleware on /api/me. */ int main(void) { chttpx_app_t app; diff --git a/example/multiple_servers.c b/example/multiple_servers.c index 956e1f8..c601429 100644 --- a/example/multiple_servers.c +++ b/example/multiple_servers.c @@ -1,17 +1,20 @@ #include +/** Health check for the public listener (port 8080). */ static void public_health(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "public"); } +/** Health check for the admin listener (port 9090). */ static void admin_health(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "admin"); } +/** Runs two independent HTTP servers in one App runtime. */ int main(void) { chttpx_app_t app; diff --git a/example/remote_call.c b/example/remote_call.c index ca0fe59..1811478 100644 --- a/example/remote_call.c +++ b/example/remote_call.c @@ -1,14 +1,9 @@ #include +/** Forwards payment creation to a remote AppRemote target. */ static void create_payment(chttpx_request_t* req, chttpx_response_t* res) { - int result = cHTTPX_Call( - req, - "payments", - cHTTPX_MethodPost, - "/payments/create", - res - ); + int result = cHTTPX_Call(req, "payments", cHTTPX_MethodPost, "/payments/create", res); if (result == cHTTPX_ERR_UNAVAILABLE) { @@ -26,6 +21,7 @@ static void create_payment(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "payments call failed"); } +/** Registers a remote payments service and exposes a local proxy route. */ int main(void) { chttpx_app_t app; diff --git a/example/tls.c b/example/tls.c index c5f002f..4718807 100644 --- a/example/tls.c +++ b/example/tls.c @@ -2,12 +2,14 @@ #include +/** JSON health payload indicating HTTPS is active. */ static void health(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"https\":true}"); } +/** HTTPS server on port 8443; certificate and key paths are CLI arguments. */ int main(int argc, char** argv) { if (argc != 3) diff --git a/example/upload.c b/example/upload.c index 4f936ce..80ba38e 100644 --- a/example/upload.c +++ b/example/upload.c @@ -1,5 +1,6 @@ #include +/** Accepts multipart field "avatar" and returns its size as JSON. */ static void upload_avatar(chttpx_request_t* req, chttpx_response_t* res) { const chttpx_file_t* avatar = cHTTPX_FormFile(req, "avatar"); @@ -10,13 +11,10 @@ static void upload_avatar(chttpx_request_t* req, chttpx_response_t* res) return; } - *res = cHTTPX_ResJson( - cHTTPX_StatusOK, - "{\"size\":%llu}", - (unsigned long long)avatar->size - ); + *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"size\":%llu}", (unsigned long long)avatar->size); } +/** Avatar upload with JPEG/PNG type and 5 MiB size limits. */ int main(void) { chttpx_app_t app; @@ -35,8 +33,7 @@ int main(void) chttpx_router_t router = cHTTPX_RoutePathPrefix(server, "/api"); - chttpx_route_t* route = - cHTTPX_Post(&router, "/avatar", upload_avatar); + chttpx_route_t* route = cHTTPX_Post(&router, "/avatar", upload_avatar); const char* allowed[] = {"image/jpeg", "image/png"}; chttpx_upload_policy_t policy = { diff --git a/include/libchttpx.h b/include/libchttpx.h index 9bf3426..151ba69 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -703,16 +703,13 @@ extern "C" * Parse and validate a JSON request body. * * Parsed strings and arrays are request-owned. On failure this function - * * creates a safe JSON 400 response in res. * * @param req Current HTTP request. - * @param res Response populated when binding - * fails. + * @param res Response populated when binding fails. * @param fields Field definitions and output targets. * @param field_count Number of field definitions. - * @return 1 on - * success, 0 on parsing or validation failure. + * @return 1 on success, 0 on parsing or validation failure. */ int cHTTPX_BindJSON(chttpx_request_t* req, struct chttpx_response* res, chttpx_validation_t* fields, size_t field_count); @@ -730,11 +727,8 @@ extern "C" * * @return A chttpx_validation_t structure initialized for a string field. */ -#define chttpx_validation_string(name, ptr, required, min_length, max_length, validator) \ - (chttpx_validation_t) \ - { \ - name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, cHTTPX_NORMALIZE_NONE, NULL \ - } +#define chttpx_validation_string(name, ptr, required, min_length, max_length, validator) \ + (chttpx_validation_t){name, ptr, required, min_length, max_length, FIELD_STRING, validator, 0, cHTTPX_NORMALIZE_NONE, NULL} /** * Macro to define an integer field for JSON request validation. @@ -746,11 +740,8 @@ extern "C" * * @return A chttpx_validation_t structure initialized for an integer field. */ -#define chttpx_validation_integer(name, ptr, required) \ - (chttpx_validation_t) \ - { \ - name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ - } +#define chttpx_validation_integer(name, ptr, required) \ + (chttpx_validation_t){name, ptr, required, 0, 0, FIELD_NUMBER, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL} /** * Macro to define a boolean field for JSON request validation. @@ -762,20 +753,25 @@ extern "C" * * @return A chttpx_validation_t structure initialized for a boolean field. */ -#define chttpx_validation_boolean(name, ptr, required) \ - (chttpx_validation_t) \ - { \ - name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL \ - } +#define chttpx_validation_boolean(name, ptr, required) \ + (chttpx_validation_t){name, ptr, required, 0, 0, FIELD_BOOL, VALIDATOR_NONE, 0, cHTTPX_NORMALIZE_NONE, NULL} -#define cHTTPX_StringField(name, ptr, required, min_length, max_length, normalizers, validator) \ - (chttpx_validation_t) \ - { \ - name, ptr, required, min_length, max_length, FIELD_STRING, VALIDATOR_NONE, 0, normalizers, validator \ - } +/** + * Define a string JSON field with normalizers and a custom validator. + * + * @param name Field name in the JSON body. + * @param ptr Pointer to the target string variable. + * @param required Non-zero when the field is required. + * @param min_length Minimum string length (0 for none). + * @param max_length Maximum string length (0 for none). + * @param normalizers Bitmask of chttpx_normalizer_t flags. + * @param validator Optional custom validator callback. + * @return Initialized chttpx_validation_t entry. + */ +#define cHTTPX_StringField(name, ptr, required, min_length, max_length, normalizers, validator) \ + (chttpx_validation_t){name, ptr, required, min_length, max_length, FIELD_STRING, VALIDATOR_NONE, 0, normalizers, validator} #ifdef __cplusplus - extern } #endif @@ -1366,22 +1362,36 @@ extern "C" /** Create a router bound to one App-managed server. */ chttpx_router_t cHTTPX_RoutePathPrefix(chttpx_serv_t* server, const char* prefix); + /** Register a route with an explicit HTTP method on a router. */ void cHTTPX_RegisterRoute(chttpx_router_t* r, const char* method, const char* path, chttpx_handler_t handler); + /** Register a GET handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Get(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Register a POST handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Post(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Register a PUT handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Put(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Register a PATCH handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Patch(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Register a DELETE handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Delete(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Register an OPTIONS handler; returns the route handle or NULL on failure. */ chttpx_route_t* cHTTPX_Options(chttpx_router_t* router, const char* path, chttpx_handler_t handler); + /** Create a nested router that shares middleware with its parent. */ chttpx_router_t cHTTPX_RouteGroup(const chttpx_router_t* parent, const char* prefix); + /** Attach middleware that runs before handlers on this router. @return cHTTPX_OK on success. */ int cHTTPX_RouterUse(chttpx_router_t* router, chttpx_middleware_t middleware); + /** Attach middleware that runs after handlers on this router. @return cHTTPX_OK on success. */ int cHTTPX_RouterUseAfter(chttpx_router_t* router, chttpx_middleware_t middleware); + /** Attach middleware that runs before this route's handler. @return cHTTPX_OK on success. */ int cHTTPX_RouteUse(chttpx_route_t* route, chttpx_middleware_t middleware); + /** Attach middleware that runs after this route's handler. @return cHTTPX_OK on success. */ int cHTTPX_RouteUseAfter(chttpx_route_t* route, chttpx_middleware_t middleware); + /** Set upload constraints for one route. @return cHTTPX_OK on success. */ int cHTTPX_RouteUploadPolicy(chttpx_route_t* route, const chttpx_upload_policy_t* policy); + /** Release router-owned resources (normally unused with App-managed servers). */ void cHTTPX_RouterFree(chttpx_router_t* router); /** Configure logging for one App-managed component. */ @@ -1399,12 +1409,8 @@ extern "C" * The provider allocates *output with malloc-compatible ownership. The * library owns that buffer after a successful compression operation. */ - typedef int (*chttpx_compression_encode_fn)(const unsigned char* input, - size_t input_size, - int level, - unsigned char** output, - size_t* output_size, - void* user_data); + typedef int (*chttpx_compression_encode_fn)(const unsigned char* input, size_t input_size, int level, + unsigned char** output, size_t* output_size, void* user_data); typedef struct { @@ -1601,9 +1607,14 @@ extern "C" * * ca_file == NULL uses the system trust store. Client certificate/key * fields are optional and enable mutual TLS when both are provided. + * + * @param app Application runtime. + * @param name Logical name used with cHTTPX_Call(). + * @param base_url Remote base URL (http:// or https://). + * @param tls_config Client TLS options; may be NULL for defaults. + * @return cHTTPX_OK on success or a negative error code. */ - int cHTTPX_AppRemoteEx(chttpx_app_t* app, const char* name, const char* base_url, - const chttpx_tls_client_config_t* tls_config); + int cHTTPX_AppRemoteEx(chttpx_app_t* app, const char* name, const char* base_url, const chttpx_tls_client_config_t* tls_config); /** Start every local server in the App in its own listener thread. */ int cHTTPX_AppStart(chttpx_app_t* app); @@ -1736,7 +1747,6 @@ extern "C" int cHTTPX_ParamBool(chttpx_request_t* req, const char* name, bool* value); #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_app.c b/src/cHTTPX_app.c index 4d59234..1fb923f 100644 --- a/src/cHTTPX_app.c +++ b/src/cHTTPX_app.c @@ -26,6 +26,9 @@ #define CHTTPX_CALL_TIMEOUT_SEC 30 +/** + * Local server entry with optional listener thread. + */ typedef struct { chttpx_serv_t* server; @@ -33,6 +36,9 @@ typedef struct bool thread_started; } chttpx_app_server_t; +/** + * Named remote base URL and TLS settings. + */ typedef struct { char* name; @@ -40,16 +46,35 @@ typedef struct chttpx_tls_client_config_t tls; } chttpx_app_remote_t; +/** + * Return the local server table backing an app. + * + * @param app Application instance. + * @return Pointer to the internal local server table. + */ static chttpx_app_server_t* app_servers(chttpx_app_t* app) { return (chttpx_app_server_t*)app->_servers; } +/** + * Return the remote server table backing an app. + * + * @param app Application instance. + * @return Pointer to the internal remote target table. + */ static chttpx_app_remote_t* app_remotes(chttpx_app_t* app) { return (chttpx_app_remote_t*)app->_remotes; } +/** + * Find a registered local server by name. + * + * @param app Application instance. + * @param name Registered local server name. + * @return Matching server, or NULL when the name is unknown. + */ static chttpx_serv_t* find_server(chttpx_app_t* app, const char* name) { if (!app || !name) @@ -65,6 +90,13 @@ static chttpx_serv_t* find_server(chttpx_app_t* app, const char* name) return NULL; } +/** + * Find a registered remote target by name. + * + * @param app Application instance. + * @param name Registered remote service name. + * @return Matching remote registration, or NULL when the name is unknown. + */ static chttpx_app_remote_t* find_remote(chttpx_app_t* app, const char* name) { if (!app || !name) @@ -80,6 +112,12 @@ static chttpx_app_remote_t* find_remote(chttpx_app_t* app, const char* name) return NULL; } +/** + * Grow the local server table when needed. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ static int ensure_server_capacity(chttpx_app_t* app) { if (app->_servers_count < app->_servers_capacity) @@ -100,6 +138,12 @@ static int ensure_server_capacity(chttpx_app_t* app) return cHTTPX_OK; } +/** + * Grow the remote server table when needed. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ static int ensure_remote_capacity(chttpx_app_t* app) { if (app->_remotes_count < app->_remotes_capacity) @@ -120,6 +164,12 @@ static int ensure_remote_capacity(chttpx_app_t* app) return cHTTPX_OK; } +/** + * Initialize application state and platform networking. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ int cHTTPX_AppInit(chttpx_app_t* app) { if (!app) @@ -138,6 +188,14 @@ int cHTTPX_AppInit(chttpx_app_t* app) return cHTTPX_OK; } +/** + * Create and register a named local HTTP server. + * + * @param app Application instance. + * @param name Unique server name within the application. + * @param config Server listen and resource limit configuration. + * @return Initialized server registered with the app, or NULL on failure. + */ chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttpx_config_t* config) { if (!app || !app->_initialized || app->_started || !name || !*name || !config) @@ -166,6 +224,11 @@ chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttp } +/** + * Return default outbound TLS client settings. + * + * @return TLS client defaults with peer verification enabled. + */ chttpx_tls_client_config_t cHTTPX_DefaultTLSClientConfig(void) { return (chttpx_tls_client_config_t){ @@ -173,6 +236,11 @@ chttpx_tls_client_config_t cHTTPX_DefaultTLSClientConfig(void) }; } +/** + * Free owned strings inside a remote TLS config copy. + * + * @param tls TLS client settings for outbound calls. + */ static void free_remote_tls_config(chttpx_tls_client_config_t* tls) { if (!tls) @@ -232,18 +300,38 @@ int cHTTPX_AppRemoteEx(chttpx_app_t* app, const char* name, const char* base_url return cHTTPX_OK; } +/** + * Register a remote base URL using default TLS client settings. + * + * @param app Application instance. + * @param name Unique remote service name within the application. + * @param base_url Remote http(s) base URL. + * @return Zero on success or a negative error code. + */ int cHTTPX_AppRemote(chttpx_app_t* app, const char* name, const char* base_url) { chttpx_tls_client_config_t tls = cHTTPX_DefaultTLSClientConfig(); return cHTTPX_AppRemoteEx(app, name, base_url, &tls); } +/** + * Thread entry that runs one server listen loop. + * + * @param arg Server whose listen loop should run on this thread. + * @return Always NULL; required by the thread API. + */ static void* app_listener(void* arg) { _chttpx_server_listen((chttpx_serv_t*)arg); return NULL; } +/** + * Start listener threads for all registered servers. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ int cHTTPX_AppStart(chttpx_app_t* app) { if (!app || !app->_initialized || app->_started) @@ -280,6 +368,12 @@ int cHTTPX_AppStart(chttpx_app_t* app) return cHTTPX_OK; } +/** + * Join all server listener threads. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ int cHTTPX_AppWait(chttpx_app_t* app) { if (!app || !app->_initialized || !app->_started) @@ -300,6 +394,12 @@ int cHTTPX_AppWait(chttpx_app_t* app) return cHTTPX_OK; } +/** + * Start servers and block until they stop. + * + * @param app Application instance. + * @return Zero on success or a negative error code. + */ int cHTTPX_AppRun(chttpx_app_t* app) { int result = cHTTPX_AppStart(app); @@ -309,6 +409,11 @@ int cHTTPX_AppRun(chttpx_app_t* app) return cHTTPX_AppWait(app); } +/** + * Stop servers and release all application resources. + * + * @param app Application instance. + */ void cHTTPX_AppShutdown(chttpx_app_t* app) { if (!app || !app->_initialized) @@ -353,6 +458,11 @@ void cHTTPX_AppShutdown(chttpx_app_t* app) memset(app, 0, sizeof(*app)); } +/** + * Release heap data owned by a synthetic internal request. + * + * @param req Current inbound HTTP request. + */ static void free_internal_request(chttpx_request_t* req) { if (!req) @@ -374,6 +484,12 @@ static void free_internal_request(chttpx_request_t* req) free(req->query); } +/** + * Copy borrowed response bodies into owned storage. + * + * @param res Response object filled by the outbound HTTP/2 call. + * @return Zero on success or a negative error code. + */ static int stabilize_response(chttpx_response_t* res) { if (!res || !res->body || res->body_size == 0 || res->body_ownership == cHTTPX_BODY_OWNED) @@ -476,6 +592,9 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha return result; } +/** + * Parsed host, port, and path for remote HTTP calls. + */ typedef struct { char host[256]; @@ -484,6 +603,13 @@ typedef struct bool tls; } chttpx_remote_url_t; +/** + * Parse http(s) URL pieces for legacy remote helpers. + * + * @param url URL string to parse. + * @param parsed Output structure receiving parsed URL fields. + * @return Zero on success or a negative error code. + */ static int parse_remote_url(const char* url, chttpx_remote_url_t* parsed) { if (!url || !parsed) @@ -562,6 +688,10 @@ static int parse_remote_url(const char* url, chttpx_remote_url_t* parsed) return 1; } +/** + * Return the last socket error code for the platform. + * @return Zero on success or a negative error code. + */ static int socket_last_error(void) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -571,6 +701,12 @@ static int socket_last_error(void) #endif } +/** + * Return whether a socket error indicates timeout. + * + * @param error Platform or library error code to classify. + * @return True when the descriptor refers to an open socket. + */ static bool socket_error_is_timeout(int error) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -580,6 +716,12 @@ static bool socket_error_is_timeout(int error) #endif } +/** + * Return whether connect is still in progress. + * + * @param error Platform or library error code to classify. + * @return True when the error indicates a non-blocking connect is still pending. + */ static bool socket_error_is_in_progress(int error) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -589,6 +731,13 @@ static bool socket_error_is_in_progress(int error) #endif } +/** + * Toggle non-blocking mode on a socket. + * + * @param socket_fd Connected TCP socket. + * @param enabled Whether the socket should use non-blocking I/O. + * @return Zero on success or a negative error code. + */ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -608,6 +757,12 @@ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) #endif } +/** + * Apply call timeouts to a connected socket. + * + * @param socket_fd Connected TCP socket. + * @return Zero on success or a negative error code. + */ static int socket_set_call_timeouts(chttpx_socket_t socket_fd) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -626,6 +781,12 @@ static int socket_set_call_timeouts(chttpx_socket_t socket_fd) return cHTTPX_OK; } +/** + * Wait for a non-blocking connect to finish. + * + * @param socket_fd Connected TCP socket. + * @return Zero on success or a negative error code. + */ static int wait_for_connect(chttpx_socket_t socket_fd) { fd_set write_set; @@ -671,6 +832,13 @@ static int wait_for_connect(chttpx_socket_t socket_fd) return socket_error_is_timeout(socket_error) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; } +/** + * Establish a TCP connection to a parsed remote URL. + * + * @param remote Parsed remote host, port, and base path. + * @param connected Output socket set when TCP connect succeeds. + * @return Zero on success or a negative error code. + */ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* connected) { if (!remote || !connected) @@ -747,6 +915,12 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co return final_result; } +/** + * Normalize remote Content-Type for local responses. + * + * @param value Content-Type header value from a remote response. + * @return Stable library Content-Type constant for the given value. + */ static const char* remote_response_content_type(const char* value) { if (!value) @@ -760,6 +934,12 @@ static const char* remote_response_content_type(const char* value) return cHTTPX_CTYPE_OCTET; } +/** + * Extract Content-Type from a raw HTTP header block. + * + * @param headers Raw HTTP response headers including the status line. + * @return Content-Type field value within headers, or NULL when absent. + */ static const char* find_remote_content_type(char* headers) { char* line = strstr(headers, "\r\n"); @@ -789,6 +969,12 @@ static const char* find_remote_content_type(char* headers) return NULL; } +/** + * Log remote TLS failures through the source server logger. + * + * @param source Originating request for inherited metadata. + * @param message Log message text. + */ static void log_remote_tls_error(chttpx_request_t* source, const char* message) { chttpx_serv_t* server = source ? source->_server : NULL; @@ -832,6 +1018,16 @@ int cHTTPX_CallEx(chttpx_request_t* source, const char* server_name, const char* return cHTTPX_ERR_NOT_FOUND; } +/** + * Dispatch a request to a local or remote app server by name. + * + * @param req Current inbound HTTP request. + * @param server_name Server name as a null-terminated C string. + * @param method HTTP method name. + * @param path Request path or route pattern. + * @param res Response object filled by the outbound HTTP/2 call. + * @return Zero on success or a negative error code. + */ int cHTTPX_Call(chttpx_request_t* req, const char* server_name, const char* method, const char* path, chttpx_response_t* res) { if (!req) diff --git a/src/cHTTPX_app.h b/src/cHTTPX_app.h index 78f0f2c..fef6f36 100644 --- a/src/cHTTPX_app.h +++ b/src/cHTTPX_app.h @@ -17,6 +17,9 @@ extern "C" #include #include +/** + * Owns local servers and remote call targets. + */ typedef struct chttpx_app { void* _servers; @@ -82,6 +85,9 @@ extern "C" */ int cHTTPX_Call(chttpx_request_t* req, const char* server_name, const char* method, const char* path, chttpx_response_t* res); +/** + * Optional overrides for cHTTPX_CallEx(). + */ typedef struct { const void* body; diff --git a/src/cHTTPX_body.c b/src/cHTTPX_body.c index 4232dde..b9d866d 100644 --- a/src/cHTTPX_body.c +++ b/src/cHTTPX_body.c @@ -34,6 +34,13 @@ #include #include +/** + * Parse content length value. + * + * @param value Parameter `value`. + * @param content_length Parameter `content_length`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int parse_content_length_value(const char* value, size_t* content_length) { if (!value || !content_length || !*value || *value == '-') @@ -49,6 +56,12 @@ static int parse_content_length_value(const char* value, size_t* content_length) return 1; } +/** + * Transfer encoding is chunked. + * + * @param value Parameter `value`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int transfer_encoding_is_chunked(const char* value) { if (!value) @@ -64,6 +77,14 @@ static int transfer_encoding_is_chunked(const char* value) return length == 7 && strncasecmp(value, "chunked", 7) == 0; } +/** + * Parse request framing. + * + * @param req Current HTTP request. + * @param content_length Parameter `content_length`. + * @param chunked Parameter `chunked`. + * @return 1 when framing is valid, 0 when headers are inconsistent or invalid. + */ static int parse_request_framing(chttpx_request_t* req, size_t* content_length, bool* chunked) { if (!req || !content_length || !chunked) @@ -105,6 +126,13 @@ static int parse_request_framing(chttpx_request_t* req, size_t* content_length, return 1; } +/** + * Content type matches. + * + * @param value Parameter `value`. + * @param expected Parameter `expected`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int content_type_matches(const char* value, const char* expected) { if (!value || !expected) @@ -118,6 +146,17 @@ static int content_type_matches(const char* value, const char* expected) return suffix == '\0' || suffix == ';' || suffix == ' ' || suffix == '\t'; } +/** + * Append bytes. + * + * @param data Parameter `data`. + * @param size Parameter `size`. + * @param capacity Parameter `capacity`. + * @param bytes Parameter `bytes`. + * @param count Parameter `count`. + * @param limit Parameter `limit`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int append_bytes(unsigned char** data, size_t* size, size_t* capacity, const unsigned char* bytes, size_t count, size_t limit) { if (*size > limit || count > limit - *size || *size == SIZE_MAX || count > SIZE_MAX - *size - 1) @@ -150,6 +189,9 @@ static int append_bytes(unsigned char** data, size_t* size, size_t* capacity, co return 1; } +/** + * Incremental reader for chunked request bodies over TLS or plain sockets. + */ typedef struct { chttpx_socket_t client_fd; @@ -161,6 +203,14 @@ typedef struct size_t initial_offset; } chunked_reader_t; +/** + * Chunked reader read. + * + * @param reader Parameter `reader`. + * @param output Parameter `output`. + * @param output_size Parameter `output_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int chunked_reader_read(chunked_reader_t* reader, unsigned char* output, size_t output_size) { if (!reader || !output || output_size == 0) @@ -186,6 +236,14 @@ static int chunked_reader_read(chunked_reader_t* reader, unsigned char* output, return result; } +/** + * Chunked reader exact. + * + * @param reader Parameter `reader`. + * @param output Parameter `output`. + * @param output_size Parameter `output_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int chunked_reader_exact(chunked_reader_t* reader, unsigned char* output, size_t output_size) { size_t offset = 0; @@ -199,6 +257,14 @@ static int chunked_reader_exact(chunked_reader_t* reader, unsigned char* output, return 1; } +/** + * Chunked reader line. + * + * @param reader Parameter `reader`. + * @param line Parameter `line`. + * @param line_size Parameter `line_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int chunked_reader_line(chunked_reader_t* reader, char* line, size_t line_size) { if (!reader || !line || line_size < 2) @@ -232,14 +298,29 @@ static int chunked_reader_line(chunked_reader_t* reader, char* line, size_t line } } +/** + * Close stream. + * + * @param resource Parameter `resource`. + */ static void close_stream(void* resource) { if (resource) fclose((FILE*)resource); } -static int decode_chunked(chttpx_request_t* req, chttpx_socket_t client_fd, const unsigned char* initial, size_t initial_size, size_t limit, - bool spool_to_disk) +/** + * Decode chunked. + * + * @param req Current HTTP request. + * @param client_fd Parameter `client_fd`. + * @param initial Parameter `initial`. + * @param initial_size Parameter `initial_size`. + * @param limit Parameter `limit`. + * @param spool_to_disk Parameter `spool_to_disk`. + * @return true on success, false otherwise. + */ +static int decode_chunked(chttpx_request_t* req, chttpx_socket_t client_fd, const unsigned char* initial, size_t initial_size, size_t limit, bool spool_to_disk) { unsigned char* decoded = NULL; size_t decoded_size = 0; @@ -364,6 +445,15 @@ static int decode_chunked(chttpx_request_t* req, chttpx_socket_t client_fd, cons return 0; } +/** + * Spool multipart body. + * + * @param req Current HTTP request. + * @param client_fd Parameter `client_fd`. + * @param initial Parameter `initial`. + * @param initial_size Parameter `initial_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int spool_multipart_body(chttpx_request_t* req, chttpx_socket_t client_fd, const unsigned char* initial, size_t initial_size) { FILE* spool = tmpfile(); @@ -425,6 +515,14 @@ static int spool_multipart_body(chttpx_request_t* req, chttpx_socket_t client_fd } /* Parse body in request */ +/** + * Parse req body. + * + * @param req Current HTTP request. + * @param client_fd Connected client socket. + * @param buffer Initial receive buffer containing request headers. + * @param buffer_len Size of buffer in bytes. + */ void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buffer, size_t buffer_len) { chttpx_serv_t* server = req ? req->_server : NULL; diff --git a/src/cHTTPX_body.h b/src/cHTTPX_body.h index df5bfe7..fa284f4 100644 --- a/src/cHTTPX_body.h +++ b/src/cHTTPX_body.h @@ -15,14 +15,23 @@ extern "C" #include "cHTTPX_request.h" -#define MAX_BODY_IN_MEMORY 1048576 // 1 MB +#define MAX_BODY_IN_MEMORY 1048576 /* 1 MB */ - /* Parse body in request */ - void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buffer, size_t buffer_len); +/** + * Read and decode the HTTP request body (Content-Length or chunked). + * + * Populates req->body or req->_multipart_stream according to content type and + * server size limits. Sets req->_parse_status on framing or I/O errors. + * + * @param req Current HTTP request. + * @param client_fd Connected client socket. + * @param buffer Initial receive buffer containing the request headers. + * @param buffer_len Size of buffer in bytes. + */ +void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buffer, size_t buffer_len); #ifdef __cplusplus - extern } #endif -#endif \ No newline at end of file +#endif diff --git a/src/cHTTPX_compression.c b/src/cHTTPX_compression.c index fad3d31..7745ab1 100644 --- a/src/cHTTPX_compression.c +++ b/src/cHTTPX_compression.c @@ -25,6 +25,7 @@ #include +/** Server-owned compression configuration and duplicated type lists. */ typedef struct { chttpx_compression_config_t config; @@ -68,6 +69,13 @@ static const char* default_exclude_types[] = { "application/wasm", }; +/** + * Compression middleware registered. + * + * @param server HTTP server instance. + * @param middleware Parameter `middleware`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int compression_middleware_registered(const chttpx_serv_t* server, chttpx_middleware_t middleware) { if (!server || !middleware) @@ -80,6 +88,13 @@ static int compression_middleware_registered(const chttpx_serv_t* server, chttpx return 0; } +/** + * Compression log. + * + * @param req Current HTTP request. + * @param level Parameter `level`. + * @param message Parameter `message`. + */ static void compression_log(chttpx_request_t* req, chttpx_log_level_t level, const char* message) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -89,6 +104,12 @@ static void compression_log(chttpx_request_t* req, chttpx_log_level_t level, con server->logger(level, req->request_id, message, server->logger_data); } +/** + * Token character. + * + * @param ch Parameter `ch`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int token_character(unsigned char ch) { if (isalnum(ch)) @@ -117,6 +138,12 @@ static int token_character(unsigned char ch) } } +/** + * Valid encoding name. + * + * @param encoding Parameter `encoding`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int valid_encoding_name(const char* encoding) { if (!encoding || !*encoding || strcasecmp(encoding, "identity") == 0) @@ -129,6 +156,14 @@ static int valid_encoding_name(const char* encoding) return 1; } +/** + * Duplicate strings. + * + * @param source Parameter `source`. + * @param count Parameter `count`. + * @param destination Parameter `destination`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int duplicate_strings(const char** source, size_t count, char*** destination) { if (!destination || (count > 0 && !source)) @@ -162,6 +197,11 @@ static int duplicate_strings(const char** source, size_t count, char*** destinat return cHTTPX_OK; } +/** + * Free compression state. + * + * @param state Parameter `state`. + */ static void free_compression_state(chttpx_compression_state_t* state) { if (!state) @@ -182,12 +222,18 @@ static void free_compression_state(chttpx_compression_state_t* state) free(state); } -static int gzip_encode_buffer(const unsigned char* input, - size_t input_size, - int level, - unsigned char** output, - size_t* output_size, - void* user_data) +/** + * Gzip encode buffer. + * + * @param input Parameter `input`. + * @param input_size Parameter `input_size`. + * @param level Parameter `level`. + * @param output Parameter `output`. + * @param output_size Parameter `output_size`. + * @param user_data Parameter `user_data`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int gzip_encode_buffer(const unsigned char* input, size_t input_size, int level, unsigned char** output, size_t* output_size, void* user_data) { (void)user_data; @@ -294,10 +340,16 @@ chttpx_compression_config_t cHTTPX_CompressionDefault(void) }; } -static int copy_providers(const chttpx_compression_provider_t* providers, - size_t count, - chttpx_compression_provider_t** providers_out, - char*** encodings_out) +/** + * Copy providers. + * + * @param providers Parameter `providers`. + * @param count Parameter `count`. + * @param providers_out Parameter `providers_out`. + * @param encodings_out Parameter `encodings_out`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int copy_providers(const chttpx_compression_provider_t* providers, size_t count, chttpx_compression_provider_t** providers_out, char*** encodings_out) { if (!providers_out || !encodings_out || (count > 0 && !providers)) return cHTTPX_ERR_INVALID_ARGUMENT; @@ -341,6 +393,13 @@ static int copy_providers(const chttpx_compression_provider_t* providers, return cHTTPX_OK; } +/** + * Prepare compression state. + * + * @param config Parameter `config`. + * @param state_out Parameter `state_out`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int prepare_compression_state(const chttpx_compression_config_t* config, chttpx_compression_state_t** state_out) { if (!config || !state_out || @@ -396,6 +455,14 @@ static int prepare_compression_state(const chttpx_compression_config_t* config, return result; } +/** + * Wildcard match ci. + * + * @param value Parameter `value`. + * @param value_length Parameter `value_length`. + * @param pattern Parameter `pattern`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int wildcard_match_ci(const char* value, size_t value_length, const char* pattern) { if (!value || !pattern) @@ -441,6 +508,13 @@ static int wildcard_match_ci(const char* value, size_t value_length, const char* return pattern_index == pattern_length; } +/** + * Mime matches. + * + * @param mime Parameter `mime`. + * @param pattern Parameter `pattern`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int mime_matches(const char* mime, const char* pattern) { if (!mime || !pattern) @@ -454,6 +528,13 @@ static int mime_matches(const char* mime, const char* pattern) return wildcard_match_ci(mime, mime_length, pattern); } +/** + * Mime is eligible. + * + * @param state Parameter `state`. + * @param mime Parameter `mime`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int mime_is_eligible(const chttpx_compression_state_t* state, const char* mime) { if (!state || !mime) @@ -473,6 +554,13 @@ static int mime_is_eligible(const chttpx_compression_state_t* state, const char* return 0; } +/** + * Response header index. + * + * @param response HTTP response. + * @param name Parameter `name`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int response_header_index(const chttpx_response_t* response, const char* name) { if (!response || !name) @@ -485,12 +573,26 @@ static int response_header_index(const chttpx_response_t* response, const char* return -1; } +/** + * Response header get. + * + * @param response HTTP response. + * @param name Parameter `name`. + * @return Pointer or NULL on failure. + */ static const char* response_header_get(const chttpx_response_t* response, const char* name) { int index = response_header_index(response, name); return index >= 0 ? response->headers[index].value : NULL; } +/** + * Comma token contains. + * + * @param value Parameter `value`. + * @param token Parameter `token`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int comma_token_contains(const char* value, const char* token) { if (!value || !token) @@ -528,11 +630,23 @@ static int comma_token_contains(const char* value, const char* token) return 0; } +/** + * Vary has accept encoding. + * + * @param value Parameter `value`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int vary_has_accept_encoding(const char* value) { return value && (comma_token_contains(value, "*") || comma_token_contains(value, "Accept-Encoding")); } +/** + * Compression headers possible. + * + * @param response HTTP response. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int compression_headers_possible(const chttpx_response_t* response) { if (!response) @@ -557,6 +671,13 @@ static int compression_headers_possible(const chttpx_response_t* response) return 1; } +/** + * Add compression headers. + * + * @param response HTTP response. + * @param encoding Parameter `encoding`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int add_compression_headers(chttpx_response_t* response, const char* encoding) { if (!response || !encoding || cHTTPX_HeaderAdd(response, "Content-Encoding", encoding) != 0) @@ -588,6 +709,12 @@ static int add_compression_headers(chttpx_response_t* response, const char* enco return 1; } +/** + * Trim string. + * + * @param start Parameter `start`. + * @param end Parameter `end`. + */ static void trim_string(char** start, char** end) { while (*start < *end && isspace((unsigned char)**start)) @@ -596,6 +723,12 @@ static void trim_string(char** start, char** end) (*end)--; } +/** + * Parse quality parameter. + * + * @param parameters Parameter `parameters`. + * @return Computed value. + */ static double parse_quality_parameter(char* parameters) { double quality = 1.0; @@ -654,12 +787,17 @@ static double parse_quality_parameter(char* parameters) return quality; } -static void quality_from_header_value(const char* header, - const char* target, - double* exact_quality, - int* exact_found, - double* wildcard_quality, - int* wildcard_found) +/** + * Quality from header value. + * + * @param header Parameter `header`. + * @param target Parameter `target`. + * @param exact_quality Parameter `exact_quality`. + * @param exact_found Parameter `exact_found`. + * @param wildcard_quality Parameter `wildcard_quality`. + * @param wildcard_found Parameter `wildcard_found`. + */ +static void quality_from_header_value(const char* header, const char* target, double* exact_quality, int* exact_found, double* wildcard_quality, int* wildcard_found) { if (!header || !target || !exact_quality || !exact_found || !wildcard_quality || !wildcard_found) return; @@ -713,6 +851,15 @@ static void quality_from_header_value(const char* header, } } +/** + * Request encoding quality. + * + * @param req Current HTTP request. + * @param encoding Parameter `encoding`. + * @param identity Parameter `identity`. + * @param header_present Parameter `header_present`. + * @return Computed value. + */ static double request_encoding_quality(const chttpx_request_t* request, const char* encoding, int identity, int* header_present) { if (!request || !encoding) @@ -748,6 +895,12 @@ static double request_encoding_quality(const chttpx_request_t* request, const ch return wildcard_found ? wildcard_quality : 0.0; } +/** + * Make empty error. + * + * @param response HTTP response. + * @param status Parameter `status`. + */ static void make_empty_error(chttpx_response_t* response, int status) { if (!response) @@ -760,6 +913,13 @@ static void make_empty_error(chttpx_response_t* response, int status) response->compression_disabled = true; } +/** + * Response semantics allow compression. + * + * @param req Current HTTP request. + * @param response HTTP response. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int response_semantics_allow_compression(const chttpx_request_t* request, const chttpx_response_t* response) { if (!request || !response || response->compression_disabled || !response->body || response->body_size == 0) @@ -784,6 +944,13 @@ static int response_semantics_allow_compression(const chttpx_request_t* request, return 1; } +/** + * Compression middleware. + * + * @param req Current HTTP request. + * @param response HTTP response. + * @return Middleware chain result (out or next). + */ static chttpx_middleware_result_t compression_middleware(chttpx_request_t* request, chttpx_response_t* response) { chttpx_serv_t* server = request ? request->_server : NULL; @@ -926,6 +1093,11 @@ void cHTTPX_ResponseCompression(chttpx_response_t* response, bool enabled) response->compression_disabled = !enabled; } +/** + * Compression server cleanup. + * + * @param server HTTP server instance. + */ void _chttpx_compression_server_cleanup(chttpx_serv_t* server) { if (!server) diff --git a/src/cHTTPX_compression.h b/src/cHTTPX_compression.h index b90e274..b865467 100644 --- a/src/cHTTPX_compression.h +++ b/src/cHTTPX_compression.h @@ -25,12 +25,7 @@ extern "C" * The provider allocates *output with malloc-compatible ownership. The * library takes ownership on success and frees it during response cleanup. */ - typedef int (*chttpx_compression_encode_fn)(const unsigned char* input, - size_t input_size, - int level, - unsigned char** output, - size_t* output_size, - void* user_data); + typedef int (*chttpx_compression_encode_fn)(const unsigned char* input, size_t input_size, int level, unsigned char** output, size_t* output_size, void* user_data); /** * Compression provider descriptor. diff --git a/src/cHTTPX_cookies.c b/src/cHTTPX_cookies.c index 98b2da3..8b1d17a 100644 --- a/src/cHTTPX_cookies.c +++ b/src/cHTTPX_cookies.c @@ -36,6 +36,15 @@ #define CHTTPX_SAMESITE_STRICT 2 #define CHTTPX_SAMESITE_NONE_MODE 3 +/** + * Append formatted text to a fixed cookie header buffer. + * + * @param buffer Destination buffer. + * @param capacity Total size of buffer. + * @param offset In/out write offset. + * @param format printf-style format string. + * @return 1 on success, 0 if the buffer would overflow. + */ static int cookie_append(char* buffer, size_t capacity, size_t* offset, const char* format, ...) { if (*offset >= capacity) @@ -50,7 +59,11 @@ static int cookie_append(char* buffer, size_t capacity, size_t* offset, const ch return 1; } -/* Parse cookie in request */ +/** + * Parse Cookie header into the request cookie table. + * + * @param req Request whose Cookie header should be parsed. + */ void _parse_req_cookies(chttpx_request_t* req) { const char* cookie_header = cHTTPX_HeaderGet(req, "Cookie"); @@ -102,6 +115,11 @@ void _parse_req_cookies(chttpx_request_t* req) } } +/** + * Free heap-allocated cookies attached to a request. + * + * @param req Request whose cookies should be released. + */ void chttpx_free_req_cookie(chttpx_request_t* req) { if (!req) @@ -123,11 +141,9 @@ void chttpx_free_req_cookie(chttpx_request_t* req) * * Searches for a cookie in the request by its name (case-insensitive). * - * @param req Pointer to the HTTP request structure. - * @param name Cookie name to search for. - * - * @return Pointer to the cookie value string if found, - * or NULL if the cookie does not exist or input is invalid. + * @param req Pointer to the HTTP request structure. + * @param name Cookie name to search for. + * @return Pointer to the cookie structure if found, or NULL if missing or invalid input. */ const chttpx_cookie_t* cHTTPX_CookieGet(chttpx_request_t* req, const char* name) { @@ -159,8 +175,9 @@ const chttpx_cookie_t* cHTTPX_CookieGet(chttpx_request_t* req, const char* name) * - Secure * - HttpOnly * - * @param req Pointer to HTTP request/response structure. - * @param cookie Pointer to cookie structure. + * @param res Pointer to HTTP response structure. + * @param cookie Pointer to cookie structure. + * @return 0 on success, -1 on error. */ int cHTTPX_CookieSet(chttpx_response_t* res, const chttpx_cookie_t* cookie) { diff --git a/src/cHTTPX_cookies.h b/src/cHTTPX_cookies.h index 22b5904..d81fb06 100644 --- a/src/cHTTPX_cookies.h +++ b/src/cHTTPX_cookies.h @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2026 netcorelink + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `libchttpx.c` for details. + */ + #ifndef COOKIES_H #define COOKIES_H @@ -9,10 +16,18 @@ extern "C" #include "cHTTPX_request.h" #include "cHTTPX_response.h" - /* Parse cookie in request */ + /** + * Parse Cookie header into the request cookie table. + * + * @param req Request whose Cookie header should be parsed. + */ void _parse_req_cookies(chttpx_request_t* req); - /* Free cookies before response */ + /** + * Free heap-allocated cookies attached to a request. + * + * @param req Request whose cookies should be released. + */ void chttpx_free_req_cookie(chttpx_request_t* req); /** @@ -20,30 +35,21 @@ extern "C" * * Searches for a cookie in the request by its name (case-insensitive). * - * @param req Pointer to the HTTP request structure. - * @param name Cookie name to search for. - * - * @return Pointer to the cookie value string if found, - * or NULL if the cookie does not exist or input is invalid. + * @param req Pointer to the HTTP request structure. + * @param name Cookie name to search for. + * @return Pointer to the cookie structure if found, or NULL if missing or invalid input. */ const chttpx_cookie_t* cHTTPX_CookieGet(chttpx_request_t* req, const char* name); /** - * Set an HTTP cookie. + * Set an HTTP cookie via Set-Cookie (RFC 6265). * - * This function formats a Set-Cookie header according to RFC 6265 - * and appends it to the header list using HeaderAdd. + * Formats a Set-Cookie header and appends it with HeaderAdd. Supports Path, Domain, + * Expires, SameSite, Secure, and HttpOnly. * - * Supported attributes: - * - Path - * - Domain - * - Expires (GMT format) - * - SameSite (Lax, Strict, None) - * - Secure - * - HttpOnly - * - * @param req Pointer to HTTP request/response structure. - * @param cookie Pointer to cookie structure. + * @param res Pointer to HTTP response structure. + * @param cookie Pointer to cookie structure. + * @return 0 on success, -1 on error. */ int cHTTPX_CookieSet(chttpx_response_t* res, const chttpx_cookie_t* cookie); @@ -51,4 +57,4 @@ extern "C" } #endif -#endif \ No newline at end of file +#endif diff --git a/src/cHTTPX_cors.h b/src/cHTTPX_cors.h index f868eb2..6d3829c 100644 --- a/src/cHTTPX_cors.h +++ b/src/cHTTPX_cors.h @@ -18,6 +18,7 @@ extern "C" struct chttpx_serv; + /** CORS policy stored on the HTTP server. */ typedef struct { uint8_t enabled; @@ -34,23 +35,18 @@ extern "C" /** * Enable and configure CORS (Cross-Origin Resource Sharing). * - * The function copies the supplied configuration. Allowed origins are - * normalized into an immutable sorted table used for logarithmic lookup - * while requests are served. + * Copies the supplied configuration. Allowed origins are sorted for binary search at request time. * * @param server Initialized HTTP server to configure. * @param origins Array of exact allowed Origin header values. * @param origins_count Number of elements in origins. - * @param methods Comma-separated allowed methods, or NULL for the default - * "GET, POST, PUT, DELETE, OPTIONS" set. - * @param headers Comma-separated allowed request headers, or NULL for - * "Content-Type". + * @param methods Comma-separated allowed methods, or NULL for "GET, POST, PUT, DELETE, OPTIONS". + * @param headers Comma-separated allowed request headers, or NULL for "Content-Type". */ void cHTTPX_Cors(struct chttpx_serv* server, const char** origins, size_t origins_count, const char* methods, const char* headers); #ifdef __cplusplus - extern } #endif -#endif \ No newline at end of file +#endif diff --git a/src/cHTTPX_crosspltm.h b/src/cHTTPX_crosspltm.h index 7cc1fd5..85d216d 100644 --- a/src/cHTTPX_crosspltm.h +++ b/src/cHTTPX_crosspltm.h @@ -34,12 +34,20 @@ extern "C" #endif #ifdef _WIN32 - typedef SOCKET chttpx_socket_t; +/** Socket handle type (Windows SOCKET, POSIX int). */ +typedef SOCKET chttpx_socket_t; #else typedef int chttpx_socket_t; #endif #ifdef CHTTPX_PLATFORM_WINDOWS + /** + * Thread-safe localtime (POSIX localtime_r on Windows). + * + * @param timep Input time value. + * @param result Output struct tm buffer. + * @return result on success. + */ static inline struct tm* localtime_r(const time_t* timep, struct tm* result) { memset(result, 0, sizeof(*result)); @@ -47,6 +55,13 @@ typedef int chttpx_socket_t; return result; } + /** + * Thread-safe UTC breakdown (POSIX gmtime_r on Windows). + * + * @param timep Input time value. + * @param result Output struct tm buffer. + * @return result on success. + */ static inline struct tm* gmtime_r(const time_t* timep, struct tm* result) { memset(result, 0, sizeof(*result)); @@ -54,6 +69,13 @@ typedef int chttpx_socket_t; return result; } + /** + * clock_gettime compatibility for Windows. + * + * @param clock_id CLOCK_MONOTONIC or real-time clock id. + * @param value Output timespec. + * @return 0 on success, -1 on error. + */ static inline int chttpx_clock_gettime(int clock_id, struct timespec* value) { if (!value) @@ -93,6 +115,15 @@ typedef int chttpx_socket_t; #define strcasecmp _stricmp #endif + /** + * Find a byte substring within a buffer (POSIX memmem when unavailable). + * + * @param haystack Buffer to search. + * @param haystacklen Length of haystack. + * @param needle Substring to find. + * @param needlelen Length of needle. + * @return Pointer into haystack, or NULL if not found. + */ static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) { if (!needlelen) diff --git a/src/cHTTPX_event.c b/src/cHTTPX_event.c index 940eeb5..437663a 100644 --- a/src/cHTTPX_event.c +++ b/src/cHTTPX_event.c @@ -15,6 +15,12 @@ struct chttpx_event_loop volatile LONG wake_pending; }; +/** + * Translate portable event flags to poll/epoll/kqueue flags. + * + * @param events Poll interest mask or output event array. + * @return Zero on success or a negative error code. + */ static short native_events(uint32_t events) { short value = 0; @@ -25,6 +31,13 @@ static short native_events(uint32_t events) return value; } +/** + * Grow pollfd tables for the fallback event loop. + * + * @param loop Event loop instance. + * @param capacity Initial capacity when growing an internal table. + * @return Zero on success or a negative error code. + */ static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) { if (capacity <= loop->capacity) @@ -47,6 +60,13 @@ static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) return 1; } +/** + * Find the index of a registered socket descriptor. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) { for (size_t i = 0; i < loop->count; i++) @@ -55,11 +75,20 @@ static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) return -1; } +/** + * Allocate a platform-specific event loop. + * @return New event loop, or NULL on allocation failure. + */ chttpx_event_loop_t* _chttpx_event_create(void) { return calloc(1, sizeof(chttpx_event_loop_t)); } +/** + * Free an event loop and registered state. + * + * @param loop Event loop instance. + */ void _chttpx_event_destroy(chttpx_event_loop_t* loop) { if (!loop) @@ -69,6 +98,15 @@ void _chttpx_event_destroy(chttpx_event_loop_t* loop) free(loop); } +/** + * Register a socket for read/write monitoring. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop || find_fd(loop, fd) >= 0 || !ensure_capacity(loop, loop->count + 1)) @@ -81,6 +119,15 @@ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return 0; } +/** + * Update interest flags or user data for a socket. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -93,6 +140,12 @@ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return 0; } +/** + * Remove a socket from the event loop. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + */ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) { if (!loop) @@ -110,6 +163,15 @@ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) loop->count--; } +/** + * Wait for ready sockets or a wake signal. + * + * @param loop Event loop instance. + * @param events Poll interest mask or output event array. + * @param max_events Capacity of the output event array. + * @param timeout_ms Maximum wait in milliseconds, or -1 to block indefinitely. + * @return Zero on success or a negative error code. + */ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) { if (!loop || !events || max_events == 0) @@ -151,12 +213,23 @@ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t return (int)output; } +/** + * Interrupt a blocking event wait. + * + * @param loop Event loop instance. + */ void _chttpx_event_wake(chttpx_event_loop_t* loop) { if (loop) InterlockedExchange(&loop->wake_pending, 1); } +/** + * Set a socket to non-blocking mode. + * + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) { u_long mode = 1; @@ -177,6 +250,12 @@ struct chttpx_event_loop int wake_fd; }; +/** + * Translate portable event flags to poll/epoll/kqueue flags. + * + * @param events Portable read/write interest flags to translate. + * @return Native epoll interest mask for the portable flags. + */ static uint32_t native_events(uint32_t events) { uint32_t value = EPOLLRDHUP; @@ -187,6 +266,10 @@ static uint32_t native_events(uint32_t events) return value; } +/** + * Allocate a platform-specific event loop. + * @return New event loop, or NULL on allocation failure. + */ chttpx_event_loop_t* _chttpx_event_create(void) { chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); @@ -209,6 +292,11 @@ chttpx_event_loop_t* _chttpx_event_create(void) return NULL; } +/** + * Free an event loop and registered state. + * + * @param loop Event loop instance. + */ void _chttpx_event_destroy(chttpx_event_loop_t* loop) { if (!loop) @@ -218,6 +306,15 @@ void _chttpx_event_destroy(chttpx_event_loop_t* loop) free(loop); } +/** + * Register a socket for read/write monitoring. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -226,6 +323,15 @@ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return epoll_ctl(loop->fd, EPOLL_CTL_ADD, fd, &event); } +/** + * Update interest flags or user data for a socket. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -234,12 +340,27 @@ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return epoll_ctl(loop->fd, EPOLL_CTL_MOD, fd, &event); } +/** + * Remove a socket from the event loop. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + */ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) { if (loop) epoll_ctl(loop->fd, EPOLL_CTL_DEL, fd, NULL); } +/** + * Wait for ready sockets or a wake signal. + * + * @param loop Event loop instance. + * @param events Poll interest mask or output event array. + * @param max_events Capacity of the output event array. + * @param timeout_ms Maximum wait in milliseconds, or -1 to block indefinitely. + * @return Zero on success or a negative error code. + */ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) { if (!loop || !events || max_events == 0) @@ -279,6 +400,11 @@ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t return output; } +/** + * Interrupt a blocking event wait. + * + * @param loop Event loop instance. + */ void _chttpx_event_wake(chttpx_event_loop_t* loop) { if (!loop) @@ -288,6 +414,12 @@ void _chttpx_event_wake(chttpx_event_loop_t* loop) (void)ignored; } +/** + * Set a socket to non-blocking mode. + * + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) { int flags = fcntl(fd, F_GETFL, 0); @@ -309,6 +441,16 @@ struct chttpx_event_loop int fd; }; +/** + * Add, enable, or disable one kqueue filter for a socket. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param filter Optional event filter passed to the polling backend. + * @param enabled Whether the socket should use non-blocking I/O. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ static int change_filter(chttpx_event_loop_t* loop, chttpx_socket_t fd, int16_t filter, bool enabled, void* data) { struct kevent change; @@ -316,6 +458,10 @@ static int change_filter(chttpx_event_loop_t* loop, chttpx_socket_t fd, int16_t return kevent(loop->fd, &change, 1, NULL, 0, NULL); } +/** + * Allocate a platform-specific event loop. + * @return New event loop, or NULL on allocation failure. + */ chttpx_event_loop_t* _chttpx_event_create(void) { chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); @@ -338,6 +484,11 @@ chttpx_event_loop_t* _chttpx_event_create(void) return loop; } +/** + * Free an event loop and registered state. + * + * @param loop Event loop instance. + */ void _chttpx_event_destroy(chttpx_event_loop_t* loop) { if (!loop) @@ -346,6 +497,15 @@ void _chttpx_event_destroy(chttpx_event_loop_t* loop) free(loop); } +/** + * Register a socket for read/write monitoring. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -360,6 +520,15 @@ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return 0; } +/** + * Update interest flags or user data for a socket. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -369,6 +538,12 @@ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return change_filter(loop, fd, EVFILT_WRITE, (events & CHTTPX_EVENT_WRITE) != 0, data); } +/** + * Remove a socket from the event loop. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + */ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) { if (!loop) @@ -379,6 +554,15 @@ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) kevent(loop->fd, changes, 2, NULL, 0, NULL); } +/** + * Wait for ready sockets or a wake signal. + * + * @param loop Event loop instance. + * @param events Poll interest mask or output event array. + * @param max_events Capacity of the output event array. + * @param timeout_ms Maximum wait in milliseconds, or -1 to block indefinitely. + * @return Zero on success or a negative error code. + */ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) { if (!loop || !events || max_events == 0) @@ -432,6 +616,11 @@ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t return (int)output; } +/** + * Interrupt a blocking event wait. + * + * @param loop Event loop instance. + */ void _chttpx_event_wake(chttpx_event_loop_t* loop) { if (!loop) @@ -441,6 +630,12 @@ void _chttpx_event_wake(chttpx_event_loop_t* loop) kevent(loop->fd, &wake, 1, NULL, 0, NULL); } +/** + * Set a socket to non-blocking mode. + * + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) { int flags = fcntl(fd, F_GETFL, 0); @@ -465,6 +660,12 @@ struct chttpx_event_loop int wake_pipe[2]; }; +/** + * Translate portable event flags to poll/epoll/kqueue flags. + * + * @param events Poll interest mask or output event array. + * @return Zero on success or a negative error code. + */ static short native_events(uint32_t events) { short value = 0; @@ -475,6 +676,13 @@ static short native_events(uint32_t events) return value; } +/** + * Grow pollfd tables for the fallback event loop. + * + * @param loop Event loop instance. + * @param capacity Initial capacity when growing an internal table. + * @return Zero on success or a negative error code. + */ static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) { if (capacity <= loop->capacity) @@ -497,6 +705,13 @@ static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity) return 1; } +/** + * Find the index of a registered socket descriptor. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) { for (size_t i = 0; i < loop->count; i++) @@ -505,6 +720,10 @@ static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd) return -1; } +/** + * Allocate a platform-specific event loop. + * @return New event loop, or NULL on allocation failure. + */ chttpx_event_loop_t* _chttpx_event_create(void) { chttpx_event_loop_t* loop = calloc(1, sizeof(*loop)); @@ -528,6 +747,11 @@ chttpx_event_loop_t* _chttpx_event_create(void) return loop; } +/** + * Free an event loop and registered state. + * + * @param loop Event loop instance. + */ void _chttpx_event_destroy(chttpx_event_loop_t* loop) { if (!loop) @@ -539,6 +763,15 @@ void _chttpx_event_destroy(chttpx_event_loop_t* loop) free(loop); } +/** + * Register a socket for read/write monitoring. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop || find_fd(loop, fd) >= 0 || !ensure_capacity(loop, loop->count + 1)) @@ -549,6 +782,15 @@ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return 0; } +/** + * Update interest flags or user data for a socket. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + * @param events Poll interest mask or output event array. + * @param data Payload bytes for the current chunk. + * @return Zero on success or a negative error code. + */ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data) { if (!loop) @@ -561,6 +803,12 @@ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t ev return 0; } +/** + * Remove a socket from the event loop. + * + * @param loop Event loop instance. + * @param fd Socket descriptor. + */ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) { if (!loop) @@ -578,6 +826,15 @@ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd) loop->count--; } +/** + * Wait for ready sockets or a wake signal. + * + * @param loop Event loop instance. + * @param events Poll interest mask or output event array. + * @param max_events Capacity of the output event array. + * @param timeout_ms Maximum wait in milliseconds, or -1 to block indefinitely. + * @return Zero on success or a negative error code. + */ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms) { if (!loop || !events || max_events == 0) @@ -617,6 +874,11 @@ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t return (int)output; } +/** + * Interrupt a blocking event wait. + * + * @param loop Event loop instance. + */ void _chttpx_event_wake(chttpx_event_loop_t* loop) { if (!loop) @@ -626,6 +888,12 @@ void _chttpx_event_wake(chttpx_event_loop_t* loop) (void)ignored; } +/** + * Set a socket to non-blocking mode. + * + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ int _chttpx_socket_set_nonblocking(chttpx_socket_t fd) { int flags = fcntl(fd, F_GETFL, 0); diff --git a/src/cHTTPX_event.h b/src/cHTTPX_event.h index 2415c7f..49632a0 100644 --- a/src/cHTTPX_event.h +++ b/src/cHTTPX_event.h @@ -11,8 +11,14 @@ #define CHTTPX_EVENT_ERROR 0x04u #define CHTTPX_EVENT_WAKE 0x08u +/** + * Opaque platform event multiplexer state. + */ typedef struct chttpx_event_loop chttpx_event_loop_t; +/** + * One ready socket and its registered interest flags. + */ typedef struct { chttpx_socket_t fd; @@ -20,13 +26,40 @@ typedef struct void* data; } chttpx_event_t; +/** Allocate a platform-specific event loop. @return Event loop or NULL on failure. */ chttpx_event_loop_t* _chttpx_event_create(void); + +/** Free an event loop and registered state. @param loop Event loop to destroy. */ void _chttpx_event_destroy(chttpx_event_loop_t* loop); + +/** + * Register a socket for read/write monitoring. + * + * @return Zero on success or -1 on failure. + */ int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data); + +/** + * Update interest flags or user data for a socket. + * + * @return Zero on success or -1 on failure. + */ int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data); + +/** Remove a socket from the event loop. */ void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd); + +/** + * Wait for ready sockets or a wake signal. + * + * @return Number of ready events, zero on timeout, or -1 on error. + */ int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms); + +/** Interrupt a blocking event wait. */ void _chttpx_event_wake(chttpx_event_loop_t* loop); + +/** Set a socket to non-blocking mode. @return Zero on success or -1 on failure. */ int _chttpx_socket_set_nonblocking(chttpx_socket_t fd); #endif diff --git a/src/cHTTPX_headers.c b/src/cHTTPX_headers.c index 429e56a..d8a4c4a 100644 --- a/src/cHTTPX_headers.c +++ b/src/cHTTPX_headers.c @@ -52,13 +52,13 @@ const char* cHTTPX_HeaderGet(chttpx_request_t* req, const char* name) /** * Add a new HTTP header. * - * This function appends a header to the request/response header list. - * Unlike HeaderSet, it does NOT replace existing headers with the same name. - * This is required for headers like "Set-Cookie" that may appear multiple times. + * Appends a header to the response header list. Unlike HeaderSet, it does not + * replace existing headers with the same name (required for Set-Cookie). * - * @param req Pointer to HTTP request/response structure. - * @param name Header name. + * @param res Pointer to HTTP response structure. + * @param name Header name. * @param value Header value. + * @return 0 on success, -1 on error. */ int cHTTPX_HeaderAdd(chttpx_response_t* res, const char* name, const char* value) { @@ -123,10 +123,10 @@ int cHTTPX_HeaderSet(chttpx_request_t* req, const char* name, const char* value) } /** - * Get the client's IP from the HEADER request. + * Get the client's IP from request headers (X-Forwarded-For or Remote-Addr). * - * @param req a pointer to the query structure - * @return const char* Client's IP + * @param req Pointer to the HTTP request. + * @return Client IP string, or NULL if not present. */ const char* cHTTPX_ClientIP(chttpx_request_t* req) { @@ -140,6 +140,14 @@ const char* cHTTPX_ClientIP(chttpx_request_t* req) return ip; } +/** + * Append one parsed header to the request (truncates overlong name/value). + * + * @param req Request to update. + * @param name Header name. + * @param value Header value. + * @return 1 on success, 0 when MAX_HEADERS is reached. + */ static int add_header(chttpx_request_t* req, const char* name, const char* value) { if (req->headers_count >= MAX_HEADERS) @@ -198,6 +206,13 @@ static int valid_header_name(const char* name, size_t length) return 1; } +/** + * Validate an HTTP field-value (no bare CR/LF or DEL). + * + * @param value Field value bytes (not necessarily null-terminated). + * @param length Number of bytes in value. + * @return 1 if valid, otherwise 0. + */ static int valid_header_value(const char* value, size_t length) { if (!value) @@ -213,7 +228,13 @@ static int valid_header_value(const char* value, size_t length) return 1; } -/* Parse headers in request */ +/** + * Parse HTTP headers from a raw request buffer. + * + * @param req Pointer to the HTTP request being parsed. + * @param buffer Raw bytes containing the request line and headers. + * @param buffer_len Length of buffer in bytes. + */ void _parse_req_headers(chttpx_request_t* req, char* buffer, size_t buffer_len) { if (!req || !buffer) diff --git a/src/cHTTPX_headers.h b/src/cHTTPX_headers.h index ddbe987..2262293 100644 --- a/src/cHTTPX_headers.h +++ b/src/cHTTPX_headers.h @@ -27,20 +27,19 @@ extern "C" /** * Add a new HTTP header. * - * This function appends a header to the request/response header list. - * Unlike HeaderSet, it does NOT replace existing headers with the same name. - * This is required for headers like "Set-Cookie" that may appear multiple times. + * Appends a header to the response header list. Unlike HeaderSet, it does not + * replace existing headers with the same name (required for Set-Cookie). * - * @param res Pointer to HTTP request/response structure. - * @param name Header name. + * @param res Pointer to HTTP response structure. + * @param name Header name. * @param value Header value. + * @return 0 on success, -1 on error. */ int cHTTPX_HeaderAdd(chttpx_response_t* res, const char* name, const char* value); /** * Set or add a request header. - * If header exists (case-insensitive), its value will be replaced. - * Otherwise a new header will be added. + * If a header exists (case-insensitive), its value is replaced; otherwise a new header is added. * * @param req Pointer to the HTTP request. * @param name Header name. @@ -50,19 +49,24 @@ extern "C" int cHTTPX_HeaderSet(chttpx_request_t* req, const char* name, const char* value); /** - * Get the client's IP from the HEADER request. + * Get the client's IP from request headers (X-Forwarded-For or Remote-Addr). * - * @param req a pointer to the query structure - * @return const char* Client's IP + * @param req Pointer to the HTTP request. + * @return Client IP string, or NULL if not present. */ const char* cHTTPX_ClientIP(chttpx_request_t* req); - /* Parse headers in request */ + /** + * Parse HTTP headers from a raw request buffer. + * + * @param req Pointer to the HTTP request being parsed. + * @param buffer Raw bytes containing the request line and headers. + * @param buffer_len Length of buffer in bytes. + */ void _parse_req_headers(chttpx_request_t* req, char* buffer, size_t buffer_len); #ifdef __cplusplus - extern } #endif -#endif \ No newline at end of file +#endif diff --git a/src/cHTTPX_http.h b/src/cHTTPX_http.h index cd8da45..3cd1c1f 100644 --- a/src/cHTTPX_http.h +++ b/src/cHTTPX_http.h @@ -173,7 +173,6 @@ extern "C" #define cHTTPX_StatusInvalidSSLCertificate 526 #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index 03a1291..072eb57 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -22,6 +22,9 @@ #define CHTTPX_H2_CALL_TIMEOUT_SEC 30 #define CHTTPX_H2_MAX_AUTHORITY 512 +/** + * Per-stream HTTP/2 request/response state on the server. + */ typedef struct chttpx_h2_stream { int32_t id; @@ -42,6 +45,9 @@ typedef struct chttpx_h2_stream size_t response_body_sent; } chttpx_h2_stream_t; +/** + * Server-side HTTP/2 session bound to one connection. + */ typedef struct { chttpx_serv_t* server; @@ -50,6 +56,9 @@ typedef struct nghttp2_session* session; } chttpx_h2_server_t; +/** + * Client-side HTTP/2 session state for outbound calls. + */ typedef struct { chttpx_socket_t fd; @@ -66,6 +75,9 @@ typedef struct size_t body_capacity; } chttpx_h2_client_t; +/** + * Streaming request body cursor for nghttp2 data provider. + */ typedef struct { const unsigned char* body; @@ -73,6 +85,9 @@ typedef struct size_t offset; } chttpx_h2_client_body_t; +/** + * Parsed remote authority and path for HTTP/2 calls. + */ typedef struct { bool tls; @@ -81,6 +96,13 @@ typedef struct char base_path[CHTTPX_MAX_PATH]; } chttpx_h2_url_t; +/** + * Build one nghttp2 name/value pair from C strings. + * + * @param name Header name bytes as a null-terminated C string. + * @param value Header value bytes as a null-terminated C string. + * @return nghttp2 name/value pair referencing the given strings. + */ static nghttp2_nv h2_nv(const char* name, const char* value) { return (nghttp2_nv){ @@ -92,6 +114,12 @@ static nghttp2_nv h2_nv(const char* name, const char* value) }; } +/** + * Return whether a header must not be forwarded on HTTP/2. + * + * @param name Header field name to test. + * @return True when the header must not be forwarded on HTTP/2. + */ static bool h2_forbidden_header(const char* name) { return strcasecmp(name, "connection") == 0 || @@ -101,11 +129,23 @@ static bool h2_forbidden_header(const char* name) strcasecmp(name, "upgrade") == 0; } +/** + * Fetch stream user data from an nghttp2 session. + * + * @param session nghttp2 session owning the stream. + * @param stream_id HTTP/2 stream id to look up. + * @return Stream user data for the id, or NULL when unset. + */ static chttpx_h2_stream_t* h2_stream(nghttp2_session* session, int32_t stream_id) { return (chttpx_h2_stream_t*)nghttp2_session_get_stream_user_data(session, stream_id); } +/** + * Release stream buffers and the stream object. + * + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + */ static void h2_stream_free(chttpx_h2_stream_t* stream) { if (!stream) @@ -115,11 +155,17 @@ static void h2_stream_free(chttpx_h2_stream_t* stream) free(stream); } -static int h2_stream_add_header(chttpx_h2_stream_t* stream, - const uint8_t* name, - size_t namelen, - const uint8_t* value, - size_t valuelen) +/** + * Decode one HPACK header into stream request state. + * + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @param name Header field name bytes. + * @param namelen Header name length in bytes. + * @param value Header field value bytes. + * @param valuelen Header value length in bytes. + * @return Zero on success or a negative error code. + */ +static int h2_stream_add_header(chttpx_h2_stream_t* stream, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen) { if (!stream || !name || !value) return NGHTTP2_ERR_CALLBACK_FAILURE; @@ -177,6 +223,13 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, return 0; } +/** + * Look up a request header by case-insensitive name. + * + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @param name Header field name to match case-insensitively. + * @return Header value string, or NULL when the name is absent. + */ static const char* h2_request_header(const chttpx_h2_stream_t* stream, const char* name) { if (!stream || !name) @@ -187,6 +240,13 @@ static const char* h2_request_header(const chttpx_h2_stream_t* stream, const cha return NULL; } +/** + * Choose max body size based on content type. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @return Zero on success or a negative error code. + */ static size_t h2_body_limit(const chttpx_h2_server_t* connection, const chttpx_h2_stream_t* stream) { const char* content_type = h2_request_header(stream, "content-type"); @@ -198,6 +258,15 @@ static size_t h2_body_limit(const chttpx_h2_server_t* connection, const chttpx_h return connection->server->max_upload_size; } +/** + * Append request body bytes with size enforcement. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @param data Payload bytes for the current chunk. + * @param len Number of payload bytes in the chunk. + * @return Zero on success or a negative error code. + */ static int h2_append_body(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream, const uint8_t* data, size_t len) { if (!stream || (len && !data)) @@ -242,10 +311,16 @@ static int h2_append_body(chttpx_h2_server_t* connection, chttpx_h2_stream_t* st return 0; } -static int h2_build_request_text(const chttpx_h2_server_t* connection, - const chttpx_h2_stream_t* stream, - char** output, - size_t* output_size) +/** + * Serialize stream state into an HTTP/1-style header block. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @param output Output pointer receiving allocated request header text. + * @param output_size Output length of the serialized header block. + * @return Zero on success or a negative error code. + */ +static int h2_build_request_text(const chttpx_h2_server_t* connection, const chttpx_h2_stream_t* stream, char** output, size_t* output_size) { if (!connection || !stream || !output || !output_size || !stream->method[0] || !stream->path[0]) return cHTTPX_ERR_PROTOCOL; @@ -326,29 +401,40 @@ static int h2_build_request_text(const chttpx_h2_server_t* connection, return cHTTPX_ERR_LIMIT; } -static ssize_t h2_server_send(nghttp2_session* session, - const uint8_t* data, - size_t length, - int flags, - void* user_data) +/** + * nghttp2 send callback writing to the connection socket. + * + * @param session Active nghttp2 session for the callback. + * @param data Payload bytes for the current chunk. + * @param length Buffer capacity or number of bytes to transfer. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Bytes transferred or a negative nghttp2 status. + */ +static ssize_t h2_server_send(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data) { (void)session; (void)flags; chttpx_h2_server_t* connection = user_data; if (!connection) return NGHTTP2_ERR_CALLBACK_FAILURE; - return _chttpx_io_send_all(connection->fd, connection->tls_session, data, length) == cHTTPX_OK - ? (ssize_t)length + return _chttpx_io_send_all(connection->fd, connection->tls_session, data, length) == cHTTPX_OK ? (ssize_t)length : NGHTTP2_ERR_CALLBACK_FAILURE; } -static ssize_t h2_response_read(nghttp2_session* session, - int32_t stream_id, - uint8_t* buf, - size_t length, - uint32_t* data_flags, - nghttp2_data_source* source, - void* user_data) +/** + * nghttp2 data provider for serialized response bodies. + * + * @param session Active nghttp2 session for the callback. + * @param stream_id HTTP/2 stream identifier. + * @param buf Output buffer filled by the nghttp2 data provider. + * @param length Buffer capacity or number of bytes to transfer. + * @param data_flags Out flags telling nghttp2 when body transmission ends. + * @param source Originating request for inherited metadata. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Bytes transferred or a negative nghttp2 status. + */ +static ssize_t h2_response_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) { (void)session; (void)stream_id; @@ -368,10 +454,16 @@ static ssize_t h2_response_read(nghttp2_session* session, return (ssize_t)take; } -static int h2_submit_text_response(chttpx_h2_server_t* connection, - chttpx_h2_stream_t* stream, - int status, - const char* content_type) +/** + * Submit a minimal HTTP/2 response without a body. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @param status HTTP status code for the response. + * @param content_type Content-Type header value. + * @return Zero on success or a negative error code. + */ +static int h2_submit_text_response(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream, int status, const char* content_type) { char status_text[4]; snprintf(status_text, sizeof(status_text), "%03d", status); @@ -389,6 +481,13 @@ static int h2_submit_text_response(chttpx_h2_server_t* connection, return rv; } +/** + * Submit a full response parsed from HTTP/1 text. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @return Zero on success or a negative error code. + */ static int h2_submit_serialized_response(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream) { if (!connection || !stream || !stream->response) @@ -448,16 +547,19 @@ static int h2_submit_serialized_response(chttpx_h2_server_t* connection, chttpx_ .read_callback = h2_response_read, }; - int rv = nghttp2_submit_response(connection->session, - stream->id, - headers, - count, - body_size ? &provider : NULL); + int rv = nghttp2_submit_response(connection->session, stream->id, headers, count, body_size ? &provider : NULL); if (rv == 0) stream->responded = true; return rv; } +/** + * Dispatch one complete HTTP/2 request to the server core. + * + * @param connection Server-side HTTP/2 connection state. + * @param stream HTTP/2 stream being decoded, buffered, or responded to. + * @return Zero on success or a negative error code. + */ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream) { if (!connection || !stream || stream->responded) @@ -470,10 +572,9 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t size_t header_size = 0; int build_result = h2_build_request_text(connection, stream, &headers, &header_size); if (build_result != cHTTPX_OK) - return h2_submit_text_response(connection, - stream, - build_result == cHTTPX_ERR_LIMIT ? cHTTPX_StatusRequestHeaderFieldsTooLarge : cHTTPX_StatusBadRequest, - cHTTPX_CTYPE_TEXT); + return h2_submit_text_response(connection, stream, + build_result == cHTTPX_ERR_LIMIT ? cHTTPX_StatusRequestHeaderFieldsTooLarge : cHTTPX_StatusBadRequest, + cHTTPX_CTYPE_TEXT); unsigned char* body = stream->body; size_t body_size = stream->body_size; @@ -483,17 +584,8 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t char* response = NULL; size_t response_size = 0; - int execute_result = _chttpx_execute_prefetched(connection->server, - connection->fd, - connection->tls_session, - headers, - header_size, - body, - body_size, - NULL, - body_size, - &response, - &response_size); + int execute_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session, + headers, header_size, body, body_size, NULL, body_size, &response, &response_size); free(headers); if (execute_result != cHTTPX_OK || !response) @@ -507,6 +599,14 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t return h2_submit_serialized_response(connection, stream); } +/** + * Allocate stream state when request headers begin. + * + * @param session Active nghttp2 session for the callback. + * @param frame Incoming nghttp2 frame metadata. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { (void)user_data; @@ -527,14 +627,20 @@ static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame return 0; } -static int h2_server_header(nghttp2_session* session, - const nghttp2_frame* frame, - const uint8_t* name, - size_t namelen, - const uint8_t* value, - size_t valuelen, - uint8_t flags, - void* user_data) +/** + * Handle one request header field on the server session. + * + * @param session Active nghttp2 session for the callback. + * @param frame Incoming nghttp2 frame metadata. + * @param name Header field name bytes. + * @param namelen Header name length in bytes. + * @param value Header field value bytes. + * @param valuelen Header value length in bytes. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_server_header(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data) { (void)flags; (void)user_data; @@ -544,12 +650,18 @@ static int h2_server_header(nghttp2_session* session, return h2_stream_add_header(stream, name, namelen, value, valuelen); } -static int h2_server_data(nghttp2_session* session, - uint8_t flags, - int32_t stream_id, - const uint8_t* data, - size_t len, - void* user_data) +/** + * Handle one request DATA chunk on the server session. + * + * @param session Active nghttp2 session for the callback. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param stream_id HTTP/2 stream identifier. + * @param data Payload bytes for the current chunk. + * @param len Number of payload bytes in the chunk. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_server_data(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) { (void)flags; chttpx_h2_server_t* connection = user_data; @@ -557,6 +669,14 @@ static int h2_server_data(nghttp2_session* session, return h2_append_body(connection, stream, data, len); } +/** + * Finalize request handling when END_STREAM is seen. + * + * @param session Active nghttp2 session for the callback. + * @param frame Incoming nghttp2 frame metadata. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { chttpx_h2_server_t* connection = user_data; @@ -577,10 +697,16 @@ static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* f return rv == 0 ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; } -static int h2_server_stream_close(nghttp2_session* session, - int32_t stream_id, - uint32_t error_code, - void* user_data) +/** + * Free stream state when nghttp2 closes a stream. + * + * @param session Active nghttp2 session for the callback. + * @param stream_id HTTP/2 stream identifier. + * @param error_code nghttp2 stream error code reported on close. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_server_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) { (void)error_code; (void)user_data; @@ -593,6 +719,14 @@ static int h2_server_stream_close(nghttp2_session* session, return 0; } +/** + * Serve one HTTP/2 connection until shutdown or I/O failure. + * + * @param server Server instance. + * @param client_fd Connected client socket. + * @param tls_session OpenSSL session for TLS I/O, or NULL for cleartext. + * @return Zero on success or a negative error code. + */ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session) { if (!server) @@ -673,6 +807,13 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* return result; } +/** + * Parse http(s) base URL into host, port, and path. + * + * @param base_url Remote http(s) base URL. + * @param parsed Output structure receiving parsed URL fields. + * @return Zero on success or a negative error code. + */ static int h2_parse_url(const char* base_url, chttpx_h2_url_t* parsed) { if (!base_url || !parsed) @@ -729,6 +870,12 @@ static int h2_parse_url(const char* base_url, chttpx_h2_url_t* parsed) return 1; } +/** + * Apply outbound call read/write timeouts on a socket. + * + * @param fd Socket descriptor. + * @return Zero on success or a negative error code. + */ static int h2_set_call_timeouts(chttpx_socket_t fd) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -745,6 +892,13 @@ static int h2_set_call_timeouts(chttpx_socket_t fd) return cHTTPX_OK; } +/** + * TCP-connect to a parsed remote URL. + * + * @param remote Parsed remote host, port, and base path. + * @param connected Output socket set when TCP connect succeeds. + * @return Zero on success or a negative error code. + */ static int h2_connect(const chttpx_h2_url_t* remote, chttpx_socket_t* connected) { if (!remote || !connected) @@ -789,6 +943,12 @@ static int h2_connect(const chttpx_h2_url_t* remote, chttpx_socket_t* connected) return final_result; } +/** + * Map arbitrary content types to library constants. + * + * @param content_type Content-Type value from the remote response. + * @return Stable library Content-Type constant for the given value. + */ static const char* h2_stable_content_type(const char* content_type) { if (!content_type || !*content_type) @@ -815,30 +975,41 @@ static const char* h2_stable_content_type(const char* content_type) return cHTTPX_CTYPE_OCTET; } -static ssize_t h2_client_send(nghttp2_session* session, - const uint8_t* data, - size_t length, - int flags, - void* user_data) +/** + * nghttp2 send callback for outbound client sessions. + * + * @param session Active nghttp2 session for the callback. + * @param data Payload bytes for the current chunk. + * @param length Buffer capacity or number of bytes to transfer. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Bytes transferred or a negative nghttp2 status. + */ +static ssize_t h2_client_send(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data) { (void)session; (void)flags; chttpx_h2_client_t* client = user_data; if (!client) return NGHTTP2_ERR_CALLBACK_FAILURE; - return _chttpx_io_send_all(client->fd, client->tls_session, data, length) == cHTTPX_OK - ? (ssize_t)length + return _chttpx_io_send_all(client->fd, client->tls_session, data, length) == cHTTPX_OK ? (ssize_t)length : NGHTTP2_ERR_CALLBACK_FAILURE; } -static int h2_client_header(nghttp2_session* session, - const nghttp2_frame* frame, - const uint8_t* name, - size_t namelen, - const uint8_t* value, - size_t valuelen, - uint8_t flags, - void* user_data) +/** + * Collect response headers for an outbound HTTP/2 call. + * + * @param session Active nghttp2 session for the callback. + * @param frame Incoming nghttp2 frame metadata. + * @param name Header field name bytes. + * @param namelen Header name length in bytes. + * @param value Header field value bytes. + * @param valuelen Header value length in bytes. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_client_header(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data) { (void)session; (void)flags; @@ -885,12 +1056,18 @@ static int h2_client_header(nghttp2_session* session, return 0; } -static int h2_client_data(nghttp2_session* session, - uint8_t flags, - int32_t stream_id, - const uint8_t* data, - size_t len, - void* user_data) +/** + * Buffer response body bytes for an outbound HTTP/2 call. + * + * @param session Active nghttp2 session for the callback. + * @param flags Frame or DATA flags supplied by nghttp2. + * @param stream_id HTTP/2 stream identifier. + * @param data Payload bytes for the current chunk. + * @param len Number of payload bytes in the chunk. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_client_data(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) { (void)session; (void)flags; @@ -928,10 +1105,16 @@ static int h2_client_data(nghttp2_session* session, return 0; } -static int h2_client_stream_close(nghttp2_session* session, - int32_t stream_id, - uint32_t error_code, - void* user_data) +/** + * Mark client stream complete when nghttp2 closes it. + * + * @param session Active nghttp2 session for the callback. + * @param stream_id HTTP/2 stream identifier. + * @param error_code nghttp2 stream error code reported on close. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Zero on success or a negative error code. + */ +static int h2_client_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) { (void)session; (void)error_code; @@ -941,13 +1124,19 @@ static int h2_client_stream_close(nghttp2_session* session, return 0; } -static ssize_t h2_client_body_read(nghttp2_session* session, - int32_t stream_id, - uint8_t* buf, - size_t length, - uint32_t* data_flags, - nghttp2_data_source* source, - void* user_data) +/** + * nghttp2 data provider for outbound request bodies. + * + * @param session Active nghttp2 session for the callback. + * @param stream_id HTTP/2 stream identifier. + * @param buf Output buffer filled by the nghttp2 data provider. + * @param length Buffer capacity or number of bytes to transfer. + * @param data_flags Out flags telling nghttp2 when body transmission ends. + * @param source Originating request for inherited metadata. + * @param user_data User pointer registered with the nghttp2 callback. + * @return Bytes transferred or a negative nghttp2 status. + */ +static ssize_t h2_client_body_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) { (void)session; (void)stream_id; @@ -966,14 +1155,8 @@ static ssize_t h2_client_body_read(nghttp2_session* session, return (ssize_t)take; } -int _chttpx_http2_call(chttpx_request_t* source, - const char* base_url, - const chttpx_tls_client_config_t* tls_config, - const char* method, - const char* path, - const void* body, - size_t body_size, - const char* content_type, +int _chttpx_http2_call(chttpx_request_t* source, const char* base_url, const chttpx_tls_client_config_t* tls_config, + const char* method, const char* path, const void* body, size_t body_size, const char* content_type, chttpx_response_t* res) { if (!source || !base_url || !method || !path || !res || (body_size && !body)) @@ -1061,9 +1244,8 @@ int _chttpx_http2_call(chttpx_request_t* source, char content_length[32]; snprintf(content_length, sizeof(content_length), "%zu", body_size); - const char* selected_content_type = content_type && *content_type - ? content_type - : (source->content_type[0] ? source->content_type : cHTTPX_CTYPE_JSON); + const char* selected_content_type = content_type && *content_type ? content_type + : (source->content_type[0] ? source->content_type : cHTTPX_CTYPE_JSON); nghttp2_nv headers[MAX_HEADERS + 8]; char lowercase_names[MAX_HEADERS][MAX_HEADER_NAME]; @@ -1110,12 +1292,7 @@ int _chttpx_http2_call(chttpx_request_t* source, .read_callback = h2_client_body_read, }; - client.stream_id = nghttp2_submit_request(client.session, - NULL, - headers, - count, - body_size ? &provider : NULL, - NULL); + client.stream_id = nghttp2_submit_request(client.session, NULL, headers, count, body_size ? &provider : NULL, NULL); if (client.stream_id < 0 || nghttp2_session_send(client.session) != 0) { result = cHTTPX_ERR_PROTOCOL; @@ -1157,10 +1334,7 @@ int _chttpx_http2_call(chttpx_request_t* source, goto done; } - *res = cHTTPX_ResBinary((uint16_t)client.status, - h2_stable_content_type(client.content_type), - client.body, - client.body_size); + *res = cHTTPX_ResBinary((uint16_t)client.status, h2_stable_content_type(client.content_type), client.body, client.body_size); if (!res->status) { result = cHTTPX_ERR_MEMORY; diff --git a/src/cHTTPX_http2.h b/src/cHTTPX_http2.h index 07d5579..889d09e 100644 --- a/src/cHTTPX_http2.h +++ b/src/cHTTPX_http2.h @@ -8,17 +8,31 @@ extern "C" { #endif +/** + * Serve one HTTP/2 connection until shutdown or I/O failure. + * + * @param server Owning server instance. + * @param client_fd Connected client socket. + * @param tls_session OpenSSL session, or NULL for cleartext h2c. + * @return Zero on success or a negative error code. + */ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session); -int _chttpx_http2_call(chttpx_request_t* source, - const char* base_url, - const chttpx_tls_client_config_t* tls_config, - const char* method, - const char* path, - const void* body, - size_t body_size, - const char* content_type, - chttpx_response_t* res); +/** + * Issue an outbound HTTP/2 request and fill a response object. + * + * @param source Originating request used for inherited metadata. + * @param base_url Remote http(s) base URL. + * @param tls_config Client TLS settings for https URLs. + * @param method HTTP method. + * @param path Request path. + * @param body Optional request body. + * @param body_size Body length in bytes. + * @param content_type Optional Content-Type override. + * @param res Output response object. + * @return Zero on success or a negative error code. + */ +int _chttpx_http2_call(chttpx_request_t* source, const char* base_url, const chttpx_tls_client_config_t* tls_config, const char* method, const char* path, const void* body, size_t body_size, const char* content_type, chttpx_response_t* res); #ifdef __cplusplus } diff --git a/src/cHTTPX_i18n.c b/src/cHTTPX_i18n.c index 7993b27..0a8e368 100644 --- a/src/cHTTPX_i18n.c +++ b/src/cHTTPX_i18n.c @@ -36,6 +36,13 @@ static i18n_manager_t* i18n_manager = NULL; +/** + * Load one locale JSON file into an i18n_locale_t structure. + * + * @param path Path to the JSON file. + * @param locale Locale code derived from the file name. + * @return Populated locale (empty on failure). + */ static i18n_locale_t load_locale_file(const char* path, const char* locale) { i18n_locale_t loc; @@ -130,6 +137,9 @@ static i18n_locale_t load_locale_file(const char* path, const char* locale) return loc; } +/** + * Free the global i18n manager (registered with atexit). + */ static void i18n_shutdown(void) { if (!i18n_manager) @@ -163,10 +173,7 @@ static void i18n_shutdown(void) * * The memory is automatically freed when the program ends. * - * @param directory The path to the directory with locale JSON files. - * - * Example: - * cHTTPX_i18n("public"); + * @param directory Path to the directory with locale JSON files. */ void cHTTPX_i18n(const char* directory) { @@ -230,13 +237,9 @@ void cHTTPX_i18n(const char* directory) * The function does not allocate memory — the returned string * belongs to the i18n manager. * - * @param key Translation key (for example: "welcome"). + * @param key Translation key (for example: "welcome"). * @param lang Language code ("en", "ru", NULL for default). - * - * @return The translation string or key if the translation is not found. - * - * Example: - * const char* text = cHTTPX_i18n_t("welcome", "ru"); + * @return Translation string, or key if not found. */ const char* cHTTPX_i18n_t(const char* key, const char* lang) { @@ -268,6 +271,15 @@ const char* cHTTPX_i18n_t(const char* key, const char* lang) return key; } +/** + * Configure the language preference list used for request negotiation. + * + * @param server Initialized HTTP server. + * @param languages Ordered array of supported language codes. + * @param count Number of elements in languages. + * @param fallback Fallback language code. + * @return cHTTPX_OK on success, otherwise a negative error code. + */ int cHTTPX_i18n_languages(chttpx_serv_t* server, const char** languages, size_t count, const char* fallback) { if (!server || !server->initialized) @@ -282,6 +294,7 @@ const char* LANGUAGE_CODES[LANG_COUNT] = { "fr" // LANG_FR }; +/** @copydoc i18n_lang_from_string */ i18n_language_t i18n_lang_from_string(const char* code) { if (!code) diff --git a/src/cHTTPX_i18n.h b/src/cHTTPX_i18n.h index d3ba89b..6783da2 100644 --- a/src/cHTTPX_i18n.h +++ b/src/cHTTPX_i18n.h @@ -20,12 +20,14 @@ extern "C" struct chttpx_serv; + /** One key/value translation entry loaded from a locale file. */ typedef struct { char* key; char* value; } i18n_entry_t; + /** Translations for a single locale (e.g. "en", "ru"). */ typedef struct { /* Language en, ru, es */ @@ -36,6 +38,7 @@ extern "C" size_t count; } i18n_locale_t; + /** In-memory table of loaded locale files. */ typedef struct { i18n_locale_t locales[MAX_LOCALES]; @@ -43,6 +46,7 @@ extern "C" i18n_locale_t* default_locale; } i18n_manager_t; + /** Supported language codes for server negotiation. */ typedef enum { LANG_EN, @@ -52,6 +56,12 @@ extern "C" LANG_COUNT } i18n_language_t; + /** + * Map a language code string to i18n_language_t. + * + * @param code Language code (e.g. "en"); NULL defaults to LANG_EN. + * @return Matching language enum, or LANG_EN if unknown. + */ i18n_language_t i18n_lang_from_string(const char* code); /** @@ -59,50 +69,34 @@ extern "C" * * Loads all locale JSON files from the specified directory. * The file name determines the locale language: - * en.json -> "en" - * ru.json -> "ru" - * fr.json -> "fr" - * - * All translations are stored globally in memory and are used by the cHTTPX_i18n_t() function. + * en.json -> "en", ru.json -> "ru", fr.json -> "fr". * - * The memory is automatically freed when the program ends. + * Memory is freed automatically at program exit. * - * @param directory The path to the directory with locale JSON files. - * - * Example: - * cHTTPX_i18n("public"); + * @param directory Path to the directory with locale JSON files. */ void cHTTPX_i18n(const char* directory); /** * Returns a translation by key and language. * - * Searches for a translation by key in the specified locale. - * If the language is not found, the default locale is used. - * If the key is not found, the key itself is returned. - * - * The function does not allocate memory — the returned string - * belongs to the i18n manager. + * Uses the default locale when lang is NULL or unknown; returns key if not found. + * The returned string belongs to the i18n manager (not copied per call). * - * @param key Translation key (for example: "welcome"). + * @param key Translation key (for example: "welcome"). * @param lang Language code ("en", "ru", NULL for default). - * - * @return The translation string or key if the translation is not found. - * - * Example: - * const char* text = cHTTPX_i18n_t("welcome", "ru"); + * @return Translation string, or key if not found. */ const char* cHTTPX_i18n_t(const char* key, const char* lang); /** * Configure the language preference list used for request negotiation. * - * @param languages Ordered array of supported language - * codes. - * @param count Number of elements in languages. - * @param fallback Fallback language code. - * @return cHTTPX_OK on - * success, otherwise a negative error code. + * @param server Initialized HTTP server. + * @param languages Ordered array of supported language codes. + * @param count Number of elements in languages. + * @param fallback Fallback language code. + * @return cHTTPX_OK on success, otherwise a negative error code. */ int cHTTPX_i18n_languages(struct chttpx_serv* server, const char** languages, size_t count, const char* fallback); diff --git a/src/cHTTPX_json.c b/src/cHTTPX_json.c index 877b481..1a48676 100644 --- a/src/cHTTPX_json.c +++ b/src/cHTTPX_json.c @@ -33,6 +33,7 @@ #include #include +/** JSON builder backing store (cJSON node plus request arena ownership). */ struct chttpx_json { chttpx_request_t* req; @@ -40,6 +41,13 @@ struct chttpx_json bool array; }; +/** + * Escape a string for JSON output. + * + * @param req Request arena that owns the returned string. + * @param value Raw string to escape. + * @return Escaped string, or NULL on error. + */ char* cHTTPX_JsonEscape(chttpx_request_t* req, const char* value) { if (!req || !value) @@ -85,6 +93,13 @@ char* cHTTPX_JsonEscape(chttpx_request_t* req, const char* value) return escaped; } +/** + * Allocate a JSON object or array builder on the request arena. + * + * @param req Request arena for allocation and cleanup. + * @param array True to create an array, false for an object. + * @return JSON builder, or NULL on error. + */ static chttpx_json_t* json_create(chttpx_request_t* req, bool array) { if (!req) @@ -104,16 +119,26 @@ static chttpx_json_t* json_create(chttpx_request_t* req, bool array) return json; } +/** @copydoc cHTTPX_JsonObject */ chttpx_json_t* cHTTPX_JsonObject(chttpx_request_t* req) { return json_create(req, false); } +/** @copydoc cHTTPX_JsonArray */ chttpx_json_t* cHTTPX_JsonArray(chttpx_request_t* req) { return json_create(req, true); } +/** + * Attach a cJSON node to a JSON object builder. + * + * @param json Target object builder. + * @param key Property name. + * @param value cJSON node (freed on error). + * @return 0 on success, -1 on error. + */ static int object_add(chttpx_json_t* json, const char* key, cJSON* value) { if (!json || json->array || !key || !value) @@ -125,6 +150,13 @@ static int object_add(chttpx_json_t* json, const char* key, cJSON* value) return 0; } +/** + * Append a cJSON node to a JSON array builder. + * + * @param json Target array builder. + * @param value cJSON node (freed on error). + * @return 0 on success, -1 on error. + */ static int array_add(chttpx_json_t* json, cJSON* value) { if (!json || !json->array || !value) @@ -136,26 +168,31 @@ static int array_add(chttpx_json_t* json, cJSON* value) return 0; } +/** @copydoc cHTTPX_JsonString */ int cHTTPX_JsonString(chttpx_json_t* json, const char* key, const char* value) { return object_add(json, key, cJSON_CreateString(value ? value : "")); } +/** @copydoc cHTTPX_JsonNumber */ int cHTTPX_JsonNumber(chttpx_json_t* json, const char* key, double value) { return object_add(json, key, cJSON_CreateNumber(value)); } +/** @copydoc cHTTPX_JsonBool */ int cHTTPX_JsonBool(chttpx_json_t* json, const char* key, bool value) { return object_add(json, key, cJSON_CreateBool(value)); } +/** @copydoc cHTTPX_JsonNull */ int cHTTPX_JsonNull(chttpx_json_t* json, const char* key) { return object_add(json, key, cJSON_CreateNull()); } +/** @copydoc cHTTPX_JsonChild */ int cHTTPX_JsonChild(chttpx_json_t* json, const char* key, chttpx_json_t* child) { if (!json || !child || json->req != child->req || !cHTTPX_Detach(child->req, child->value)) @@ -163,26 +200,31 @@ int cHTTPX_JsonChild(chttpx_json_t* json, const char* key, chttpx_json_t* child) return object_add(json, key, child->value); } +/** @copydoc cHTTPX_JsonArrayString */ int cHTTPX_JsonArrayString(chttpx_json_t* json, const char* value) { return array_add(json, cJSON_CreateString(value ? value : "")); } +/** @copydoc cHTTPX_JsonArrayNumber */ int cHTTPX_JsonArrayNumber(chttpx_json_t* json, double value) { return array_add(json, cJSON_CreateNumber(value)); } +/** @copydoc cHTTPX_JsonArrayBool */ int cHTTPX_JsonArrayBool(chttpx_json_t* json, bool value) { return array_add(json, cJSON_CreateBool(value)); } +/** @copydoc cHTTPX_JsonArrayNull */ int cHTTPX_JsonArrayNull(chttpx_json_t* json) { return array_add(json, cJSON_CreateNull()); } +/** @copydoc cHTTPX_JsonArrayChild */ int cHTTPX_JsonArrayChild(chttpx_json_t* json, chttpx_json_t* child) { if (!json || !child || json->req != child->req || !cHTTPX_Detach(child->req, child->value)) @@ -190,6 +232,7 @@ int cHTTPX_JsonArrayChild(chttpx_json_t* json, chttpx_json_t* child) return array_add(json, child->value); } +/** @copydoc cHTTPX_ResJsonObject */ chttpx_response_t cHTTPX_ResJsonObject(uint16_t status, chttpx_json_t* json) { if (!json || !json->value) @@ -202,6 +245,14 @@ chttpx_response_t cHTTPX_ResJsonObject(uint16_t status, chttpx_json_t* json) return response; } +/** + * Build a JSON object response with one string field. + * + * @param status HTTP status code. + * @param name JSON property name. + * @param message Property value. + * @return JSON HTTP response. + */ static chttpx_response_t json_named_response(uint16_t status, const char* name, const char* message) { cJSON* object = cJSON_CreateObject(); @@ -217,11 +268,25 @@ static chttpx_response_t json_named_response(uint16_t status, const char* name, return response; } +/** + * JSON error response with an "error" string field. + * + * @param status HTTP status code. + * @param message Error message text. + * @return JSON HTTP response. + */ chttpx_response_t cHTTPX_ResError(uint16_t status, const char* message) { return json_named_response(status, "error", message); } +/** + * JSON response with a "message" string field. + * + * @param status HTTP status code. + * @param message Message text. + * @return JSON HTTP response. + */ chttpx_response_t cHTTPX_ResMessage(uint16_t status, const char* message) { return json_named_response(status, "message", message); diff --git a/src/cHTTPX_json.h b/src/cHTTPX_json.h index a3342b9..e00b716 100644 --- a/src/cHTTPX_json.h +++ b/src/cHTTPX_json.h @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2026 netcorelink + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `libchttpx.c` for details. + */ + #ifndef JSON_H #define JSON_H @@ -9,48 +16,134 @@ extern "C" #include "cHTTPX_request.h" #include "cHTTPX_response.h" + /** Opaque JSON builder (object or array) tied to a request arena. */ typedef struct chttpx_json chttpx_json_t; - /** Escape a string for JSON. The returned string is owned by the request arena. */ + /** + * Escape a string for JSON output. + * + * @param req Request arena that owns the returned string. + * @param value Raw string to escape. + * @return Escaped string, or NULL on error. + */ char* cHTTPX_JsonEscape(chttpx_request_t* req, const char* value); - /** Create an empty JSON object owned by the request arena. */ + /** + * Create an empty JSON object owned by the request arena. + * + * @param req Request arena for allocation and cleanup. + * @return JSON builder, or NULL on error. + */ chttpx_json_t* cHTTPX_JsonObject(chttpx_request_t* req); - /** Create an empty JSON array owned by the request arena. */ + /** + * Create an empty JSON array owned by the request arena. + * + * @param req Request arena for allocation and cleanup. + * @return JSON builder, or NULL on error. + */ chttpx_json_t* cHTTPX_JsonArray(chttpx_request_t* req); - /** Add a string property to a JSON object. */ + /** + * Add a string property to a JSON object. + * + * @param json Target object builder. + * @param key Property name. + * @param value String value (NULL becomes empty string). + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonString(chttpx_json_t* json, const char* key, const char* value); - /** Add a numeric property to a JSON object. */ + /** + * Add a numeric property to a JSON object. + * + * @param json Target object builder. + * @param key Property name. + * @param value Numeric value. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonNumber(chttpx_json_t* json, const char* key, double value); - /** Add a boolean property to a JSON object. */ + /** + * Add a boolean property to a JSON object. + * + * @param json Target object builder. + * @param key Property name. + * @param value Boolean value. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonBool(chttpx_json_t* json, const char* key, bool value); - /** Add a null property to a JSON object. */ + /** + * Add a null property to a JSON object. + * + * @param json Target object builder. + * @param key Property name. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonNull(chttpx_json_t* json, const char* key); - /** Add a nested object or array to a JSON object. */ + /** + * Add a nested object or array to a JSON object. + * + * @param json Target object builder. + * @param key Property name. + * @param child Nested builder (ownership transfers to parent). + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonChild(chttpx_json_t* json, const char* key, chttpx_json_t* child); - /** Append a string value to a JSON array. */ + /** + * Append a string value to a JSON array. + * + * @param json Target array builder. + * @param value String value (NULL becomes empty string). + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonArrayString(chttpx_json_t* json, const char* value); - /** Append a numeric value to a JSON array. */ + /** + * Append a numeric value to a JSON array. + * + * @param json Target array builder. + * @param value Numeric value. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonArrayNumber(chttpx_json_t* json, double value); - /** Append a boolean value to a JSON array. */ + /** + * Append a boolean value to a JSON array. + * + * @param json Target array builder. + * @param value Boolean value. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonArrayBool(chttpx_json_t* json, bool value); - /** Append a null value to a JSON array. */ + /** + * Append a null value to a JSON array. + * + * @param json Target array builder. + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonArrayNull(chttpx_json_t* json); - /** Append a nested object or array to a JSON array. */ + /** + * Append a nested object or array to a JSON array. + * + * @param json Target array builder. + * @param child Nested builder (ownership transfers to parent). + * @return 0 on success, -1 on error. + */ int cHTTPX_JsonArrayChild(chttpx_json_t* json, chttpx_json_t* child); - /** Serialize a JSON builder into an application/json HTTP response. */ + /** + * Serialize a JSON builder into an application/json HTTP response. + * + * @param status HTTP status code. + * @param json Builder to serialize. + * @return Response with JSON body. + */ chttpx_response_t cHTTPX_ResJsonObject(uint16_t status, chttpx_json_t* json); #ifdef __cplusplus diff --git a/src/cHTTPX_media.c b/src/cHTTPX_media.c index 0ab04e4..bba8861 100644 --- a/src/cHTTPX_media.c +++ b/src/cHTTPX_media.c @@ -40,6 +40,17 @@ #define MULTIPART_FORM_VALUE_LIMIT (1024 * 1024) #define MULTIPART_MAX_PARTS 256 +/** + * Append part value. + * + * @param data Parameter `data`. + * @param size Parameter `size`. + * @param capacity Parameter `capacity`. + * @param bytes Parameter `bytes`. + * @param count Parameter `count`. + * @param limit Parameter `limit`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int append_part_value(unsigned char** data, size_t* size, size_t* capacity, const unsigned char* bytes, size_t count, size_t limit) { if (*size > limit || count > limit - *size || *size == SIZE_MAX || count > SIZE_MAX - *size - 1) @@ -71,6 +82,11 @@ static int append_part_value(unsigned char** data, size_t* size, size_t* capacit return 1; } +/** + * Remove temporary file. + * + * @param resource Parameter `resource`. + */ static void remove_temporary_file(void* resource) { char* path = resource; @@ -81,6 +97,13 @@ static void remove_temporary_file(void* resource) } } +/** + * Create temporary file. + * + * @param path Parameter `path`. + * @param path_size Parameter `path_size`. + * @return Open temporary file or NULL on failure. + */ static FILE* create_temporary_file(char* path, size_t path_size) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -106,8 +129,18 @@ static FILE* create_temporary_file(char* path, size_t path_size) #endif } -static int track_file(chttpx_request_t* req, const char* path, const char* field_name, const char* original_name, const char* content_type, - size_t size) +/** + * Track file. + * + * @param req Current HTTP request. + * @param path Parameter `path`. + * @param field_name Parameter `field_name`. + * @param original_name Parameter `original_name`. + * @param content_type Parameter `content_type`. + * @param size Parameter `size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int track_file(chttpx_request_t* req, const char* path, const char* field_name, const char* original_name, const char* content_type, size_t size) { chttpx_file_t* files = cHTTPX_Alloc(req, sizeof(*files) * (req->files_count + 1)); if (!files) @@ -139,6 +172,17 @@ static int track_file(chttpx_request_t* req, const char* path, const char* field return file->field_name && file->original_name && file->content_type; } +/** + * Add form value. + * + * @param req Current HTTP request. + * @param name Parameter `name`. + * @param name_size Parameter `name_size`. + * @param value Parameter `value`. + * @param value_size Parameter `value_size`. + * @param decode Parameter `decode`. + * @return true on success, false otherwise. + */ static int add_form_value(chttpx_request_t* req, const char* name, size_t name_size, const char* value, size_t value_size, bool decode) { chttpx_query_t* values = cHTTPX_Alloc(req, sizeof(*values) * (req->form_values_count + 1)); @@ -177,6 +221,11 @@ static int add_form_value(chttpx_request_t* req, const char* name, size_t name_s return 1; } +/** + * Parse urlencoded. + * + * @param req Current HTTP request. + */ static void parse_urlencoded(chttpx_request_t* req) { const char* cursor = (const char*)req->body; @@ -195,6 +244,16 @@ static void parse_urlencoded(chttpx_request_t* req) } } +/** + * Header attribute. + * + * @param headers Parameter `headers`. + * @param headers_size Parameter `headers_size`. + * @param attribute Parameter `attribute`. + * @param output Parameter `output`. + * @param output_size Parameter `output_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int header_attribute(const char* headers, size_t headers_size, const char* attribute, char* output, size_t output_size) { const char* found = memmem_case(headers, headers_size, attribute, strlen(attribute)); @@ -209,6 +268,14 @@ static int header_attribute(const char* headers, size_t headers_size, const char return 1; } +/** + * Multipart boundary. + * + * @param req Current HTTP request. + * @param boundary Parameter `boundary`. + * @param boundary_size Parameter `boundary_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int multipart_boundary(const chttpx_request_t* req, char* boundary, size_t boundary_size) { if (!req) @@ -251,8 +318,20 @@ static int multipart_boundary(const chttpx_request_t* req, char* boundary, size_ return 0; } -static void parse_part_headers(const char* headers, size_t headers_size, char* name, size_t name_size, char* filename, size_t filename_size, - int* has_filename, char* content_type, size_t content_type_size) +/** + * Parse part headers. + * + * @param headers Parameter `headers`. + * @param headers_size Parameter `headers_size`. + * @param name Parameter `name`. + * @param name_size Parameter `name_size`. + * @param filename Parameter `filename`. + * @param filename_size Parameter `filename_size`. + * @param has_filename Parameter `has_filename`. + * @param content_type Parameter `content_type`. + * @param content_type_size Parameter `content_type_size`. + */ +static void parse_part_headers(const char* headers, size_t headers_size, char* name, size_t name_size, char* filename, size_t filename_size, int* has_filename, char* content_type, size_t content_type_size) { name[0] = '\0'; filename[0] = '\0'; @@ -277,6 +356,11 @@ static void parse_part_headers(const char* headers, size_t headers_size, char* n content_type[size] = '\0'; } +/** + * Parse multipart buffered. + * + * @param req Current HTTP request. + */ static void parse_multipart_buffered(chttpx_request_t* req) { char boundary[204]; @@ -319,8 +403,7 @@ static void parse_multipart_buffered(chttpx_request_t* req) size_t data_size = next_offset - 2; char name[256], filename[512], content_type[512]; int has_filename = 0; - parse_part_headers((const char*)part, headers_size, name, sizeof(name), filename, sizeof(filename), &has_filename, content_type, - sizeof(content_type)); + parse_part_headers((const char*)part, headers_size, name, sizeof(name), filename, sizeof(filename), &has_filename, content_type, sizeof(content_type)); if (!name[0]) goto bad_request; @@ -356,6 +439,14 @@ static void parse_multipart_buffered(chttpx_request_t* req) req->_parse_status = cHTTPX_StatusBadRequest; } +/** + * Read line. + * + * @param stream Parameter `stream`. + * @param line Parameter `line`. + * @param capacity Parameter `capacity`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int read_line(FILE* stream, char* line, size_t capacity) { size_t size = 0; @@ -374,6 +465,15 @@ static int read_line(FILE* stream, char* line, size_t capacity) return 1; } +/** + * Read stream headers. + * + * @param stream Parameter `stream`. + * @param headers Parameter `headers`. + * @param capacity Parameter `capacity`. + * @param headers_size Parameter `headers_size`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int read_stream_headers(FILE* stream, char* headers, size_t capacity, size_t* headers_size) { *headers_size = 0; @@ -394,8 +494,19 @@ static int read_stream_headers(FILE* stream, char* headers, size_t capacity, siz } } -static int multipart_copy_part(FILE* stream, const char* boundary, FILE* output, unsigned char** value, size_t* value_size, size_t value_limit, - int* final_boundary) +/** + * Multipart copy part. + * + * @param stream Parameter `stream`. + * @param boundary Parameter `boundary`. + * @param output Parameter `output`. + * @param value Parameter `value`. + * @param value_size Parameter `value_size`. + * @param value_limit Parameter `value_limit`. + * @param final_boundary Parameter `final_boundary`. + * @return Open temporary file or NULL on failure. + */ +static int multipart_copy_part(FILE* stream, const char* boundary, FILE* output, unsigned char** value, size_t* value_size, size_t value_limit, int* final_boundary) { char next_marker[256]; char final_marker[256]; @@ -499,6 +610,12 @@ static int multipart_copy_part(FILE* stream, const char* boundary, FILE* output, return ok; } +/** + * Parse multipart stream. + * + * @param req Current HTTP request. + * @param stream Parameter `stream`. + */ static void parse_multipart_stream(chttpx_request_t* req, FILE* stream) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -531,8 +648,7 @@ static void parse_multipart_stream(chttpx_request_t* req, FILE* stream) char name[256], filename[512], content_type[512]; int has_filename = 0; - parse_part_headers(headers, headers_size, name, sizeof(name), filename, sizeof(filename), &has_filename, content_type, - sizeof(content_type)); + parse_part_headers(headers, headers_size, name, sizeof(name), filename, sizeof(filename), &has_filename, content_type, sizeof(content_type)); if (!name[0]) goto bad_request; @@ -601,6 +717,12 @@ static void parse_multipart_stream(chttpx_request_t* req, FILE* stream) req->_parse_status = cHTTPX_StatusInternalServerError; } +/** + * Save raw upload stream. + * + * @param req Current HTTP request. + * @param stream Parameter `stream`. + */ static void save_raw_upload_stream(chttpx_request_t* req, FILE* stream) { if (!req || !stream || fseek(stream, 0, SEEK_SET) != 0) @@ -647,6 +769,13 @@ static void save_raw_upload_stream(chttpx_request_t* req, FILE* stream) req->_parse_status = cHTTPX_StatusInternalServerError; } +/** + * Save raw upload. + * + * @param req Current HTTP request. + * @param initial_buffer Parameter `initial_buffer`. + * @param initial_len Parameter `initial_len`. + */ static void save_raw_upload(chttpx_request_t* req, char* initial_buffer, size_t initial_len) { char path[512]; @@ -697,6 +826,13 @@ static void save_raw_upload(chttpx_request_t* req, char* initial_buffer, size_t req->_parse_status = cHTTPX_StatusBadRequest; } +/** + * Parse media. + * + * @param req Current HTTP request. + * @param buffer Parameter `buffer`. + * @param buffer_len Parameter `buffer_len`. + */ void _parse_media(chttpx_request_t* req, char* buffer, size_t buffer_len) { if (!req || !req->content_type[0] || req->_parse_status) @@ -738,11 +874,13 @@ void _parse_media(chttpx_request_t* req, char* buffer, size_t buffer_len) } } +/** @copydoc cHTTPX_RequestFile */ const chttpx_file_t* cHTTPX_RequestFile(chttpx_request_t* req) { return req && req->files_count ? &req->files[0] : NULL; } +/** @copydoc cHTTPX_FormFile */ const chttpx_file_t* cHTTPX_FormFile(chttpx_request_t* req, const char* name) { if (!req || !name) @@ -755,6 +893,7 @@ const chttpx_file_t* cHTTPX_FormFile(chttpx_request_t* req, const char* name) return NULL; } +/** @copydoc cHTTPX_FormValue */ const char* cHTTPX_FormValue(chttpx_request_t* req, const char* name) { if (!req || !name) @@ -767,6 +906,7 @@ const char* cHTTPX_FormValue(chttpx_request_t* req, const char* name) return NULL; } +/** @copydoc cHTTPX_FileDetach */ int cHTTPX_FileDetach(chttpx_request_t* req, const chttpx_file_t* file) { if (!req || !file || !file->path || !cHTTPX_Detach(req, (void*)file->path)) @@ -791,12 +931,14 @@ int cHTTPX_FileDetach(chttpx_request_t* req, const chttpx_file_t* file) return 1; } +/** @copydoc cHTTPX_FileKeep */ int cHTTPX_FileKeep(chttpx_request_t* req) { const chttpx_file_t* file = cHTTPX_RequestFile(req); return file ? cHTTPX_FileDetach(req, file) : 0; } +/** @copydoc cHTTPX_MimeMatch */ bool cHTTPX_MimeMatch(const char* mime, const char* pattern) { if (!mime || !pattern) @@ -809,16 +951,19 @@ bool cHTTPX_MimeMatch(const char* mime, const char* pattern) return mime_size == pattern_size && strncasecmp(mime, pattern, mime_size) == 0; } +/** @copydoc cHTTPX_MimeIsImage */ bool cHTTPX_MimeIsImage(const char* mime) { return cHTTPX_MimeMatch(mime, "image/*"); } +/** @copydoc cHTTPX_MimeIsVideo */ bool cHTTPX_MimeIsVideo(const char* mime) { return cHTTPX_MimeMatch(mime, "video/*"); } +/** @copydoc cHTTPX_MimeIsAudio */ bool cHTTPX_MimeIsAudio(const char* mime) { return cHTTPX_MimeMatch(mime, "audio/*"); diff --git a/src/cHTTPX_media.h b/src/cHTTPX_media.h index 1a394aa..4283dd8 100644 --- a/src/cHTTPX_media.h +++ b/src/cHTTPX_media.h @@ -18,13 +18,20 @@ extern "C" #define FILE_BUFFER 65536 + /** Maps a file extension to a MIME content type string. */ typedef struct { const char* ctype; const char* ext; } content_type_map_t; - /* Parse media in request */ + /** + * Parse uploaded media, multipart form data, or URL-encoded fields. + * + * @param req Current HTTP request with body or spool stream already parsed. + * @param buffer Initial receive buffer (used for streaming raw uploads). + * @param buffer_len Size of buffer in bytes. + */ void _parse_media(chttpx_request_t* req, char* buffer, size_t buffer_len); /** Return the first uploaded file, or NULL when the request has no file. */ diff --git a/src/cHTTPX_metrics.c b/src/cHTTPX_metrics.c index 885df3d..6f47974 100644 --- a/src/cHTTPX_metrics.c +++ b/src/cHTTPX_metrics.c @@ -22,6 +22,7 @@ #include #endif +/** Per-route request and status-class counters. */ typedef struct { const char* method; @@ -30,6 +31,7 @@ typedef struct uint64_t status_class[5]; } chttpx_route_metrics_entry_t; +/** Internal metrics state (atomics, route table, mutex). */ typedef struct { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -250,9 +252,15 @@ static int route_metrics_rehash(chttpx_metrics_state_t* state, size_t capacity) * @param route_template Registered route template. * @return Route metrics entry or NULL on allocation failure. */ -static chttpx_route_metrics_entry_t* route_metrics_entry(chttpx_metrics_state_t* state, - const char* method, - const char* route_template) +/** + * Route metrics entry. + * + * @param state Parameter `state`. + * @param method Parameter `method`. + * @param route_template Parameter `route_template`. + * @return Pointer or NULL on failure. + */ +static chttpx_route_metrics_entry_t* route_metrics_entry(chttpx_metrics_state_t* state, const char* method, const char* route_template) { if (!state || !method || !route_template) return NULL; @@ -460,12 +468,17 @@ void _chttpx_metrics_request_begin(chttpx_serv_t* server, size_t request_bytes) * @param response_bytes Response body size in bytes. * @param duration_seconds Request duration in seconds. */ -void _chttpx_metrics_request_end(chttpx_serv_t* server, - const char* method, - const char* route_template, - int status, - size_t response_bytes, - double duration_seconds) +/** + * Metrics request end. + * + * @param server HTTP server instance. + * @param method Parameter `method`. + * @param route_template Parameter `route_template`. + * @param status Parameter `status`. + * @param response_bytes Parameter `response_bytes`. + * @param duration_seconds Parameter `duration_seconds`. + */ +void _chttpx_metrics_request_end(chttpx_serv_t* server, const char* method, const char* route_template, int status, size_t response_bytes, double duration_seconds) { chttpx_metrics_state_t* state = metrics_state(server); if (!state) @@ -595,6 +608,13 @@ typedef struct size_t capacity; } prometheus_buffer_t; +/** + * Prometheus reserve. + * + * @param buffer Parameter `buffer`. + * @param extra Parameter `extra`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int prometheus_reserve(prometheus_buffer_t* buffer, size_t extra) { if (!buffer || extra > SIZE_MAX - buffer->length - 1) @@ -624,6 +644,13 @@ static int prometheus_reserve(prometheus_buffer_t* buffer, size_t extra) return 1; } +/** + * Prometheus append. + * + * @param buffer Parameter `buffer`. + * @param text Parameter `text`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int prometheus_append(prometheus_buffer_t* buffer, const char* text) { size_t length = strlen(text); @@ -635,6 +662,13 @@ static int prometheus_append(prometheus_buffer_t* buffer, const char* text) return 1; } +/** + * Prometheus appendf. + * + * @param buffer Parameter `buffer`. + * @param format Parameter `format`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int prometheus_appendf(prometheus_buffer_t* buffer, const char* format, ...) { va_list args; @@ -664,6 +698,13 @@ static int prometheus_appendf(prometheus_buffer_t* buffer, const char* format, . return 1; } +/** + * Prometheus append label value. + * + * @param buffer Parameter `buffer`. + * @param value Parameter `value`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int prometheus_append_label_value(prometheus_buffer_t* buffer, const char* value) { if (!value) @@ -698,8 +739,14 @@ static int prometheus_append_label_value(prometheus_buffer_t* buffer, const char return 1; } -static int prometheus_global_metrics(prometheus_buffer_t* buffer, - const chttpx_metrics_t* metrics) +/** + * Prometheus global metrics. + * + * @param buffer Parameter `buffer`. + * @param metrics Parameter `metrics`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int prometheus_global_metrics(prometheus_buffer_t* buffer, const chttpx_metrics_t* metrics) { static const char* bucket_labels[] = { "0.001", @@ -777,8 +824,14 @@ static int prometheus_global_metrics(prometheus_buffer_t* buffer, (unsigned long long)metrics->request_duration_count); } -static int prometheus_runtime_metrics(prometheus_buffer_t* buffer, - const chttpx_runtime_metrics_t* metrics) +/** + * Prometheus runtime metrics. + * + * @param buffer Parameter `buffer`. + * @param metrics Parameter `metrics`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int prometheus_runtime_metrics(prometheus_buffer_t* buffer, const chttpx_runtime_metrics_t* metrics) { if (!metrics) return 1; @@ -802,8 +855,14 @@ static int prometheus_runtime_metrics(prometheus_buffer_t* buffer, (double)metrics->queue_wait_nanoseconds_total / 1000000000.0); } -static int prometheus_route_metrics(prometheus_buffer_t* buffer, - chttpx_metrics_state_t* state) +/** + * Prometheus route metrics. + * + * @param buffer Parameter `buffer`. + * @param state Parameter `state`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ +static int prometheus_route_metrics(prometheus_buffer_t* buffer, chttpx_metrics_state_t* state) { if (!prometheus_append(buffer, "# HELP libchttpx_route_requests_total HTTP requests grouped by registered route template, method and status class.\n" @@ -836,6 +895,12 @@ static int prometheus_route_metrics(prometheus_buffer_t* buffer, return 1; } +/** + * Metrics handler. + * + * @param req Current HTTP request. + * @param response HTTP response. + */ static void metrics_handler(chttpx_request_t* req, chttpx_response_t* res) { chttpx_serv_t* server = req ? req->_server : NULL; diff --git a/src/cHTTPX_metrics.h b/src/cHTTPX_metrics.h index 8c26d4a..fe8cf70 100644 --- a/src/cHTTPX_metrics.h +++ b/src/cHTTPX_metrics.h @@ -115,12 +115,7 @@ extern "C" void _chttpx_metrics_connection_opened(struct chttpx_serv* server); void _chttpx_metrics_connection_closed(struct chttpx_serv* server); void _chttpx_metrics_request_begin(struct chttpx_serv* server, size_t request_bytes); - void _chttpx_metrics_request_end(struct chttpx_serv* server, - const char* method, - const char* route_template, - int status, - size_t response_bytes, - double duration_seconds); + void _chttpx_metrics_request_end(struct chttpx_serv* server, const char* method, const char* route_template, int status, size_t response_bytes, double duration_seconds); void _chttpx_metrics_parser_failure(struct chttpx_serv* server); void _chttpx_metrics_timeout_failure(struct chttpx_serv* server); void _chttpx_metrics_rate_limit_failure(struct chttpx_serv* server); diff --git a/src/cHTTPX_middlewares.c b/src/cHTTPX_middlewares.c index c8f93c1..4582fcd 100644 --- a/src/cHTTPX_middlewares.c +++ b/src/cHTTPX_middlewares.c @@ -23,6 +23,7 @@ #include #include +/** Server-owned rate limiter hash table and synchronization. */ typedef struct { rate_limiter_entry_t entries[MAX_MIDDLEWARE_RATE_LIMIT_TABLE_SIZE]; @@ -36,6 +37,13 @@ typedef struct #endif } chttpx_rate_limiter_state_t; +/** + * Middleware registered. + * + * @param stack Parameter `stack`. + * @param middleware Parameter `middleware`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int middleware_registered(const chttpx_middleware_stack_t* stack, chttpx_middleware_t middleware) { if (!stack || !middleware) @@ -67,6 +75,11 @@ void cHTTPX_MiddlewareUseAfter(chttpx_serv_t* server, chttpx_middleware_t middle server->middleware.after_middlewares[server->middleware.after_middleware_count++] = middleware; } +/** + * Rate limiter hash. + * + * @param ip Parameter `ip`. + */ static uint32_t rate_limiter_hash(const char* ip) { uint64_t hash = 5381; @@ -78,6 +91,11 @@ static uint32_t rate_limiter_hash(const char* ip) return (uint32_t)(hash % MAX_MIDDLEWARE_RATE_LIMIT_TABLE_SIZE); } +/** + * Rate limiter lock. + * + * @param state Parameter `state`. + */ static void rate_limiter_lock(chttpx_rate_limiter_state_t* state) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -87,6 +105,11 @@ static void rate_limiter_lock(chttpx_rate_limiter_state_t* state) #endif } +/** + * Rate limiter unlock. + * + * @param state Parameter `state`. + */ static void rate_limiter_unlock(chttpx_rate_limiter_state_t* state) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -96,6 +119,13 @@ static void rate_limiter_unlock(chttpx_rate_limiter_state_t* state) #endif } +/** + * Rate limiter middleware. + * + * @param req Current HTTP request. + * @param response HTTP response. + * @return Middleware chain result (out or next). + */ static chttpx_middleware_result_t rate_limiter_middleware(chttpx_request_t* req, chttpx_response_t* res) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -167,10 +197,21 @@ void cHTTPX_MiddlewareRateLimiter(chttpx_serv_t* server, uint32_t max_requests, cHTTPX_MiddlewareUse(server, rate_limiter_middleware); } +/** + * Recovery init. + * + */ void _recovery_init(void) { } +/** + * Recovery middleware. + * + * @param req Current HTTP request. + * @param response HTTP response. + * @return Middleware chain result (out or next). + */ static chttpx_middleware_result_t recovery_middleware(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -183,6 +224,13 @@ void cHTTPX_MiddlewareRecovery(chttpx_serv_t* server) cHTTPX_MiddlewareUse(server, recovery_middleware); } +/** + * Diff ms. + * + * @param a Parameter `a`. + * @param b Parameter `b`. + * @return Computed value. + */ static double diff_ms(struct timespec a, struct timespec b) { return (b.tv_sec - a.tv_sec) * 1000.0 + (b.tv_nsec - a.tv_nsec) / 1e6; @@ -196,8 +244,7 @@ void postmiddleware_logging_write(chttpx_request_t* req, chttpx_response_t* res) double ms = diff_ms(res->start_ts, res->end_ts); char message[2048]; - snprintf(message, sizeof(message), "%s \"%s %s %s\" %d %zu \"%s\" %.4fms", req->client_ip, req->method ? req->method : "", - req->path ? req->path : "", req->protocol, res->status, res->body_size, req->user_agent, ms); + snprintf(message, sizeof(message), "%s \"%s %s %s\" %d %zu \"%s\" %.4fms", req->client_ip, req->method ? req->method : "", req->path ? req->path : "", req->protocol, res->status, res->body_size, req->user_agent, ms); if (server->logger && server->log_level <= cHTTPX_LOG_INFO) server->logger(cHTTPX_LOG_INFO, req->request_id, message, server->logger_data); @@ -209,6 +256,11 @@ void cHTTPX_MiddlewareLogging(chttpx_serv_t* server) server->logging_enabled = true; } +/** + * Middleware server cleanup. + * + * @param server HTTP server instance. + */ void _chttpx_middleware_server_cleanup(chttpx_serv_t* server) { if (!server || !server->rate_limiter_state) diff --git a/src/cHTTPX_middlewares.h b/src/cHTTPX_middlewares.h index f9b8e48..3f52707 100644 --- a/src/cHTTPX_middlewares.h +++ b/src/cHTTPX_middlewares.h @@ -23,16 +23,17 @@ extern "C" struct chttpx_serv; - /* Enum for result all middlewares */ + /** Middleware chain control: stop (out) or continue (next). */ typedef enum { out = 0, next = 1, } chttpx_middleware_result_t; + /** Middleware function invoked before or after the route handler. */ typedef chttpx_middleware_result_t (*chttpx_middleware_t)(chttpx_request_t* req, chttpx_response_t* res); - /* Struct for base middlewares */ + /** Registered before/after middleware stacks for one server. */ typedef struct { chttpx_middleware_t middlewares[MAX_MIDDLEWARES]; @@ -62,7 +63,7 @@ extern "C" #define MAX_MIDDLEWARE_RATE_LIMIT_TABLE_SIZE 4096 - /* Struct for middleware [RATE LIMITER]*/ + /** Per-IP sliding window counter for the rate limiter middleware. */ typedef struct { /* Start limit window time*/ @@ -120,7 +121,6 @@ extern "C" void _chttpx_middleware_server_cleanup(struct chttpx_serv* server); #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_params.c b/src/cHTTPX_params.c index d7bf700..ce81278 100644 --- a/src/cHTTPX_params.c +++ b/src/cHTTPX_params.c @@ -27,14 +27,11 @@ #include #include -#include "cHTTPX_crosspltm.h" - /** * Get a route parameter value by its name. - * @param req Pointer to the current HTTP request structure. + * @param req Pointer to the current HTTP request structure. * @param name Name of the route parameter (e.g., "uuid"). - * - * @return Pointer to the parameter value string if found, or NULL if the parameter does not exist. + * @return Pointer to the parameter value string if found, or NULL if absent. */ const char* cHTTPX_Param(chttpx_request_t* req, const char* name) { @@ -52,6 +49,15 @@ const char* cHTTPX_Param(chttpx_request_t* req, const char* name) return NULL; } +/** + * Parse a decimal string into a bounded signed 64-bit integer. + * + * @param text Input string. + * @param min_value Minimum allowed value (inclusive). + * @param max_value Maximum allowed value (inclusive). + * @param value Output parsed value. + * @return 1 on success, otherwise 0. + */ static int parse_i64(const char* text, long long min_value, long long max_value, long long* value) { if (!text || !*text || !value) @@ -65,6 +71,14 @@ static int parse_i64(const char* text, long long min_value, long long max_value, return 1; } +/** + * Parse a route parameter as a signed integer. + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output integer on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamInt(chttpx_request_t* req, const char* name, int* value) { long long parsed; @@ -74,6 +88,14 @@ int cHTTPX_ParamInt(chttpx_request_t* req, const char* name, int* value) return 1; } +/** + * Parse a route parameter as an unsigned 64-bit integer. + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output value on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamU64(chttpx_request_t* req, const char* name, uint64_t* value) { const char* text = cHTTPX_Param(req, name); @@ -88,6 +110,14 @@ int cHTTPX_ParamU64(chttpx_request_t* req, const char* name, uint64_t* value) return 1; } +/** + * Parse a route parameter as a boolean (true/false/1/0). + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output boolean on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamBool(chttpx_request_t* req, const char* name, bool* value) { const char* text = cHTTPX_Param(req, name); diff --git a/src/cHTTPX_params.h b/src/cHTTPX_params.h index 2bc9da2..dd3ca68 100644 --- a/src/cHTTPX_params.h +++ b/src/cHTTPX_params.h @@ -17,24 +17,44 @@ extern "C" /** * Get a route parameter value by its name. - * @param req Pointer to the current HTTP request structure. - * @param name Name of the route parameter (e.g., "uuid"). * - * @return Pointer to the parameter value string if found, or NULL if the parameter does not exist. + * @param req Pointer to the current HTTP request structure. + * @param name Name of the route parameter (e.g., "uuid"). + * @return Pointer to the parameter value string if found, or NULL if absent. */ const char* cHTTPX_Param(chttpx_request_t* req, const char* name); - /** Parse a route parameter as an integer. Returns 1 on success, otherwise 0. */ + /** + * Parse a route parameter as a signed integer. + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output integer on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamInt(chttpx_request_t* req, const char* name, int* value); - /** Parse a route parameter as an unsigned 64-bit integer. Returns 1 on success, otherwise 0. */ + /** + * Parse a route parameter as an unsigned 64-bit integer. + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output value on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamU64(chttpx_request_t* req, const char* name, uint64_t* value); - /** Parse a route parameter as a boolean. Returns 1 on success, otherwise 0. */ + /** + * Parse a route parameter as a boolean (true/false/1/0). + * + * @param req Current HTTP request. + * @param name Route parameter name. + * @param value Output boolean on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_ParamBool(chttpx_request_t* req, const char* name, bool* value); #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_queries.c b/src/cHTTPX_queries.c index ca27ecc..21ef436 100644 --- a/src/cHTTPX_queries.c +++ b/src/cHTTPX_queries.c @@ -33,8 +33,8 @@ * Searches the parsed URL query parameters (e.g. ?name=value&age=10) * and returns the value associated with the given parameter name. * - * @param req Pointer to the current HTTP request. - * @param name Name of the query parameter. + * @param req Pointer to the current HTTP request. + * @param name Name of the query parameter. * @return Pointer to the parameter value string if found, or NULL if not present. */ const char* cHTTPX_Query(chttpx_request_t* req, const char* name) @@ -53,7 +53,12 @@ const char* cHTTPX_Query(chttpx_request_t* req, const char* name) return NULL; } -/* Parse queries in request */ +/** + * Parse a URL query string into the request query table. + * + * @param req Request to populate. + * @param query Mutable query substring (ampersand-separated pairs). + */ void _parse_req_query(chttpx_request_t* req, char* query) { if (!req || !query) @@ -102,6 +107,12 @@ void _parse_req_query(chttpx_request_t* req, char* query) } } +/** + * Convert one hex digit to 0–15, or -1 if invalid. + * + * @param value ASCII hex digit. + * @return Numeric value, or -1. + */ static int hex_value(char value) { if (value >= '0' && value <= '9') @@ -113,6 +124,15 @@ static int hex_value(char value) return -1; } +/** + * Decode percent-encoded URL data into a caller-provided buffer. + * + * @param destination Destination buffer. + * @param destination_size Destination size including the terminator. + * @param source Encoded source string. + * @param plus_as_space Decode '+' as a space when true. + * @return 1 on success, otherwise 0. + */ int cHTTPX_UrlDecode(char* destination, size_t destination_size, const char* source, bool plus_as_space) { if (!destination || destination_size == 0 || !source) @@ -146,6 +166,16 @@ int cHTTPX_UrlDecode(char* destination, size_t destination_size, const char* sou return 1; } +/** + * Parse a named query parameter as a bounded signed 64-bit integer. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param min_value Minimum allowed value (inclusive). + * @param max_value Maximum allowed value (inclusive). + * @param value Output parsed value. + * @return 1 on success, otherwise 0. + */ static int query_i64(chttpx_request_t* req, const char* name, long long min_value, long long max_value, long long* value) { const char* text = cHTTPX_Query(req, name); @@ -160,6 +190,14 @@ static int query_i64(chttpx_request_t* req, const char* name, long long min_valu return 1; } +/** + * Parse a query parameter as a signed integer. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output integer on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryInt(chttpx_request_t* req, const char* name, int* value) { long long parsed; @@ -169,6 +207,14 @@ int cHTTPX_QueryInt(chttpx_request_t* req, const char* name, int* value) return 1; } +/** + * Parse a query parameter as an unsigned 64-bit integer. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output value on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryU64(chttpx_request_t* req, const char* name, uint64_t* value) { const char* text = cHTTPX_Query(req, name); @@ -183,6 +229,14 @@ int cHTTPX_QueryU64(chttpx_request_t* req, const char* name, uint64_t* value) return 1; } +/** + * Parse a query parameter as a boolean (true/false/1/0). + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output boolean on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryBool(chttpx_request_t* req, const char* name, bool* value) { const char* text = cHTTPX_Query(req, name); @@ -197,6 +251,14 @@ int cHTTPX_QueryBool(chttpx_request_t* req, const char* name, bool* value) return 1; } +/** + * Parse a query parameter as a double. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output double on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryDouble(chttpx_request_t* req, const char* name, double* value) { const char* text = cHTTPX_Query(req, name); @@ -211,6 +273,15 @@ int cHTTPX_QueryDouble(chttpx_request_t* req, const char* name, double* value) return 1; } +/** + * Parse a query parameter as uint64_t, or write default_value when absent. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output value. + * @param default_value Value used when the parameter is missing. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryU64Default(chttpx_request_t* req, const char* name, uint64_t* value, uint64_t default_value) { if (!value) diff --git a/src/cHTTPX_queries.h b/src/cHTTPX_queries.h index 2d2cb1b..434e4f1 100644 --- a/src/cHTTPX_queries.h +++ b/src/cHTTPX_queries.h @@ -18,48 +18,85 @@ extern "C" /** * Get a query parameter value by name. * - * Searches the parsed URL query parameters (e.g. ?name=value&age=10) - * and returns the value associated with the given parameter name. + * Searches parsed URL query parameters (e.g. ?name=value&age=10). * - * @param req Pointer to the current HTTP request. - * @param name Name of the query parameter. + * @param req Pointer to the current HTTP request. + * @param name Name of the query parameter. * @return Pointer to the parameter value string if found, or NULL if not present. */ const char* cHTTPX_Query(chttpx_request_t* req, const char* name); - /** Parse a query parameter as an integer. Returns 1 on success, otherwise 0. */ + /** + * Parse a query parameter as a signed integer. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output integer on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryInt(chttpx_request_t* req, const char* name, int* value); - /** Parse a query parameter as an unsigned 64-bit integer. Returns 1 on success, otherwise 0. */ + /** + * Parse a query parameter as an unsigned 64-bit integer. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output value on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryU64(chttpx_request_t* req, const char* name, uint64_t* value); - /** Parse a query parameter as a boolean. Returns 1 on success, otherwise 0. */ + /** + * Parse a query parameter as a boolean (true/false/1/0). + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output boolean on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryBool(chttpx_request_t* req, const char* name, bool* value); - /** Parse a query parameter as a double. Returns 1 on success, otherwise 0. */ + /** + * Parse a query parameter as a double. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output double on success. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryDouble(chttpx_request_t* req, const char* name, double* value); - /** Parse a query parameter or write default_value when it is absent or invalid. */ + /** + * Parse a query parameter as uint64_t, or write default_value when absent. + * + * @param req Current HTTP request. + * @param name Query parameter name. + * @param value Output value. + * @param default_value Value used when the parameter is missing. + * @return 1 on success, otherwise 0. + */ int cHTTPX_QueryU64Default(chttpx_request_t* req, const char* name, uint64_t* value, uint64_t default_value); /** * Decode percent-encoded URL data into a caller-provided buffer. * - * @param destination Destination buffer. - * @param - * destination_size Destination size including the terminator. - * @param source Encoded source string. - * @param plus_as_space - * Decode '+' as a space when true. + * @param destination Destination buffer. + * @param destination_size Destination size including the terminator. + * @param source Encoded source string. + * @param plus_as_space Decode '+' as a space when true. * @return 1 on success, otherwise 0. */ int cHTTPX_UrlDecode(char* destination, size_t destination_size, const char* source, bool plus_as_space); - /* Parse queries in request */ + /** + * Parse a URL query string into the request query table. + * + * @param req Request to populate. + * @param query Mutable query substring (ampersand-separated pairs). + */ void _parse_req_query(chttpx_request_t* req, char* query); #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_request.c b/src/cHTTPX_request.c index b1b3cef..b214653 100644 --- a/src/cHTTPX_request.c +++ b/src/cHTTPX_request.c @@ -40,6 +40,7 @@ #define CHTTPX_CLEANUP_BLOCK_CAPACITY 32 #define CHTTPX_CONTEXT_BUCKET_COUNT 32 +/** One deferred cleanup callback registered on a request. */ typedef struct chttpx_cleanup_entry { void* resource; @@ -47,6 +48,7 @@ typedef struct chttpx_cleanup_entry struct chttpx_cleanup_entry* next; } chttpx_cleanup_entry_t; +/** Fixed-size pool block for cleanup entries. */ typedef struct chttpx_cleanup_block { size_t used; @@ -54,6 +56,7 @@ typedef struct chttpx_cleanup_block struct chttpx_cleanup_block* next; } chttpx_cleanup_block_t; +/** Request-scoped list of deferred cleanup callbacks. */ typedef struct { chttpx_cleanup_entry_t* head; @@ -61,6 +64,7 @@ typedef struct chttpx_cleanup_block_t* extra_blocks; } chttpx_cleanup_state_t; +/** Named context bucket entry in the request hash table. */ typedef struct chttpx_context_entry { void* value; @@ -69,6 +73,7 @@ typedef struct chttpx_context_entry char name[]; } chttpx_context_entry_t; +/** Open hash table of named request contexts. */ typedef struct { chttpx_context_entry_t* buckets[CHTTPX_CONTEXT_BUCKET_COUNT]; @@ -123,6 +128,12 @@ static chttpx_cleanup_state_t* cleanup_state(chttpx_request_t* req, int create) * @param state Cleanup state owned by the request. * @return Reusable cleanup entry or NULL on allocation failure. */ +/** + * Cleanup entry alloc. + * + * @param state Parameter `state`. + * @return Pointer or NULL on failure. + */ static chttpx_cleanup_entry_t* cleanup_entry_alloc(chttpx_cleanup_state_t* state) { if (!state) @@ -503,6 +514,7 @@ int cHTTPX_OnBodyChunk(chttpx_request_t* req, chttpx_body_chunk_fn callback, voi return 0; } +/** Localized validation error message templates. */ typedef struct { const char* required; @@ -666,6 +678,12 @@ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t fiel } /* Validator email string */ +/** + * Is valid email. + * + * @param email Parameter `email`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int is_valid_email(const char* email) { if (!email) @@ -688,6 +706,16 @@ static int is_valid_email(const char* email) return 1; } +/** + * Set error. + * + * @param error_msg Parameter `error_msg`. + * @param error_size Parameter `error_size`. + * @param lang Parameter `lang`. + * @param key Parameter `key`. + * @param field_name Parameter `field_name`. + * @param num Parameter `num`. + */ static void set_error(char* error_msg, size_t error_size, i18n_language_t lang, int key, const char* field_name, size_t num) { validation_messages_t* msg = messages[lang]; @@ -798,6 +826,15 @@ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t f return 1; } +/** + * Parse and validate a JSON request body, writing a 400 response on failure. + * + * @param req Current HTTP request. + * @param res Response populated when binding fails. + * @param fields Field definitions and output targets. + * @param field_count Number of field definitions. + * @return 1 on success, 0 on parsing or validation failure. + */ int cHTTPX_BindJSON(chttpx_request_t* req, chttpx_response_t* res, chttpx_validation_t* fields, size_t field_count) { if (!req || !res || !fields) diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h index 04372d3..4f497b8 100644 --- a/src/cHTTPX_request.h +++ b/src/cHTTPX_request.h @@ -30,14 +30,14 @@ extern "C" #define MAX_HEADER_NAME 128 #define MAX_HEADER_VALUE 4096 - /* Header structure */ + /** One HTTP request header name/value pair (fixed-size storage). */ typedef struct { char name[MAX_HEADER_NAME]; char value[MAX_HEADER_VALUE]; } chttpx_header_t; - /* Query structure */ + /** Parsed query-string name/value pair (heap strings). */ typedef struct { char* name; @@ -48,7 +48,7 @@ extern "C" #define MAX_PARAM_NAME 128 #define MAX_PARAM_VALUE 1024 - /* Param structure */ + /** Route template parameter extracted from the request path. */ typedef struct { char name[MAX_PARAM_NAME]; @@ -57,7 +57,7 @@ extern "C" #define MAX_COOKIES 64 - /* Cookie structure */ + /** Parsed Cookie header entry. */ typedef struct { char* name; @@ -75,7 +75,7 @@ extern "C" int same_site; } chttpx_cookie_t; - /* Validation structs */ + /** JSON field type used by cHTTPX_Parse / cHTTPX_Validate. */ typedef enum { FIELD_STRING, @@ -85,18 +85,21 @@ extern "C" FIELD_NUMBER_ARRAY } validation_t; + /** Parsed JSON array of strings (request-owned items). */ typedef struct { char** items; size_t count; } chttpx_string_array_t; + /** Parsed JSON array of numbers (request-owned items). */ typedef struct { int* items; size_t count; } chttpx_number_array_t; + /** Built-in string validator kind for chttpx_validation_t. */ typedef enum { VALIDATOR_NONE, @@ -105,8 +108,10 @@ extern "C" VALIDATOR_URL, } validator_type_t; + /** Optional application validator invoked during cHTTPX_Validate. */ typedef bool (*chttpx_custom_validator_t)(const void* value, char* error, size_t error_size); + /** Bit flags for normalizing parsed string fields. */ typedef enum { cHTTPX_NORMALIZE_NONE = 0, @@ -122,6 +127,7 @@ extern "C" #define CHTTPX_UPPERCASE cHTTPX_UPPERCASE #endif + /** One field binding for JSON parse and validate helpers. */ typedef struct { const char* name; @@ -150,11 +156,13 @@ extern "C" chttpx_custom_validator_t custom_validator; } chttpx_validation_t; - /* Function for free REQuest context */ + /** Callback that releases a named request context value. */ typedef void (*chttpx_context_free_fn)(void*); + /** Callback registered with cHTTPX_Defer for automatic cleanup. */ typedef void (*chttpx_cleanup_fn)(void*); + /** Uploaded file metadata tracked on the request. */ typedef struct { const char* path; @@ -165,9 +173,10 @@ extern "C" const char* field_name; } chttpx_file_t; + /** Chunk callback used by cHTTPX_OnBodyChunk. */ typedef int (*chttpx_body_chunk_fn)(const unsigned char* data, size_t size, void* user_data); - // REQuest + /** Per-request HTTP state populated by the server parser. */ typedef struct { char* method; @@ -379,16 +388,13 @@ extern "C" * Parse and validate a JSON request body. * * Parsed strings and arrays are request-owned. On failure this function - * * creates a safe JSON 400 response in res. * * @param req Current HTTP request. - * @param res Response populated when binding - * fails. + * @param res Response populated when binding fails. * @param fields Field definitions and output targets. * @param field_count Number of field definitions. - * @return 1 on - * success, 0 on parsing or validation failure. + * @return 1 on success, 0 on parsing or validation failure. */ int cHTTPX_BindJSON(chttpx_request_t* req, struct chttpx_response* res, chttpx_validation_t* fields, size_t field_count); @@ -451,7 +457,6 @@ extern "C" } #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index ed952eb..9e421c5 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -204,6 +204,15 @@ const char* cHTTPX_StatusReason(uint16_t status) chttpx_response_t cHTTPX_ResJson(uint16_t status, const char* fmt, ...); +/** + * Match route. + * + * @param template Parameter `template`. + * @param path Parameter `path`. + * @param params Parameter `params`. + * @param param_count Parameter `param_count`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int match_route(const char* template, const char* path, chttpx_param_t* params, int* param_count) { int count = 0; @@ -284,6 +293,11 @@ static chttpx_route_t* find_route(chttpx_request_t* req) return NULL; } +/** + * Socket read timed out. + * + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int socket_read_timed_out(void) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -294,6 +308,16 @@ static int socket_read_timed_out(void) #endif } +/** + * Read req. + * + * @param server HTTP server instance. + * @param fd Parameter `fd`. + * @param tls_session Parameter `tls_session`. + * @param buffer Parameter `buffer`. + * @param buffer_size Parameter `buffer_size`. + * @return Bytes read or a negative error code. + */ static ssize_t read_req(chttpx_serv_t* server, chttpx_socket_t fd, void* tls_session, char* buffer, size_t buffer_size) { size_t total = 0; @@ -341,6 +365,12 @@ static ssize_t read_req(chttpx_serv_t* server, chttpx_socket_t fd, void* tls_ses } } +/** + * Set client timeout. + * + * @param server HTTP server instance. + * @param client_fd Parameter `client_fd`. + */ static void set_client_timeout(chttpx_serv_t* server, chttpx_socket_t client_fd) { if (!server) @@ -393,8 +423,24 @@ static const char* allowed_origin_cors(chttpx_serv_t* server, const char* req_or } /* Etag for response cache */ +/** + * Generate etag. + * + * @param body Parameter `body`. + * @param body_size Parameter `body_size`. + * @return Pointer or NULL on failure. + */ static const char* generate_etag(const unsigned char* body, size_t body_size); +/** + * Append response header. + * + * @param buffer Parameter `buffer`. + * @param capacity Parameter `capacity`. + * @param length Parameter `length`. + * @param format Parameter `format`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int append_response_header(char* buffer, size_t capacity, size_t* length, const char* format, ...) { if (*length >= capacity) @@ -501,6 +547,12 @@ static int build_response_buffer(chttpx_request_t* req, chttpx_response_t res, c return cHTTPX_ERR_LIMIT; } +/** + * Send response. + * + * @param req Current HTTP request. + * @param response HTTP response. + */ static void send_response(chttpx_request_t* req, chttpx_response_t res) { char* response = NULL; @@ -515,6 +567,13 @@ static void send_response(chttpx_request_t* req, chttpx_response_t res) } /* Handle browser CORS preflight without hijacking ordinary OPTIONS routes. */ +/** + * Build cors preflight. + * + * @param req Current HTTP request. + * @param response HTTP response. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int build_cors_preflight(chttpx_request_t* req, chttpx_response_t* res) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -526,6 +585,12 @@ static int build_cors_preflight(chttpx_request_t* req, chttpx_response_t* res) return 1; } +/** + * Is cors preflight. + * + * @param req Current HTTP request. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int is_cors_preflight(chttpx_request_t* req) { chttpx_response_t res = {0}; @@ -536,6 +601,12 @@ static int is_cors_preflight(chttpx_request_t* req) return 1; } +/** + * Valid request id. + * + * @param value Parameter `value`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int valid_request_id(const char* value) { if (!value || !*value || strlen(value) > 64) @@ -548,6 +619,11 @@ static int valid_request_id(const char* value) return 1; } +/** + * Set request id. + * + * @param req Current HTTP request. + */ static void set_request_id(chttpx_request_t* req) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -567,6 +643,13 @@ static void set_request_id(chttpx_request_t* req) sequence); } +/** + * Language allowed. + * + * @param server HTTP server instance. + * @param language Parameter `language`. + * @return Non-zero on success, 0 on failure, or a negative error code. + */ static int language_allowed(chttpx_serv_t* server, const char* language) { if (!server || !language || !*language) @@ -581,6 +664,11 @@ static int language_allowed(chttpx_serv_t* server, const char* language) return 0; } +/** + * Set request language. + * + * @param req Current HTTP request. + */ static void set_request_language(chttpx_request_t* req) { chttpx_serv_t* server = req ? req->_server : NULL; @@ -630,6 +718,16 @@ static void set_request_language(chttpx_request_t* req) } } +/** + * Parse req buffer. + * + * @param server HTTP server instance. + * @param client_fd Parameter `client_fd`. + * @param tls_session Parameter `tls_session`. + * @param buffer Parameter `buffer`. + * @param received Parameter `received`. + * @return Pointer or NULL on failure. + */ static chttpx_request_t* parse_req_buffer(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* buffer, size_t received) { chttpx_request_t* req = calloc(1, sizeof(chttpx_request_t)); @@ -719,12 +817,22 @@ static chttpx_request_t* parse_req_buffer(chttpx_serv_t* server, chttpx_socket_t return req; } +/** + * Close prefetched stream. + * + * @param resource Parameter `resource`. + */ static void close_prefetched_stream(void* resource) { if (resource) fclose((FILE*)resource); } +/** + * Free request object. + * + * @param req Current HTTP request. + */ static void free_request_object(chttpx_request_t* req) { if (!req) @@ -743,6 +851,20 @@ static void free_request_object(chttpx_request_t* req) free(req); } +/** + * Parse prefetched request. + * + * @param server HTTP server instance. + * @param client_fd Parameter `client_fd`. + * @param tls_session Parameter `tls_session`. + * @param headers Parameter `headers`. + * @param header_size Parameter `header_size`. + * @param body Parameter `body`. + * @param body_size Parameter `body_size`. + * @param body_stream Parameter `body_stream`. + * @param content_length Parameter `content_length`. + * @return Open temporary file or NULL on failure. + */ static chttpx_request_t* parse_prefetched_request(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length) { chttpx_request_t* req = calloc(1, sizeof(*req)); @@ -1000,6 +1122,13 @@ void* chttpx_handle(void* arg) return NULL; } +/** + * Generate etag. + * + * @param body Parameter `body`. + * @param body_size Parameter `body_size`. + * @return Pointer or NULL on failure. + */ static const char* generate_etag(const unsigned char* body, size_t body_size) { uint64_t hash = 5381; @@ -1027,6 +1156,15 @@ static const char* generate_etag(const unsigned char* body, size_t body_size) * @param fmt printf-style format string for the JSON body. * @param ... Format arguments. */ +/** + * Response format. + * + * @param status Parameter `status`. + * @param content_type Parameter `content_type`. + * @param fallback Parameter `fallback`. + * @param fmt Parameter `fmt`. + * @param args Parameter `args`. + */ static chttpx_response_t response_format(uint16_t status, const char* content_type, const char* fallback, const char* fmt, va_list args) { if (!fmt) diff --git a/src/cHTTPX_response.h b/src/cHTTPX_response.h index 2eefbc9..f67e59b 100644 --- a/src/cHTTPX_response.h +++ b/src/cHTTPX_response.h @@ -20,7 +20,7 @@ extern "C" struct chttpx_serv; - // RESponse + /** Ownership mode for response body memory. */ typedef enum { cHTTPX_BODY_BORROWED = 0, @@ -32,6 +32,7 @@ extern "C" #define CHTTPX_BODY_OWNED cHTTPX_BODY_OWNED #endif + /** HTTP response built by handlers or helper constructors. */ typedef struct chttpx_response { /* Response status code */ @@ -59,7 +60,7 @@ extern "C" struct timespec end_ts; } chttpx_response_t; - /* handler */ + /** Application route handler callback. */ typedef void (*chttpx_handler_t)(chttpx_request_t* req, chttpx_response_t* res); /** @@ -74,9 +75,7 @@ extern "C" int _chttpx_dispatch(struct chttpx_serv* server, chttpx_request_t* req, chttpx_response_t* res); /* Internal adapter used by the HTTP/2 transport to reuse the public request/handler API. */ - int _chttpx_execute_prefetched(struct chttpx_serv* server, chttpx_socket_t client_fd, void* tls_session, - char* headers, size_t header_size, unsigned char* body, size_t body_size, - FILE* body_stream, size_t content_length, char** output, size_t* output_size); + int _chttpx_execute_prefetched(struct chttpx_serv* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size); /** * Create a JSON HTTP response with formatted content. @@ -147,16 +146,14 @@ extern "C" /** * Send an entire buffer, retrying partial socket writes. * - * @param fd Connected socket descriptor. - * @param data Buffer - * to send. + * @param fd Connected socket descriptor. + * @param data Buffer to send. * @param size Buffer size in bytes. * @return cHTTPX_OK on success, otherwise a negative error code. */ int cHTTPX_SendAll(chttpx_socket_t fd, const void* data, size_t size); #ifdef __cplusplus - extern } #endif diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c index 640fcff..7b0d3ee 100644 --- a/src/cHTTPX_runtime.c +++ b/src/cHTTPX_runtime.c @@ -21,6 +21,9 @@ #define CHTTPX_RUNTIME_EVENTS 256 #define CHTTPX_CHUNK_LINE_MAX 128 +/** + * High-level connection lifecycle state. + */ typedef enum { CHTTPX_CONN_TLS, @@ -32,6 +35,9 @@ typedef enum CHTTPX_CONN_CLOSING } chttpx_connection_state_t; +/** + * Chunked transfer decoding sub-state. + */ typedef enum { CHTTPX_CHUNK_SIZE, @@ -46,6 +52,9 @@ typedef enum struct chttpx_runtime; +/** + * One accepted client connection in the runtime. + */ typedef struct chttpx_connection { struct chttpx_runtime* runtime; @@ -82,6 +91,9 @@ typedef struct chttpx_connection struct chttpx_connection* completion_next; } chttpx_connection_t; +/** + * Event loop, worker pool, and connection lists for a server. + */ typedef struct chttpx_runtime { chttpx_serv_t* server; @@ -100,11 +112,23 @@ typedef struct chttpx_runtime } chttpx_runtime_t; +/** + * Return whether the runtime shutdown flag is set. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @return True when graceful shutdown has been requested. + */ static bool runtime_is_stopping(chttpx_runtime_t* runtime) { return __atomic_load_n(&runtime->stopping, __ATOMIC_ACQUIRE); } +/** + * Return whether a server socket handle is open. + * + * @param fd Socket descriptor. + * @return True when the descriptor refers to an open socket. + */ static bool socket_valid(chttpx_socket_t fd) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -114,6 +138,12 @@ static bool socket_valid(chttpx_socket_t fd) #endif } +/** + * Restore blocking mode and socket timeouts on a connection. + * + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_set_blocking(chttpx_connection_t* connection) { if (!connection) @@ -146,6 +176,10 @@ static int connection_set_blocking(chttpx_connection_t* connection) return 0; } +/** + * Return a monotonic timestamp in milliseconds. + * @return Zero on success or a negative error code. + */ static uint64_t monotonic_ms(void) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -157,6 +191,11 @@ static uint64_t monotonic_ms(void) #endif } +/** + * Lock the worker completion queue. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void completion_lock(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -166,6 +205,11 @@ static void completion_lock(chttpx_runtime_t* runtime) #endif } +/** + * Unlock the worker completion queue. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void completion_unlock(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -175,6 +219,12 @@ static void completion_unlock(chttpx_runtime_t* runtime) #endif } +/** + * Initialize runtime synchronization primitives. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @return Zero on success or a negative error code. + */ static int sync_init(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -185,6 +235,11 @@ static int sync_init(chttpx_runtime_t* runtime) #endif } +/** + * Destroy runtime synchronization primitives. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void sync_destroy(chttpx_runtime_t* runtime) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -194,6 +249,12 @@ static void sync_destroy(chttpx_runtime_t* runtime) #endif } +/** + * Enqueue a connection onto the completion list. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + */ static void completion_push(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { completion_lock(runtime); @@ -203,6 +264,12 @@ static void completion_push(chttpx_runtime_t* runtime, chttpx_connection_t* conn _chttpx_event_wake(runtime->event_loop); } +/** + * Detach and return the entire completion list. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @return Head of the detached worker completion list, or NULL when empty. + */ static chttpx_connection_t* completion_take_all(chttpx_runtime_t* runtime) { completion_lock(runtime); @@ -212,6 +279,12 @@ static chttpx_connection_t* completion_take_all(chttpx_runtime_t* runtime) return list; } +/** + * Worker pool entry point that runs one connection job. + * + * @param job Connection job executed on a worker thread. + * @param context Runtime instance registered at pool creation. + */ static void runtime_worker_execute(void* job, void* context) { chttpx_connection_t* connection = job; @@ -225,6 +298,12 @@ static void runtime_worker_execute(void* job, void* context) completion_push(runtime, connection); } +/** + * Remove a connection from the active connection list. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + */ static void connection_unlink(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { chttpx_connection_t** current = &runtime->connections; @@ -239,6 +318,12 @@ static void connection_unlink(chttpx_runtime_t* runtime, chttpx_connection_t* co } } +/** + * Close socket/TLS state and free connection buffers. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + */ static void connection_close(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { if (!runtime || !connection) @@ -268,17 +353,37 @@ static void connection_close(chttpx_runtime_t* runtime, chttpx_connection_t* con free(connection); } +/** + * Refresh idle timeout tracking for a connection. + * + * @param connection Active client connection managed by the runtime. + */ static void connection_touch(chttpx_connection_t* connection) { connection->last_activity_ms = monotonic_ms(); } +/** + * Compare a length-prefixed string to a literal case-insensitively. + * + * @param value Value as a null-terminated C string. + * @param value_size value size for the operation. + * @param expected Expected as a null-terminated C string. + * @return True when the values match. + */ static bool span_equal_ci(const char* value, size_t value_size, const char* expected) { size_t expected_size = strlen(expected); return value_size == expected_size && strncasecmp(value, expected, value_size) == 0; } +/** + * Test whether a Content-Type matches an expected value. + * + * @param value Value as a null-terminated C string. + * @param expected Expected as a null-terminated C string. + * @return True when the values match. + */ static bool content_type_matches(const char* value, const char* expected) { if (!value || !expected) @@ -290,6 +395,14 @@ static bool content_type_matches(const char* value, const char* expected) return suffix == '\0' || suffix == ';' || suffix == ' ' || suffix == '\t'; } +/** + * Parse a decimal size from a header value span. + * + * @param value Value as a null-terminated C string. + * @param value_size value size for the operation. + * @param output Output pointer receiving allocated request header text. + * @return Zero on success or a negative error code. + */ static int parse_size_value(const char* value, size_t value_size, size_t* output) { if (!value || !value_size || !output || value_size >= 32) @@ -313,6 +426,12 @@ static int parse_size_value(const char* value, size_t value_size, size_t* output return 1; } +/** + * Initialize body storage mode from request headers. + * + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_prepare_body(chttpx_connection_t* connection) { const char* start = connection->headers; @@ -429,6 +548,14 @@ static int connection_prepare_body(chttpx_connection_t* connection) return 0; } +/** + * Store decoded body bytes respecting configured limits. + * + * @param connection Active client connection managed by the runtime. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ static int connection_store_decoded(chttpx_connection_t* connection, const unsigned char* data, size_t size) { if (size == 0) @@ -472,6 +599,14 @@ static int connection_store_decoded(chttpx_connection_t* connection, const unsig return 1; } +/** + * Advance chunked transfer decoding for one connection. + * + * @param connection Active client connection managed by the runtime. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ static int connection_feed_chunked(chttpx_connection_t* connection, const unsigned char* data, size_t size) { size_t offset = 0; @@ -576,6 +711,14 @@ static int connection_feed_chunked(chttpx_connection_t* connection, const unsign return 0; } +/** + * Append body bytes for fixed-length or decoded bodies. + * + * @param connection Active client connection managed by the runtime. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ static int connection_feed_body(chttpx_connection_t* connection, const unsigned char* data, size_t size) { if (connection->chunked) @@ -601,11 +744,25 @@ static int connection_feed_body(chttpx_connection_t* connection, const unsigned return 0; } +/** + * Return whether the expected request body has been received. + * + * @param connection Active client connection managed by the runtime. + * @return True when the buffered body size matches Content-Length. + */ static bool connection_body_complete(chttpx_connection_t* connection) { return connection->chunked ? connection->chunk_state == CHTTPX_CHUNK_DONE : connection->body_received >= connection->content_length; } +/** + * Queue a short error response for a connection. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @param status HTTP status code for the response. + * @return Zero on success or a negative error code. + */ static int connection_error_response(chttpx_runtime_t* runtime, chttpx_connection_t* connection, int status) { (void)status; @@ -613,6 +770,13 @@ static int connection_error_response(chttpx_runtime_t* runtime, chttpx_connectio return 0; } +/** + * Hand a ready connection to the worker pool. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_submit(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { _chttpx_event_del(runtime->event_loop, connection->fd); @@ -632,6 +796,15 @@ static int connection_submit(chttpx_runtime_t* runtime, chttpx_connection_t* con return 1; } +/** + * Parse incoming bytes through headers and body states. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ static int connection_process_bytes(chttpx_runtime_t* runtime, chttpx_connection_t* connection, const unsigned char* data, size_t size) { if (connection->state == CHTTPX_CONN_READING_HEADERS) @@ -736,6 +909,13 @@ static int connection_process_bytes(chttpx_runtime_t* runtime, chttpx_connection return 1; } +/** + * Advance non-blocking server TLS handshake for a connection. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { int result = _chttpx_tls_accept_step(connection->tls_session); @@ -774,6 +954,13 @@ static int connection_tls_step(chttpx_runtime_t* runtime, chttpx_connection_t* c return 0; } +/** + * Read available bytes when a socket is readable. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_read_ready(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { unsigned char buffer[BUFFER_SIZE]; @@ -819,6 +1006,13 @@ static int connection_read_ready(chttpx_runtime_t* runtime, chttpx_connection_t* } } +/** + * Flush pending response bytes when a socket is writable. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + * @param connection Active client connection managed by the runtime. + * @return Zero on success or a negative error code. + */ static int connection_write_ready(chttpx_runtime_t* runtime, chttpx_connection_t* connection) { while (connection->write_offset < connection->write_size) @@ -857,6 +1051,11 @@ static int connection_write_ready(chttpx_runtime_t* runtime, chttpx_connection_t return 0; } +/** + * Apply worker results and close finished connections. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void drain_completions(chttpx_runtime_t* runtime) { chttpx_connection_t* connection = completion_take_all(runtime); @@ -869,6 +1068,11 @@ static void drain_completions(chttpx_runtime_t* runtime) } } +/** + * Accept new clients until the server limit is reached. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void accept_connections(chttpx_runtime_t* runtime) { chttpx_serv_t* server = runtime->server; @@ -967,6 +1171,11 @@ static void accept_connections(chttpx_runtime_t* runtime) } } +/** + * Close idle connections that exceeded configured timeouts. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void scan_timeouts(chttpx_runtime_t* runtime) { uint64_t now = monotonic_ms(); @@ -989,6 +1198,11 @@ static void scan_timeouts(chttpx_runtime_t* runtime) } } +/** + * Stop accepting and close active connections during shutdown. + * + * @param runtime Runtime coordinating accepts, I/O, and worker dispatch. + */ static void begin_shutdown(chttpx_runtime_t* runtime) { if (runtime->shutdown_started) @@ -1010,6 +1224,12 @@ static void begin_shutdown(chttpx_runtime_t* runtime) } } +/** + * Create event loop and worker pool for a server. + * + * @param server Server instance. + * @return Zero on success or a negative error code. + */ int _chttpx_runtime_init(chttpx_serv_t* server) { if (!server || !socket_valid(server->server_fd) || server->max_clients == 0) @@ -1048,6 +1268,11 @@ int _chttpx_runtime_init(chttpx_serv_t* server) return cHTTPX_ERR_IO; } +/** + * Run the accept/read/write event loop until stop is requested. + * + * @param server Server instance. + */ void _chttpx_runtime_listen(chttpx_serv_t* server) { chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; @@ -1109,6 +1334,13 @@ void _chttpx_runtime_listen(chttpx_serv_t* server) drain_completions(runtime); } +/** + * Copy worker pool statistics for a server. + * + * @param server Server instance. + * @param stats Output buffer receiving worker statistics. + * @return Zero on success or a negative error code. + */ int _chttpx_runtime_worker_stats(chttpx_serv_t* server, chttpx_worker_stats_t* stats) { if (!server || !stats) @@ -1122,6 +1354,11 @@ int _chttpx_runtime_worker_stats(chttpx_serv_t* server, chttpx_worker_stats_t* s return cHTTPX_OK; } +/** + * Request graceful runtime shutdown. + * + * @param server Server instance. + */ void _chttpx_runtime_request_stop(chttpx_serv_t* server) { chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; @@ -1132,6 +1369,11 @@ void _chttpx_runtime_request_stop(chttpx_serv_t* server) _chttpx_event_wake(runtime->event_loop); } +/** + * Tear down runtime resources after listen returns. + * + * @param server Server instance. + */ void _chttpx_runtime_cleanup(chttpx_serv_t* server) { chttpx_runtime_t* runtime = server ? server->runtime_state : NULL; diff --git a/src/cHTTPX_runtime.h b/src/cHTTPX_runtime.h index a50e736..8366ff1 100644 --- a/src/cHTTPX_runtime.h +++ b/src/cHTTPX_runtime.h @@ -4,10 +4,42 @@ #include "cHTTPX_serv.h" #include "cHTTPX_worker.h" +/** + * Create event loop and worker pool for a server. + * + * @param server Target server. + * @return Zero on success or a negative error code. + */ int _chttpx_runtime_init(chttpx_serv_t* server); + +/** + * Run the accept/read/write event loop until stop is requested. + * + * @param server Target server. + */ void _chttpx_runtime_listen(chttpx_serv_t* server); + +/** + * Request graceful runtime shutdown. + * + * @param server Target server. + */ void _chttpx_runtime_request_stop(chttpx_serv_t* server); + +/** + * Tear down runtime resources after listen returns. + * + * @param server Target server. + */ void _chttpx_runtime_cleanup(chttpx_serv_t* server); + +/** + * Copy worker pool statistics for a server. + * + * @param server Target server. + * @param stats Output stats buffer. + * @return Zero on success or a negative error code. + */ int _chttpx_runtime_worker_stats(chttpx_serv_t* server, chttpx_worker_stats_t* stats); #endif diff --git a/src/cHTTPX_serv.c b/src/cHTTPX_serv.c index f750305..8873313 100644 --- a/src/cHTTPX_serv.c +++ b/src/cHTTPX_serv.c @@ -21,6 +21,14 @@ #include +/** + * Default stderr logger used when none is configured. + * + * @param level Maximum severity level to emit. + * @param request_id Request correlation id for logging. + * @param message Log message text. + * @param user_data Unused logger user data for the default logger. + */ static void default_logger(chttpx_log_level_t level, const char* request_id, const char* message, void* user_data) { (void)user_data; @@ -30,6 +38,11 @@ static void default_logger(chttpx_log_level_t level, const char* request_id, con fprintf(stderr, "[%s] request_id=%s %s\n", names[level], request_id && *request_id ? request_id : "-", message ? message : ""); } +/** + * Sleep the current thread for a number of milliseconds. + * + * @param milliseconds Number of milliseconds to sleep. + */ static void server_sleep_ms(unsigned int milliseconds) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -39,6 +52,12 @@ static void server_sleep_ms(unsigned int milliseconds) #endif } +/** + * Return whether a server socket handle is open. + * + * @param fd Socket descriptor. + * @return True when the descriptor refers to an open socket. + */ static bool socket_valid(chttpx_socket_t fd) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -48,6 +67,11 @@ static bool socket_valid(chttpx_socket_t fd) #endif } +/** + * Mark the listening socket as closed. + * + * @param server Server instance. + */ static void invalidate_socket(chttpx_serv_t* server) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -57,6 +81,11 @@ static void invalidate_socket(chttpx_serv_t* server) #endif } +/** + * Free owned language strings on a server. + * + * @param server Server instance. + */ static void free_server_languages(chttpx_serv_t* server) { if (!server) @@ -72,6 +101,15 @@ static void free_server_languages(chttpx_serv_t* server) server->default_language = NULL; } +/** + * Replace configured response languages for a server. + * + * @param server Server instance. + * @param languages Preferred language tags in priority order. + * @param count Number of language tags supplied. + * @param fallback Language tag used when no preference matches. + * @return Zero on success or a negative error code. + */ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, size_t count, const char* fallback) { if (!server || !fallback || (count > 0 && !languages)) @@ -115,6 +153,11 @@ int _chttpx_server_set_languages(chttpx_serv_t* server, const char** languages, return cHTTPX_OK; } +/** + * Release owned upload policy strings on a route. + * + * @param registered Registered route entry to modify. + */ static void free_route_upload_policy(chttpx_route_t* registered) { if (!registered || !registered->has_upload_policy) @@ -127,6 +170,11 @@ static void free_route_upload_policy(chttpx_route_t* registered) registered->has_upload_policy = false; } +/** + * Return a server configuration populated with library defaults. + * + * @return Default listen port, limits, and timeout values. + */ chttpx_config_t cHTTPX_DefaultConfig(void) { return (chttpx_config_t){.port = 8080, @@ -144,6 +192,15 @@ chttpx_config_t cHTTPX_DefaultConfig(void) .log_level = cHTTPX_LOG_INFO}; } +/** + * Initialize server fields, socket, TLS, metrics, and runtime. + * + * @param server Server instance. + * @param app Application instance. + * @param name Unique server name within the owning application. + * @param config Server listen and resource limit configuration. + * @return Zero on success or a negative error code. + */ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const char* name, const chttpx_config_t* config) { if (!server || !app || !name || !*name || !config || config->max_clients == 0 || @@ -329,6 +386,15 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha return cHTTPX_ERR_SOCKET; } +/** + * Allocate and register one route on a server. + * + * @param router Route table receiving registrations. + * @param method HTTP method name. + * @param path Request path or route pattern. + * @param handler Request handler callback. + * @return Registered route entry, or NULL on allocation failure. + */ static chttpx_route_t* route(chttpx_router_t* router, const char* method, const char* path, chttpx_handler_t handler) { chttpx_serv_t* server = router ? router->serv : NULL; @@ -372,6 +438,13 @@ static chttpx_route_t* route(chttpx_router_t* router, const char* method, const return registered; } +/** + * Build a router scoped to a path prefix. + * + * @param server Server instance. + * @param prefix Route path prefix for grouped registration. + * @return Router bound to the server and prefix, zeroed when the server is invalid. + */ chttpx_router_t cHTTPX_RoutePathPrefix(chttpx_serv_t* server, const char* prefix) { chttpx_router_t router; @@ -384,6 +457,14 @@ chttpx_router_t cHTTPX_RoutePathPrefix(chttpx_serv_t* server, const char* prefix return router; } +/** + * Register a handler on a router. + * + * @param router Route table receiving registrations. + * @param method HTTP method name. + * @param path Request path or route pattern. + * @param handler Request handler callback. + */ void cHTTPX_RegisterRoute(chttpx_router_t* router, const char* method, const char* path, chttpx_handler_t handler) { if (!router || !router->serv || !method || !path || !handler) @@ -396,6 +477,15 @@ void cHTTPX_RegisterRoute(chttpx_router_t* router, const char* method, const cha route(router, method, full_path, handler); } +/** + * Register a route using a router prefix. + * + * @param router Route table receiving registrations. + * @param method HTTP method name. + * @param path Request path or route pattern. + * @param handler Request handler callback. + * @return Registered route entry, or NULL when registration fails. + */ static chttpx_route_t* register_route(chttpx_router_t* router, const char* method, const char* path, chttpx_handler_t handler) { if (!router || !router->serv || !method || !path || !handler) @@ -421,6 +511,13 @@ CHTTPX_ROUTE_HELPER(cHTTPX_Patch, cHTTPX_MethodPatch) CHTTPX_ROUTE_HELPER(cHTTPX_Delete, cHTTPX_MethodDelete) CHTTPX_ROUTE_HELPER(cHTTPX_Options, cHTTPX_MethodOptions) +/** + * Derive a child router with an additional prefix. + * + * @param parent Router whose server and middleware are inherited. + * @param prefix Additional path prefix appended to the parent router. + * @return Child router sharing the parent server and middleware stack. + */ chttpx_router_t cHTTPX_RouteGroup(const chttpx_router_t* parent, const char* prefix) { chttpx_router_t group; @@ -437,6 +534,13 @@ chttpx_router_t cHTTPX_RouteGroup(const chttpx_router_t* parent, const char* pre return group; } +/** + * Append a before-handler middleware to a router. + * + * @param router Route table receiving registrations. + * @param middleware Middleware callback to attach. + * @return Zero on success or a negative error code. + */ int cHTTPX_RouterUse(chttpx_router_t* router, chttpx_middleware_t middleware) { if (!router || !middleware || router->middleware_count >= MAX_MIDDLEWARES) @@ -445,6 +549,13 @@ int cHTTPX_RouterUse(chttpx_router_t* router, chttpx_middleware_t middleware) return cHTTPX_OK; } +/** + * Append an after-handler middleware to a router. + * + * @param router Route table receiving registrations. + * @param middleware Middleware callback to attach. + * @return Zero on success or a negative error code. + */ int cHTTPX_RouterUseAfter(chttpx_router_t* router, chttpx_middleware_t middleware) { if (!router || !middleware || router->after_middleware_count >= MAX_MIDDLEWARES) @@ -453,6 +564,13 @@ int cHTTPX_RouterUseAfter(chttpx_router_t* router, chttpx_middleware_t middlewar return cHTTPX_OK; } +/** + * Append a before-handler middleware to one route. + * + * @param registered Registered route entry to modify. + * @param middleware Middleware callback to attach. + * @return Zero on success or a negative error code. + */ int cHTTPX_RouteUse(chttpx_route_t* registered, chttpx_middleware_t middleware) { if (!registered || !middleware || registered->middleware_count >= MAX_MIDDLEWARES) @@ -461,6 +579,13 @@ int cHTTPX_RouteUse(chttpx_route_t* registered, chttpx_middleware_t middleware) return cHTTPX_OK; } +/** + * Append an after-handler middleware to one route. + * + * @param registered Registered route entry to modify. + * @param middleware Middleware callback to attach. + * @return Zero on success or a negative error code. + */ int cHTTPX_RouteUseAfter(chttpx_route_t* registered, chttpx_middleware_t middleware) { if (!registered || !middleware || registered->after_middleware_count >= MAX_MIDDLEWARES) @@ -469,6 +594,13 @@ int cHTTPX_RouteUseAfter(chttpx_route_t* registered, chttpx_middleware_t middlew return cHTTPX_OK; } +/** + * Attach upload size/type policy to a route. + * + * @param registered Registered route entry to modify. + * @param policy Upload size and allowed content-type policy to copy onto the route. + * @return Zero on success or a negative error code. + */ int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_policy_t* policy) { if (!registered || !policy || (policy->allowed_types_count > 0 && !policy->allowed_types)) @@ -505,12 +637,25 @@ int cHTTPX_RouteUploadPolicy(chttpx_route_t* registered, const chttpx_upload_pol return cHTTPX_OK; } +/** + * Clear a stack-allocated router. + * + * @param router Route table receiving registrations. + */ void cHTTPX_RouterFree(chttpx_router_t* router) { if (router) memset(router, 0, sizeof(*router)); } +/** + * Configure logging callback and level for a server. + * + * @param server Server instance. + * @param logger Logger callback to install. + * @param user_data Opaque pointer passed to the logger callback. + * @param level Maximum severity level to emit. + */ void cHTTPX_SetLogger(chttpx_serv_t* server, chttpx_logger_fn logger, void* user_data, chttpx_log_level_t level) { if (!server || !server->initialized) @@ -521,6 +666,11 @@ void cHTTPX_SetLogger(chttpx_serv_t* server, chttpx_logger_fn logger, void* user server->log_level = level; } +/** + * Enter the runtime listen loop for one server. + * + * @param server Server instance. + */ void _chttpx_server_listen(chttpx_serv_t* server) { if (!server || !server->initialized || !socket_valid(server->server_fd)) @@ -534,6 +684,11 @@ void _chttpx_server_listen(chttpx_serv_t* server) __atomic_store_n(&server->listening, false, __ATOMIC_RELEASE); } +/** + * Stop listening and release server-owned resources. + * + * @param server Server instance. + */ void _chttpx_server_shutdown(chttpx_serv_t* server) { if (!server || !server->initialized) diff --git a/src/cHTTPX_serv.h b/src/cHTTPX_serv.h index af6fafa..8bef8d1 100644 --- a/src/cHTTPX_serv.h +++ b/src/cHTTPX_serv.h @@ -26,6 +26,9 @@ extern "C" struct chttpx_app; +/** + * Library-wide status codes. + */ typedef enum { cHTTPX_OK = 0, @@ -65,6 +68,9 @@ extern "C" #endif +/** + * Server logging verbosity. + */ typedef enum { cHTTPX_LOG_DEBUG, @@ -84,6 +90,9 @@ extern "C" typedef void (*chttpx_logger_fn)(chttpx_log_level_t level, const char* request_id, const char* message, void* user_data); +/** + * Address family binding mode for listeners. + */ typedef enum { cHTTPX_NETWORK_IPV4 = 0, @@ -97,6 +106,9 @@ extern "C" #define CHTTPX_NETWORK_DUAL cHTTPX_NETWORK_DUAL #endif +/** + * Server TLS certificate configuration. + */ typedef struct { bool enabled; @@ -106,6 +118,9 @@ extern "C" bool require_client_cert; } chttpx_tls_config_t; +/** + * Outbound TLS trust and client certificate settings. + */ typedef struct { bool verify_peer; @@ -114,6 +129,9 @@ extern "C" const char* client_key_file; } chttpx_tls_client_config_t; +/** + * User-supplied server configuration. + */ typedef struct { uint16_t port; @@ -136,6 +154,9 @@ extern "C" void* logger_data; } chttpx_config_t; +/** + * Route-level upload limits and allowed media types. + */ typedef struct { size_t max_size; @@ -143,6 +164,9 @@ extern "C" size_t allowed_types_count; } chttpx_upload_policy_t; +/** + * Registered HTTP method, path, handler, and middleware chain. + */ typedef struct chttpx_route { const char* method; @@ -157,6 +181,9 @@ extern "C" bool compression_disabled; } chttpx_route_t; +/** + * HTTP server instance with routes and runtime state. + */ typedef struct chttpx_serv { struct chttpx_app* app; @@ -205,12 +232,18 @@ extern "C" chttpx_cors_t cors; } chttpx_serv_t; +/** + * Accepted client socket passed to worker jobs. + */ typedef struct { chttpx_serv_t* server; chttpx_socket_t client_fd; } chttpx_client_ctx_t; +/** + * Mutable route registration scope with middleware stacks. + */ typedef struct chttpx_router { chttpx_serv_t* serv; diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index 6f6e9d8..e3e7095 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -19,8 +19,18 @@ #include #include -static int chttpx_h2_alpn_select(SSL* ssl, const unsigned char** out, unsigned char* outlen, - const unsigned char* in, unsigned int inlen, void* arg) +/** + * Select HTTP/2 during TLS ALPN negotiation. + * + * @param ssl OpenSSL session participating in ALPN selection. + * @param out Output pointer to the selected ALPN protocol bytes. + * @param outlen Output length of the selected ALPN protocol. + * @param in Client ALPN extension bytes offered during handshake. + * @param inlen Length of the client ALPN extension. + * @param arg Unused ALPN callback user argument. + * @return SSL_TLSEXT_ERR_OK when HTTP/2 is selected, otherwise a fatal alert code. + */ +static int chttpx_h2_alpn_select(SSL* ssl, const unsigned char** out, unsigned char* outlen, const unsigned char* in, unsigned int inlen, void* arg) { (void)ssl; (void)arg; @@ -44,6 +54,12 @@ static int chttpx_h2_alpn_select(SSL* ssl, const unsigned char** out, unsigned c return SSL_TLSEXT_ERR_ALERT_FATAL; } +/** + * Return whether the negotiated ALPN protocol is HTTP/2. + * + * @param ssl OpenSSL session after handshake completion. + * @return Non-zero when the negotiated ALPN protocol is HTTP/2. + */ static int chttpx_tls_selected_h2(SSL* ssl) { const unsigned char* protocol = NULL; @@ -53,6 +69,13 @@ static int chttpx_tls_selected_h2(SSL* ssl) } #endif +/** + * Log a TLS failure through the server logger. + * + * @param server Server instance. + * @param request_id Request correlation id for logging. + * @param prefix Short failure prefix prepended to OpenSSL detail. + */ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const char* prefix) { if (!server || !server->logger || server->log_level > cHTTPX_LOG_ERROR) @@ -75,6 +98,11 @@ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const #endif } +/** + * Return whether TLS support was compiled in. + * + * @return Non-zero when OpenSSL support is enabled at build time. + */ int _chttpx_tls_available(void) { #ifdef CHTTPX_ENABLE_TLS @@ -84,6 +112,11 @@ int _chttpx_tls_available(void) #endif } +/** + * Free owned TLS path strings on a server. + * + * @param server Server instance. + */ static void free_server_tls_strings(chttpx_serv_t* server) { if (!server) @@ -95,6 +128,13 @@ static void free_server_tls_strings(chttpx_serv_t* server) memset(&server->tls, 0, sizeof(server->tls)); } +/** + * Load server certificate material and prepare the TLS context. + * + * @param server Server instance. + * @param config TLS configuration supplied by the caller. + * @return Zero on success or a negative error code. + */ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* config) { if (!server || !config) @@ -153,9 +193,7 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co if (names) SSL_CTX_set_client_CA_list(ctx, names); - SSL_CTX_set_verify(ctx, - server->tls.require_client_cert - ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + SSL_CTX_set_verify(ctx, server->tls.require_client_cert, ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : SSL_VERIFY_PEER, NULL); } @@ -179,6 +217,11 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co #endif } +/** + * Release server TLS context and owned path strings. + * + * @param server Server instance. + */ void _chttpx_tls_server_cleanup(chttpx_serv_t* server) { if (!server) @@ -192,6 +235,14 @@ void _chttpx_tls_server_cleanup(chttpx_serv_t* server) free_server_tls_strings(server); } +/** + * Perform a blocking TLS accept and require HTTP/2 ALPN. + * + * @param server Server instance. + * @param client_fd Connected client socket. + * @param session Out pointer receiving the accepted OpenSSL session. + * @return Zero on success or a negative error code. + */ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session) { if (!server || !session) @@ -224,6 +275,14 @@ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** #endif } +/** + * Allocate a TLS session and start a non-blocking accept handshake. + * + * @param server Server instance. + * @param client_fd Connected client socket. + * @param session Out pointer receiving the new OpenSSL session. + * @return Zero on success, WANT_READ/WANT_WRITE while pending, or an error code. + */ int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session) { if (!server || !session) @@ -249,6 +308,12 @@ int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, v #endif } +/** + * Advance a non-blocking TLS accept handshake. + * + * @param session OpenSSL session started by accept_begin. + * @return Zero when complete, WANT_READ/WANT_WRITE while pending, or an error code. + */ int _chttpx_tls_accept_step(void* session) { if (!session) @@ -268,6 +333,11 @@ int _chttpx_tls_accept_step(void* session) #endif } +/** + * Shut down and free one TLS session. + * + * @param session OpenSSL session to shut down and free. + */ void _chttpx_tls_session_close(void* session) { #ifdef CHTTPX_ENABLE_TLS @@ -281,9 +351,17 @@ void _chttpx_tls_session_close(void* session) #endif } -int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, - const chttpx_tls_client_config_t* config, - void** context, void** session) +/** + * Connect as a TLS client with HTTP/2 ALPN and optional mTLS. + * + * @param socket_fd Connected TCP socket. + * @param host Remote host name or IP literal. + * @param config TLS configuration supplied by the caller. + * @param context Out pointer receiving the client OpenSSL context. + * @param session Out pointer receiving the connected OpenSSL session. + * @return Zero on success or a negative error code. + */ +int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, const chttpx_tls_client_config_t* config, void** context, void** session) { if (!host || !*host || !context || !session) return cHTTPX_ERR_INVALID_ARGUMENT; @@ -311,8 +389,7 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, if (selected.verify_peer) { SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); - int trust_ok = selected.ca_file - ? SSL_CTX_load_verify_locations(ctx, selected.ca_file, NULL) + int trust_ok = selected.ca_file ? SSL_CTX_load_verify_locations(ctx, selected.ca_file, NULL) : SSL_CTX_set_default_verify_paths(ctx); if (trust_ok != 1) { @@ -383,6 +460,12 @@ int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, #endif } +/** + * Release client TLS session and context. + * + * @param context Client OpenSSL context returned from client_connect. + * @param session Client OpenSSL session returned from client_connect. + */ void _chttpx_tls_client_close(void* context, void* session) { #ifdef CHTTPX_ENABLE_TLS @@ -399,6 +482,15 @@ void _chttpx_tls_client_close(void* context, void* session) #endif } +/** + * Blocking receive through TLS or plain socket. + * + * @param fd Socket descriptor. + * @param tls_session OpenSSL session for TLS I/O, or NULL for cleartext. + * @param buffer Memory buffer for socket receive or send. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size) { if (!buffer || size == 0) @@ -442,6 +534,15 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t } } +/** + * Non-blocking receive with WANT_READ/WANT_WRITE semantics. + * + * @param fd Socket descriptor. + * @param tls_session OpenSSL session for TLS I/O, or NULL for cleartext. + * @param buffer Memory buffer for socket receive or send. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size) { if (!buffer || size == 0) @@ -483,6 +584,15 @@ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buf return cHTTPX_ERR_IO; } +/** + * Non-blocking send with WANT_READ/WANT_WRITE semantics. + * + * @param fd Socket descriptor. + * @param tls_session OpenSSL session for TLS I/O, or NULL for cleartext. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) { if (size > 0 && !data) @@ -528,6 +638,15 @@ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const voi return cHTTPX_ERR_IO; } +/** + * Send an entire buffer, retrying through TLS or plain socket. + * + * @param fd Socket descriptor. + * @param tls_session OpenSSL session for TLS I/O, or NULL for cleartext. + * @param data Payload bytes for the current chunk. + * @param size Buffer size in bytes for the I/O operation. + * @return Zero on success or a negative error code. + */ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, size_t size) { if (size > 0 && !data) diff --git a/src/cHTTPX_tls.h b/src/cHTTPX_tls.h index 7648e3c..098bae1 100644 --- a/src/cHTTPX_tls.h +++ b/src/cHTTPX_tls.h @@ -8,25 +8,89 @@ extern "C" { #endif +/** Return whether TLS support was compiled in. */ int _chttpx_tls_available(void); + +/** + * Log a TLS failure through the server logger. + * + * @param server Server providing the logger callback. + * @param request_id Correlation id for log output. + * @param message Short failure prefix. + */ void _chttpx_tls_log_error(chttpx_serv_t* server, const char* request_id, const char* message); + +/** + * Load server certificate material and prepare the TLS context. + * + * @param server Target server. + * @param config TLS configuration from server setup. + * @return Zero on success or a negative error code. + */ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* config); + +/** Release server TLS context and owned path strings. @param server Target server. */ void _chttpx_tls_server_cleanup(chttpx_serv_t* server); + +/** + * Perform a blocking TLS accept and require HTTP/2 ALPN. + * + * @param server Owning server. + * @param client_fd Accepted client socket. + * @param session Out pointer receiving the accepted OpenSSL session. + * @return Zero on success or a negative error code. + */ int _chttpx_tls_accept(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session); + #define CHTTPX_IO_WANT_READ -1001 #define CHTTPX_IO_WANT_WRITE -1002 + +/** + * Allocate a TLS session and start a non-blocking accept handshake. + * + * @param server Owning server. + * @param client_fd Accepted client socket. + * @param session Out pointer receiving the new OpenSSL session. + * @return Zero on success, WANT_READ/WANT_WRITE while pending, or an error code. + */ int _chttpx_tls_accept_begin(chttpx_serv_t* server, chttpx_socket_t client_fd, void** session); + +/** + * Advance a non-blocking TLS accept handshake. + * + * @param session OpenSSL session from accept_begin. + * @return Zero when complete, WANT_READ/WANT_WRITE while pending, or an error code. + */ int _chttpx_tls_accept_step(void* session); + +/** Shut down and free one TLS session. @param session OpenSSL session to release. */ void _chttpx_tls_session_close(void* session); -int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, - const chttpx_tls_client_config_t* config, - void** context, void** session); +/** + * Connect as a TLS client with HTTP/2 ALPN and optional mutual TLS. + * + * @param socket_fd Connected TCP socket. + * @param host SNI host name or IP literal. + * @param config Client trust and certificate settings. + * @param context Out pointer receiving the client OpenSSL context. + * @param session Out pointer receiving the connected OpenSSL session. + * @return Zero on success or a negative error code. + */ +int _chttpx_tls_client_connect(chttpx_socket_t socket_fd, const char* host, const chttpx_tls_client_config_t* config, void** context, void** session); + +/** Release client TLS session and context. */ void _chttpx_tls_client_close(void* context, void* session); +/** Blocking receive through TLS or plain socket. */ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size); + +/** Non-blocking receive with WANT_READ/WANT_WRITE semantics. */ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buffer, size_t size); + +/** Non-blocking send with WANT_READ/WANT_WRITE semantics. */ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const void* data, size_t size); + +/** Send an entire buffer, retrying through TLS or plain socket. */ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, size_t size); #ifdef __cplusplus diff --git a/src/cHTTPX_utils.h b/src/cHTTPX_utils.h index 308c536..459564b 100644 --- a/src/cHTTPX_utils.h +++ b/src/cHTTPX_utils.h @@ -14,12 +14,19 @@ extern "C" typedef HANDLE thread_t; + /** Arguments passed to a newly created Windows worker thread. */ typedef struct { void* (*function)(void*); void* argument; } chttpx_thread_start_t; + /** + * Windows thread entry that invokes the user start routine. + * + * @param value Heap-allocated chttpx_thread_start_t (freed here). + * @return Always 0. + */ static DWORD WINAPI _thread_start(LPVOID value) { chttpx_thread_start_t* start = value; @@ -30,6 +37,14 @@ extern "C" return 0; } + /** + * Create a worker thread (Windows). + * + * @param thread Output thread handle. + * @param func User function accepting void* argument. + * @param arg Argument passed to func. + * @return 0 on success, -1 on error. + */ static inline int _thread_create(thread_t* thread, void* (*func)(void*), void* arg) { chttpx_thread_start_t* start = malloc(sizeof(*start)); @@ -43,6 +58,12 @@ extern "C" return *thread ? 0 : -1; } + /** + * Wait for a worker thread to finish (Windows). + * + * @param thread Thread handle from _thread_create. + * @return 0 on success. + */ static inline int _thread_join(thread_t thread) { WaitForSingleObject(thread, INFINITE); @@ -54,11 +75,25 @@ extern "C" typedef pthread_t thread_t; +/** + * Create a worker thread (POSIX). + * + * @param thread Output pthread_t. + * @param func User function accepting void* argument. + * @param arg Argument passed to func. + * @return pthread_create result (0 on success). + */ static inline int _thread_create(thread_t* thread, void* (*func)(void*), void* arg) { return pthread_create(thread, NULL, func, arg); } +/** + * Wait for a worker thread to finish (POSIX). + * + * @param thread Thread id from _thread_create. + * @return pthread_join result (0 on success). + */ static inline int _thread_join(thread_t thread) { return pthread_join(thread, NULL); @@ -73,6 +108,15 @@ static inline int _thread_join(thread_t thread) #include +/** + * Case-insensitive memmem over a bounded haystack. + * + * @param haystack Buffer to search. + * @param haystack_len Length of haystack. + * @param needle Substring to find. + * @param needle_len Length of needle. + * @return Pointer into haystack, or NULL if not found. + */ static inline const char* memmem_case(const void* haystack, size_t haystack_len, const void* needle, size_t needle_len) { const unsigned char* h = (const unsigned char*)haystack; diff --git a/src/cHTTPX_websocket.c b/src/cHTTPX_websocket.c index 0dd5fc3..7a58669 100644 --- a/src/cHTTPX_websocket.c +++ b/src/cHTTPX_websocket.c @@ -22,18 +22,23 @@ #include "cHTTPX_websocket.h" -// Beta version +/* Beta version */ +/** + * Perform the HTTP Upgrade handshake for WebSocket (legacy API). + * + * The legacy raw-socket upgrade API cannot identify an HTTP/2 stream. The + * symbol is kept for source compatibility and fails explicitly instead of + * emitting an invalid connection-level upgrade response. + * + * @param client_socket Connected client socket. + * @param sec_wsocket_key Sec-WebSocket-Key header value. + * @return cHTTPX_ERR_UNAVAILABLE. + */ int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key) { (void)client_socket; (void)sec_wsocket_key; - - /* - * The legacy raw-socket upgrade API cannot identify an HTTP/2 stream. - * Keep the symbol for source compatibility and fail explicitly instead - * of emitting an invalid connection-level upgrade response. - */ return cHTTPX_ERR_UNAVAILABLE; } diff --git a/src/cHTTPX_websocket.h b/src/cHTTPX_websocket.h index b643b86..a101994 100644 --- a/src/cHTTPX_websocket.h +++ b/src/cHTTPX_websocket.h @@ -16,6 +16,7 @@ #define CHTTPX_WSOCKET_OPCODE_PING 0x9 #define CHTTPX_WSOCKET_OPCODE_PONG 0xA +/** Parsed WebSocket frame header and payload view. */ typedef struct { /* FIN - final fragment @@ -34,20 +35,53 @@ typedef struct unsigned char* payload; } wsocket_frame_t; +/** Connected WebSocket client handle. */ typedef struct { int socket; int connected; } chttpx_wsocket_t; +/** Callback invoked when a WebSocket message payload is received. */ typedef void (*chttpx_wsocket_handler_t)(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); +/** Route handler invoked after a successful WebSocket upgrade. */ typedef void (*chttpx_wsocket_route_t)(chttpx_wsocket_t* wsocket); +/** + * Register a WebSocket route on a router. + * + * @param r Router that owns the route. + * @param path URL path template. + * @param handler Handler invoked after upgrade. + */ void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, chttpx_wsocket_route_t handler); +/** + * Perform the HTTP Upgrade handshake for WebSocket (legacy API). + * + * @param client_socket Connected client socket. + * @param sec_wsocket_key Sec-WebSocket-Key header value. + * @return cHTTPX_OK on success or a negative error code. + */ int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); +/** + * Send one WebSocket frame payload to the client. + * + * @param wsocket Connected WebSocket handle. + * @param data Payload bytes. + * @param len Payload length in bytes. + * @return cHTTPX_OK on success or a negative error code. + */ int cHTTPX_WSocketSend(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); -int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len); \ No newline at end of file +/** + * Receive WebSocket payload bytes into a caller buffer. + * + * @param wsocket Connected WebSocket handle. + * @param buffer Output buffer. + * @param len Maximum bytes to read. + * @return Number of bytes read or a negative error code. + */ +int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len); diff --git a/src/cHTTPX_worker.c b/src/cHTTPX_worker.c index 32b2136..2fa2065 100644 --- a/src/cHTTPX_worker.c +++ b/src/cHTTPX_worker.c @@ -7,6 +7,9 @@ #include #include +/** + * Queued work item with enqueue timestamp. + */ typedef struct { void* data; @@ -42,6 +45,11 @@ struct chttpx_worker_pool #endif }; +/** + * Return a monotonic timestamp in nanoseconds. + * + * @return Monotonic timestamp in nanoseconds, or zero if unavailable. + */ static uint64_t monotonic_ns(void) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -56,6 +64,11 @@ static uint64_t monotonic_ns(void) #endif } +/** + * Acquire the worker pool mutex. + * + * @param pool Worker pool to operate on. + */ static void pool_lock(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -65,6 +78,11 @@ static void pool_lock(chttpx_worker_pool_t* pool) #endif } +/** + * Release the worker pool mutex. + * + * @param pool Worker pool to operate on. + */ static void pool_unlock(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -74,6 +92,11 @@ static void pool_unlock(chttpx_worker_pool_t* pool) #endif } +/** + * Wait on the worker pool condition variable. + * + * @param pool Worker pool to operate on. + */ static void pool_wait(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -83,6 +106,11 @@ static void pool_wait(chttpx_worker_pool_t* pool) #endif } +/** + * Wake one worker thread. + * + * @param pool Worker pool to operate on. + */ static void pool_signal(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -92,6 +120,11 @@ static void pool_signal(chttpx_worker_pool_t* pool) #endif } +/** + * Wake all worker threads. + * + * @param pool Worker pool to operate on. + */ static void pool_broadcast(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -101,6 +134,12 @@ static void pool_broadcast(chttpx_worker_pool_t* pool) #endif } +/** + * Initialize worker pool mutex and condition variable. + * + * @param pool Worker pool to operate on. + * @return Zero on success or a negative error code. + */ static int pool_sync_init(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -121,6 +160,11 @@ static int pool_sync_init(chttpx_worker_pool_t* pool) #endif } +/** + * Destroy worker pool synchronization objects. + * + * @param pool Worker pool to operate on. + */ static void pool_sync_destroy(chttpx_worker_pool_t* pool) { #ifdef CHTTPX_PLATFORM_WINDOWS @@ -131,6 +175,13 @@ static void pool_sync_destroy(chttpx_worker_pool_t* pool) #endif } +/** + * Dequeue the next job or return false when stopping. + * + * @param pool Worker pool to operate on. + * @param job Output job removed from the queue head. + * @return True when a job was dequeued, false when the pool is stopping. + */ static bool worker_take(chttpx_worker_pool_t* pool, chttpx_worker_job_t* job) { pool_lock(pool); @@ -158,6 +209,12 @@ static bool worker_take(chttpx_worker_pool_t* pool, chttpx_worker_job_t* job) return true; } +/** + * Worker thread main loop. + * + * @param argument Worker pool handle passed to the thread entry. + * @return Always NULL; required by the thread API. + */ static void* worker_main(void* argument) { chttpx_worker_pool_t* pool = argument; @@ -183,10 +240,16 @@ static void* worker_main(void* argument) return NULL; } -int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, - size_t queue_capacity, - chttpx_worker_execute_fn execute, - void* context) +/** + * Create a fixed-size worker thread pool with a bounded job queue. + * + * @param pool Out pointer receiving the created worker pool. + * @param queue_capacity Maximum queued jobs before submit rejects. + * @param execute Callback invoked for each dequeued job. + * @param context User context forwarded to the execute callback. + * @return Zero on success or -1 on failure. + */ +int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, size_t queue_capacity, chttpx_worker_execute_fn execute, void* context) { if (!pool || !queue_capacity || !execute) return -1; @@ -237,6 +300,13 @@ int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, return 0; } +/** + * Enqueue one job; returns false when stopping or the queue is full. + * + * @param pool Worker pool to operate on. + * @param job Opaque job pointer to enqueue. + * @return True when the job was queued, false when stopping or the queue is full. + */ bool _chttpx_worker_submit(chttpx_worker_pool_t* pool, void* job) { if (!pool || !job) @@ -261,6 +331,11 @@ bool _chttpx_worker_submit(chttpx_worker_pool_t* pool, void* job) return true; } +/** + * Signal worker threads to drain the queue and exit. + * + * @param pool Worker pool to operate on. + */ void _chttpx_worker_stop(chttpx_worker_pool_t* pool) { if (!pool) @@ -272,6 +347,11 @@ void _chttpx_worker_stop(chttpx_worker_pool_t* pool) pool_unlock(pool); } +/** + * Stop workers, join threads, and free pool resources. + * + * @param pool Worker pool to operate on. + */ void _chttpx_worker_pool_destroy(chttpx_worker_pool_t* pool) { if (!pool) @@ -287,6 +367,12 @@ void _chttpx_worker_pool_destroy(chttpx_worker_pool_t* pool) free(pool); } +/** + * Snapshot queue depth and worker counters. + * + * @param pool Worker pool to operate on. + * @param stats Output buffer receiving worker statistics. + */ void _chttpx_worker_stats(chttpx_worker_pool_t* pool, chttpx_worker_stats_t* stats) { if (!stats) diff --git a/src/cHTTPX_worker.h b/src/cHTTPX_worker.h index 1de6b79..364ee47 100644 --- a/src/cHTTPX_worker.h +++ b/src/cHTTPX_worker.h @@ -7,9 +7,22 @@ #define CHTTPX_WORKER_THREADS 32 +/** + * Opaque worker thread pool handle. + */ typedef struct chttpx_worker_pool chttpx_worker_pool_t; + +/** + * Worker pool job callback. + * + * @param job Opaque job pointer submitted to the pool. + * @param context User context registered at pool creation. + */ typedef void (*chttpx_worker_execute_fn)(void* job, void* context); +/** + * Snapshot of worker pool queue and counters. + */ typedef struct { size_t queue_depth; @@ -19,13 +32,23 @@ typedef struct uint64_t total_queue_wait_ns; } chttpx_worker_stats_t; -int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, - size_t queue_capacity, - chttpx_worker_execute_fn execute, - void* context); +/** + * Create a worker pool. + * + * @return Zero on success or -1 on failure. + */ +int _chttpx_worker_pool_create(chttpx_worker_pool_t** pool, size_t queue_capacity, chttpx_worker_execute_fn execute, void* context); + +/** Enqueue one job. @return False when stopping or the queue is full. */ bool _chttpx_worker_submit(chttpx_worker_pool_t* pool, void* job); + +/** Signal workers to stop after draining the queue. */ void _chttpx_worker_stop(chttpx_worker_pool_t* pool); + +/** Stop workers, join threads, and free the pool. */ void _chttpx_worker_pool_destroy(chttpx_worker_pool_t* pool); + +/** Snapshot queue depth and worker counters. */ void _chttpx_worker_stats(chttpx_worker_pool_t* pool, chttpx_worker_stats_t* stats); #endif diff --git a/tests/fuzz_headers.c b/tests/fuzz_headers.c index 740a750..5c0bde1 100644 --- a/tests/fuzz_headers.c +++ b/tests/fuzz_headers.c @@ -4,6 +4,13 @@ #include #include +/** + * libFuzzer entry: feeds arbitrary bytes into _parse_req_headers. + * + * @param data Fuzz input buffer. + * @param size Length of data. + * @return Always 0 (no test assertions). + */ int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (!data || size == 0 || size > 32768) diff --git a/tests/test_compression.c b/tests/test_compression.c index bf93ced..b780bf6 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -22,6 +22,7 @@ typedef struct static unsigned char large_text[LARGE_BODY_SIZE]; static unsigned char binary_body[LARGE_BODY_SIZE]; +/** Fills repeatable large_text and binary_body buffers. */ static void fill_bodies(void) { for (size_t i = 0; i < LARGE_BODY_SIZE; i++) @@ -31,24 +32,28 @@ static void fill_bodies(void) } } +/** Returns a large text/plain body. */ static void large_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "text/plain; charset=utf-8", large_text, sizeof(large_text)); } +/** Returns a body below the compression min_size threshold. */ static void small_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "small"); } +/** Returns a non-text binary body (image/png). */ static void binary_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "image/png", binary_body, sizeof(binary_body)); } +/** Large response with cHTTPX_ResponseCompression(res, false). */ static void response_disabled_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -56,6 +61,7 @@ static void response_disabled_handler(chttpx_request_t* req, chttpx_response_t* cHTTPX_ResponseCompression(res, false); } +/** Response that already sets Content-Encoding: br. */ static void already_encoded_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -63,6 +69,7 @@ static void already_encoded_handler(chttpx_request_t* req, chttpx_response_t* re assert(cHTTPX_HeaderAdd(res, "Content-Encoding", "br") == 0); } +/** Response with Cache-Control: no-transform (must not compress). */ static void no_transform_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -70,6 +77,7 @@ static void no_transform_handler(chttpx_request_t* req, chttpx_response_t* res) assert(cHTTPX_HeaderAdd(res, "Cache-Control", "public, no-transform") == 0); } +/** Polls until the server listening flag is set (integration tests). */ static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) @@ -84,6 +92,7 @@ static void wait_until_listening(chttpx_serv_t* server) assert(__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)); } +/** Substring search over a byte buffer (test helper). */ static const unsigned char* find_bytes(const unsigned char* haystack, size_t haystack_size, const char* needle) { size_t needle_size = strlen(needle); @@ -97,6 +106,7 @@ static const unsigned char* find_bytes(const unsigned char* haystack, size_t hay return NULL; } +/** Sends one HTTP/2 request via _chttpx_http2_call and formats the raw response. */ static test_response_t exchange(uint16_t port, const char* request) { test_response_t response; @@ -177,27 +187,32 @@ static test_response_t exchange(uint16_t port, const char* request) return response; } +/** @return Payload size excluding the synthesized status/header block. */ static size_t response_body_size(const test_response_t* response) { assert(response && response->size >= response->header_size); return response->size - response->header_size; } +/** @return Pointer to the response payload bytes. */ static const unsigned char* response_body(const test_response_t* response) { return response->bytes + response->header_size; } +/** Asserts a substring appears in the formatted header block. */ static void assert_header(const test_response_t* response, const char* text) { assert(strstr(response->headers, text) != NULL); } +/** Asserts a substring is absent from the formatted header block. */ static void assert_no_header(const test_response_t* response, const char* text) { assert(strstr(response->headers, text) == NULL); } +/** Expects an uncompressed 200 with the full large_text body. */ static void assert_identity_large(const test_response_t* response) { assert_header(response, "HTTP/2 200 OK"); @@ -206,6 +221,7 @@ static void assert_identity_large(const test_response_t* response) assert(memcmp(response_body(response), large_text, sizeof(large_text)) == 0); } +/** Expects gzip Content-Encoding and a body that inflates to large_text. */ static void assert_gzip_large(const test_response_t* response) { assert_header(response, "HTTP/2 200 OK"); @@ -232,12 +248,9 @@ static void assert_gzip_large(const test_response_t* response) assert(memcmp(decompressed, large_text, sizeof(large_text)) == 0); } -static int failing_provider(const unsigned char* input, - size_t input_size, - int level, - unsigned char** output, - size_t* output_size, - void* user_data) +/** Compression provider that always returns cHTTPX_ERR_COMPRESSION. */ +static int failing_provider(const unsigned char* input, size_t input_size, int level, unsigned char** output, + size_t* output_size, void* user_data) { (void)input; (void)input_size; @@ -248,6 +261,7 @@ static int failing_provider(const unsigned char* input, return cHTTPX_ERR_COMPRESSION; } +/** Integration tests for Accept-Encoding negotiation and gzip responses. */ int main(void) { fill_bodies(); @@ -280,74 +294,55 @@ int main(void) assert(cHTTPX_AppStart(&app) == cHTTPX_OK); wait_until_listening(server); - test_response_t response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + test_response_t response = + exchange(server->port, "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_gzip_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0.5, identity;q=1\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0.5, identity;q=1\r\n\r\n"); assert_identity_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: br;q=1, gzip;q=0.8, identity;q=0.2\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: br;q=1, gzip;q=0.8, identity;q=0.2\r\n\r\n"); assert_gzip_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: GZIP ; q=1.0, identity;q=0\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: GZIP ; q=1.0, identity;q=0\r\n\r\n"); assert_gzip_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: *;q=1, identity;q=0\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: *;q=1, identity;q=0\r\n\r\n"); assert_gzip_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0\r\n\r\n"); + response = exchange(server->port, "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0\r\n\r\n"); assert_identity_large(&response); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0, identity;q=0\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=0, identity;q=0\r\n\r\n"); assert_header(&response, "HTTP/2 406 Not Acceptable"); assert(response_body_size(&response) == 0); - response = exchange( - server->port, - "GET /small HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = exchange(server->port, "GET /small HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_header(&response, "HTTP/2 200 OK"); assert_no_header(&response, "content-encoding:"); - response = exchange( - server->port, - "GET /binary HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = exchange(server->port, "GET /binary HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_header(&response, "HTTP/2 200 OK"); assert_no_header(&response, "content-encoding:"); assert(response_body_size(&response) == sizeof(binary_body)); - response = exchange( - server->port, - "GET /response-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = + exchange(server->port, "GET /response-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_identity_large(&response); - response = exchange( - server->port, - "GET /route-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = exchange(server->port, "GET /route-disabled HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_identity_large(&response); - response = exchange( - server->port, - "GET /encoded HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = exchange(server->port, "GET /encoded HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_header(&response, "content-encoding: br"); assert_no_header(&response, "content-encoding: gzip"); - response = exchange( - server->port, - "GET /no-transform HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); + response = exchange(server->port, "GET /no-transform HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n"); assert_header(&response, "cache-control: public, no-transform"); assert_no_header(&response, "content-encoding:"); @@ -362,9 +357,8 @@ int main(void) failing_config.providers_count = 1; assert(cHTTPX_CompressionUse(server, &failing_config) == cHTTPX_OK); - response = exchange( - server->port, - "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=1, identity;q=0.5\r\n\r\n"); + response = exchange(server->port, + "GET /large HTTP/2\r\nHost: localhost\r\nAccept-Encoding: gzip;q=1, identity;q=0.5\r\n\r\n"); assert_identity_large(&response); cHTTPX_AppShutdown(&app); diff --git a/tests/test_core.c b/tests/test_core.c index b45a741..207885b 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -8,12 +8,14 @@ static int cleanup_calls; +/** @param value Pointer to an int; value is summed into cleanup_calls before free. */ static void count_cleanup(void* value) { cleanup_calls += *(int*)value; free(value); } +/** Custom validator used by test_bind_and_json; accepts only "valid_user". */ static bool username_validator(const void* value, char* error, size_t error_size) { if (strcmp(value, "valid_user") == 0) @@ -22,6 +24,7 @@ static bool username_validator(const void* value, char* error, size_t error_size return false; } +/** No-op middleware that always continues the chain. */ static chttpx_middleware_result_t middleware(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -29,12 +32,14 @@ static chttpx_middleware_result_t middleware(chttpx_request_t* req, chttpx_respo return next; } +/** Returns HTTP 204 No Content. */ static void handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResNoContent(); } +/** Verifies alloc, defer, context, and cleanup callback ordering. */ static void test_request_lifecycle(void) { cleanup_calls = 0; @@ -55,6 +60,7 @@ static void test_request_lifecycle(void) assert(cleanup_calls == 5); } +/** Stresses context and defer registries beyond initial hash capacity. */ static void test_request_internal_indexes(void) { chttpx_request_t req = {0}; @@ -96,6 +102,7 @@ static void test_request_internal_indexes(void) cHTTPX_RequestCleanup(&req); } +/** Covers route params, query parsing, and typed query accessors. */ static void test_typed_values(void) { chttpx_request_t req = {0}; @@ -123,6 +130,7 @@ static void test_typed_values(void) free(req.query); } +/** Covers BindJSON, JsonObject, and JSON escaping in responses. */ static void test_bind_and_json(void) { chttpx_request_t req = {0}; @@ -151,6 +159,7 @@ static void test_bind_and_json(void) cHTTPX_RequestCleanup(&req); } +/** Parses a small in-memory multipart form and temp file lifecycle. */ static void test_multipart(void) { chttpx_request_t req = {0}; @@ -175,7 +184,7 @@ static void test_multipart(void) assert(fopen(path, "rb") == NULL); } - +/** Ensures header parsing stops at the header/body boundary. */ static void test_header_boundary(void) { chttpx_request_t req = {0}; @@ -191,6 +200,7 @@ static void test_header_boundary(void) assert(cHTTPX_HeaderGet(&req, "Fake") == NULL); } +/** Rejects duplicate Content-Length and unsupported Transfer-Encoding on body parse. */ static void test_request_framing(void) { chttpx_serv_t server = {0}; @@ -220,6 +230,7 @@ static void test_request_framing(void) } +/** Chunked raw octet-stream upload via disk-backed stream. */ static void test_streamed_raw_chunked_upload(void) { chttpx_serv_t server = {0}; @@ -264,6 +275,7 @@ static void test_streamed_raw_chunked_upload(void) assert(fopen(path, "rb") == NULL); } +/** Multipart upload with Content-Length and chunked framing. */ static void test_streamed_multipart(void) { chttpx_serv_t server = {0}; @@ -341,6 +353,7 @@ static void test_streamed_multipart(void) cHTTPX_RequestCleanup(&req); } +/** Frees routes registered on a stack-allocated test server. */ static void free_test_routes(chttpx_serv_t* server) { for (size_t i = 0; i < server->routes_count; i++) @@ -364,6 +377,7 @@ static void free_test_routes(chttpx_serv_t* server) server->routes_capacity = 0; } +/** Router groups, middleware stacks, and upload policy snapshotting. */ static void test_routing_api(void) { chttpx_serv_t server = {0}; @@ -397,6 +411,7 @@ static void test_routing_api(void) free_test_routes(&server); } +/** Status reasons, MIME helpers, Bearer token, and ResMessage escaping. */ static void test_helpers(void) { assert(strcmp(cHTTPX_StatusReason(404), "Not Found") == 0); @@ -412,6 +427,7 @@ static void test_helpers(void) cHTTPX_ResponseCleanup(&response); } +/** Runs core unit tests for request parsing, binding, and routing helpers. */ int main(void) { test_request_lifecycle(); diff --git a/tests/test_metrics.c b/tests/test_metrics.c index 54a2574..5d4fdca 100644 --- a/tests/test_metrics.c +++ b/tests/test_metrics.c @@ -32,6 +32,7 @@ typedef struct size_t header_size; } http_response_t; +/** Parameterized route handler for metrics cardinality tests. */ static void user_handler(chttpx_request_t* req, chttpx_response_t* res) { const char* id = cHTTPX_Param(req, "id"); @@ -40,6 +41,7 @@ static void user_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "text/plain", body, sizeof(body) - 1); } +/** Fixed route handler registered for many /route/{i} paths. */ static void static_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -47,6 +49,7 @@ static void static_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResBinary(cHTTPX_StatusOK, "text/plain", body, sizeof(body) - 1); } +/** Polls until server->listening is true. */ static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) @@ -55,12 +58,14 @@ static void wait_until_listening(chttpx_serv_t* server) assert(__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)); } +/** @return Pointer to the response body inside a formatted http_response_t. */ static const char* response_body(http_response_t* response) { assert(response && response->header_size <= response->size); return response->bytes + response->header_size; } +/** Issues GET path against port via _chttpx_http2_call. */ static http_response_t exchange(uint16_t port, const char* path) { http_response_t response; @@ -86,6 +91,7 @@ static http_response_t exchange(uint16_t port, const char* path) return response; } +/** Concurrent client hammering /users/{id} routes. */ static void* worker(void* data) { worker_ctx_t* ctx = data; @@ -99,6 +105,7 @@ static void* worker(void* data) return NULL; } +/** Background thread reading metrics snapshots until stopped. */ static void* snapshot_reader(void* data) { snapshot_ctx_t* ctx = data; @@ -111,12 +118,14 @@ static void* snapshot_reader(void* data) return NULL; } +/** @return Elapsed wall time between two CLOCK_MONOTONIC samples. */ static double diff_seconds(struct timespec start, struct timespec end) { return (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1000000000.0; } +/** Validates counters, histograms, Prometheus scrape output, and snapshot speed. */ int main(void) { chttpx_app_t app; diff --git a/tests/test_server.c b/tests/test_server.c index a8b9808..52574b3 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -16,6 +16,7 @@ static uint16_t public_port; static uint16_t internal_port; static int last_remote_call_result; +/** Records body, language, and request id; returns 200 "done". */ static void request_handler(chttpx_request_t* req, chttpx_response_t* res) { __atomic_fetch_add(&handler_calls, 1, __ATOMIC_SEQ_CST); @@ -25,12 +26,14 @@ static void request_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "done"); } +/** Captures client_ip and returns 204. */ static void ip_handler(chttpx_request_t* req, chttpx_response_t* res) { snprintf(observed_client_ip, sizeof(observed_client_ip), "%s", req->client_ip); *res = cHTTPX_ResNoContent(); } +/** OPTIONS handler for CORS preflight bypass tests. */ static void options_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; @@ -38,25 +41,29 @@ static void options_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResNoContent(); } +/** Intentionally leaves res unset to trigger a 500. */ static void empty_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; (void)res; } +/** Internal target for cHTTPX_Call; echoes accepted JSON with observed body. */ static void internal_handler(chttpx_request_t* req, chttpx_response_t* res) { - snprintf(internal_observed_body, sizeof(internal_observed_body), "%.*s", (int)req->body_size, - req->body ? (const char*)req->body : ""); + snprintf(internal_observed_body, sizeof(internal_observed_body), "%.*s", + (int)req->body_size, req->body ? (const char*)req->body : ""); *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"server\":\"internal\",\"accepted\":true}"); } +/** Local cHTTPX_Call proxy to the "internal" server. */ static void proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { if (cHTTPX_Call(req, "internal", cHTTPX_MethodPost, "/process", res) != cHTTPX_OK) *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "internal server call failed"); } +/** Proxies to AppRemote "remote-payments". */ static void remote_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "remote-payments", cHTTPX_MethodPost, "/process", res); @@ -64,12 +71,14 @@ static void remote_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "remote server call failed"); } +/** Remote server handler that always returns 500. */ static void remote_error_handler(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "remote error"); } +/** Proxies to remote /error and maps failures to 502. */ static void remote_error_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "remote-payments", cHTTPX_MethodPost, "/error", res); @@ -77,6 +86,7 @@ static void remote_error_proxy_handler(chttpx_request_t* req, chttpx_response_t* *res = cHTTPX_ResError(cHTTPX_StatusBadGateway, "remote server call failed"); } +/** Maps cHTTPX_ERR_UNAVAILABLE from Call to 503. */ static void unavailable_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { last_remote_call_result = cHTTPX_Call(req, "unavailable", cHTTPX_MethodPost, "/process", res); @@ -89,6 +99,7 @@ static void unavailable_proxy_handler(chttpx_request_t* req, chttpx_response_t* *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "unexpected call result"); } +/** cHTTPX_CallEx with a custom JSON body override. */ static void custom_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) { static const char custom_body[] = "{\"custom\":true}"; @@ -102,6 +113,7 @@ static void custom_proxy_handler(chttpx_request_t* req, chttpx_response_t* res) *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "custom internal call failed"); } +/** Builds an HTTP/2 request, dispatches via _chttpx_http2_call, formats raw response. */ static void exchange_family(uint16_t port, int family, const char* request, char* response, size_t response_size) { assert(request && response && response_size > 0); @@ -186,16 +198,19 @@ static void exchange_family(uint16_t port, int family, const char* request, char cHTTPX_ResponseCleanup(&result); } +/** IPv4 loopback wrapper around exchange_family. */ static void exchange(uint16_t port, const char* request, char* response, size_t response_size) { exchange_family(port, AF_INET, request, response, response_size); } +/** IPv6 loopback wrapper around exchange_family. */ static void exchange_ipv6(uint16_t port, const char* request, char* response, size_t response_size) { exchange_family(port, AF_INET6, request, response, response_size); } +/** Opens a TCP connection left idle to exercise concurrency limits. */ static chttpx_socket_t open_idle_connection(uint16_t port) { chttpx_socket_t socket_fd = socket(AF_INET, SOCK_STREAM, 0); @@ -213,6 +228,7 @@ static chttpx_socket_t open_idle_connection(uint16_t port) } +/** Polls until server->listening is true. */ static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) @@ -227,6 +243,7 @@ static void wait_until_listening(chttpx_serv_t* server) assert(__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)); } +/** Multi-server App integration: CORS, Call, remote proxies, and limits. */ int main(void) { chttpx_app_t remote_app; @@ -323,58 +340,31 @@ int main(void) for (size_t i = 0; i < CHTTPX_ARRAY_LEN(idle_connections); i++) chttpx_close(idle_connections[i]); - exchange_ipv6(public_port, - "GET /ip HTTP/2\r\n" - "Host: localhost\r\n" - "\r\n", - response, sizeof(response)); + exchange_ipv6(public_port, "GET /ip HTTP/2\r\nHost: localhost\r\n\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(strcmp(observed_client_ip, "::1") == 0); - exchange(public_port, - "GET /ip HTTP/2\r\n" - "Host: localhost\r\n" - "\r\n", - response, sizeof(response)); + exchange(public_port, "GET /ip HTTP/2\r\nHost: localhost\r\n\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(strcmp(observed_client_ip, "127.0.0.1") == 0); exchange(public_port, - "OPTIONS /body HTTP/2\r\n" - "Host: localhost\r\n" - "Origin: https://example.com\r\n" - "Access-Control-Request-Method: POST\r\n" - "\r\n", + "OPTIONS /body HTTP/2\r\nHost: localhost\r\nOrigin: https://example.com\r\nAccess-Control-Request-Method: POST\r\n\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(strstr(response, "access-control-allow-origin: https://example.com") != NULL); assert(__atomic_load_n(&options_handler_calls, __ATOMIC_SEQ_CST) == 0); - exchange(public_port, - "OPTIONS /body HTTP/2\r\n" - "Host: localhost\r\n" - "\r\n", - response, sizeof(response)); + exchange(public_port, "OPTIONS /body HTTP/2\r\nHost: localhost\r\n\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/2 204 No Content") != NULL); assert(__atomic_load_n(&options_handler_calls, __ATOMIC_SEQ_CST) == 1); - exchange(public_port, - "GET /empty HTTP/2\r\n" - "Host: localhost\r\n" - "\r\n", - response, sizeof(response)); + exchange(public_port, "GET /empty HTTP/2\r\nHost: localhost\r\n\r\n", response, sizeof(response)); assert(strstr(response, "HTTP/2 500 Internal Server Error") != NULL); assert(strstr(response, "connection:") == NULL); exchange(public_port, - "POST /body HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: text/plain\r\n" - "Content-Length: 5\r\n" - "Accept-Language: en;q=0.2, ru-RU;q=0.9\r\n" - "X-Request-ID: integration-123\r\n" - "\r\n" - "hello", + "POST /body HTTP/2\r\nHost: localhost\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nAccept-Language: en;q=0.2, ru-RU;q=0.9\r\nX-Request-ID: integration-123\r\n\r\nhello", response, sizeof(response)); assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(observed_body, "hello") == 0); @@ -382,76 +372,43 @@ int main(void) assert(strcmp(observed_request_id, "integration-123") == 0); exchange(public_port, - "POST /proxy HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: application/json\r\n" - "Content-Length: 18\r\n" - "\r\n" - "{\"plan\":\"premium\"}", + "POST /proxy HTTP/2\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"plan\":\"premium\"}", response, sizeof(response)); assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strstr(response, "\"server\":\"internal\"") != NULL); assert(strcmp(internal_observed_body, "{\"plan\":\"premium\"}") == 0); exchange(public_port, - "POST /proxy-remote HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: application/json\r\n" - "Content-Length: 18\r\n" - "\r\n" - "{\"plan\":\"premium\"}", + "POST /proxy-remote HTTP/2\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"plan\":\"premium\"}", response, sizeof(response)); assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strstr(response, "\"server\":\"internal\"") != NULL); assert(last_remote_call_result == cHTTPX_OK); - exchange(public_port, - "POST /proxy-remote-error HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Length: 0\r\n" - "\r\n", - response, sizeof(response)); + exchange(public_port, "POST /proxy-remote-error HTTP/2\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n", response, + sizeof(response)); assert(strstr(response, "HTTP/2 500 Internal Server Error") != NULL); assert(last_remote_call_result == cHTTPX_OK); - exchange(public_port, - "POST /proxy-unavailable HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Length: 0\r\n" - "\r\n", - response, sizeof(response)); + exchange(public_port, "POST /proxy-unavailable HTTP/2\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n", response, + sizeof(response)); assert(strstr(response, "HTTP/2 503 Service Unavailable") != NULL); assert(last_remote_call_result == cHTTPX_ERR_UNAVAILABLE); exchange(public_port, - "POST /proxy-custom HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: application/json\r\n" - "Content-Length: 18\r\n" - "\r\n" - "{\"plan\":\"premium\"}", + "POST /proxy-custom HTTP/2\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"plan\":\"premium\"}", response, sizeof(response)); assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(internal_observed_body, "{\"custom\":true}") == 0); exchange(internal_port, - "POST /process HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: text/plain\r\n" - "Content-Length: 4\r\n" - "\r\n" - "ping", + "POST /process HTTP/2\r\nHost: localhost\r\nContent-Type: text/plain\r\nContent-Length: 4\r\n\r\nping", response, sizeof(response)); assert(strstr(response, "HTTP/2 200 OK") != NULL); assert(strcmp(internal_observed_body, "ping") == 0); exchange(public_port, - "POST /body HTTP/2\r\n" - "Host: localhost\r\n" - "Content-Type: application/json\r\n" - "Content-Length: 40\r\n" - "\r\n" - "0123456789012345678901234567890123456789", + "POST /body HTTP/2\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 40\r\n\r\n0123456789012345678901234567890123456789", response, sizeof(response)); assert(strstr(response, "HTTP/2 413 Payload Too Large") != NULL); diff --git a/tests/test_tls.c b/tests/test_tls.c index 2f3c504..480af6c 100644 --- a/tests/test_tls.c +++ b/tests/test_tls.c @@ -4,12 +4,14 @@ #include #include +/** TLS server health handler returning {"secure":true}. */ static void secure_health(chttpx_request_t* req, chttpx_response_t* res) { (void)req; *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"secure\":true}"); } +/** Verifies TLS verify failure and trusted CA success for AppRemote CallEx. */ int main(int argc, char** argv) { if (argc != 3) From 45f0a0e395444d04bd75d8087e5f20aa30fb4d55 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Fri, 25 Sep 2026 02:51:22 +0500 Subject: [PATCH 44/48] fix: correct TLS client certificate verification expression --- src/cHTTPX_tls.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index e3e7095..b542219 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -193,9 +193,7 @@ int _chttpx_tls_server_init(chttpx_serv_t* server, const chttpx_tls_config_t* co if (names) SSL_CTX_set_client_CA_list(ctx, names); - SSL_CTX_set_verify(ctx, server->tls.require_client_cert, ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) - : SSL_VERIFY_PEER, - NULL); + SSL_CTX_set_verify(ctx, server->tls.require_client_cert ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : SSL_VERIFY_PEER, NULL); } else if (server->tls.require_client_cert) { From 18f49edbd61978a2e5ffc69c31316807b6bf1b9e Mon Sep 17 00:00:00 2001 From: nenishyi Date: Sun, 27 Sep 2026 02:14:58 +0500 Subject: [PATCH 45/48] refactor: drop Windows support (#64) * refactor: make platform layer Linux-only * refactor: use pthreads only * build: remove Windows targets * ci: remove Windows build job * build: remove Windows installer * build: remove Windows bundled cJSON source * build: remove Windows bundled cJSON header * docs: mark libchttpx as Linux-only * docs: mark libchttpx as Linux-only * release: remove Windows installation instructions * release: remove Windows installation instructions * docs: update website platform support * docs: update website platform support --- .github/workflows/auto-release.yml | 13 +- .github/workflows/ci.yml | 28 - .github/workflows/release.yml | 11 +- Makefile | 69 +- README.md | 32 +- README_RU.md | 33 +- docs/site/about.html | 2 +- docs/site/index.html | 8 +- lib/cjson/cJSON.c | 3191 ---------------------------- lib/cjson/cJSON.h | 306 --- scripts/install.ps1 | 33 - src/cHTTPX_crosspltm.h | 155 +- src/cHTTPX_utils.h | 70 +- 13 files changed, 58 insertions(+), 3893 deletions(-) delete mode 100644 lib/cjson/cJSON.c delete mode 100644 lib/cjson/cJSON.h delete mode 100644 scripts/install.ps1 diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 301ca38..a19fb54 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -150,13 +150,11 @@ jobs:

golangci-lint logo

netcorelink/libchttpx

-

A powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers

+

A powerful HTTP/2 server library in C/C++ for Linux

--- - > :warning: **Warning:** Use the `linux environment` for stable library operation. - ## Linux native packages Download the package for your distribution from this release, then install it with the system package manager: @@ -172,7 +170,7 @@ jobs: sudo dnf install ./libchttpx-dev-*.rpm # Arch Linux - sudo pacman -U ./libchttpx-dev-*.pkg.tar.zst + sudo pacman -U ./libchttpx-dev_*.pkg.tar.zst # Alpine Linux sudo apk add --allow-untrusted ./libchttpx-dev_*.apk @@ -184,12 +182,6 @@ jobs: curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh ``` - ## Windows - - ```powershell - iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex - ``` - - name: Checkout APT repository uses: actions/checkout@v4 with: @@ -264,4 +256,3 @@ jobs: run: | set -euo pipefail gh workflow run pages.yml --ref main - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d04999..2c733ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,31 +189,3 @@ jobs: run: | make clean make TLS=1 libchttpx.so - - build-windows: - name: Build (Windows) - needs: style - runs-on: windows-latest - defaults: - run: - shell: msys2 {0} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - update: true - install: >- - mingw-w64-x86_64-gcc - mingw-w64-x86_64-make - mingw-w64-x86_64-zlib - mingw-w64-x86_64-nghttp2 - - - name: Build - run: mingw32-make win SHELL=cmd - - - name: Run tests - run: mingw32-make test-win SHELL=cmd diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 579324c..df4e405 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,13 +53,11 @@ jobs:

golangci-lint logo

netcorelink/libchttpx

-

A powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers

+

A powerful HTTP/2 server library in C/C++ for Linux

--- - > :warning: **Warning:** Use the `linux environment` for stable library operation. - ## Linux native packages Download the package for your distribution from this release, then install it with the system package manager: @@ -87,12 +85,6 @@ jobs: curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh ``` - ## Windows - - ```powershell - iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex - ``` - - name: Checkout APT repository uses: actions/checkout@v4 with: @@ -167,4 +159,3 @@ jobs: run: | set -euo pipefail gh workflow run pages.yml --ref main - diff --git a/Makefile b/Makefile index 7af7b81..35effab 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,6 @@ TLS_LDFLAGS += -lssl -lcrypto endif CFLAGS += $(TLS_CFLAGS) -TARGET_DLL = libchttpx.dll CLANG_FORMAT = clang-format FUZZ_CC ?= clang @@ -28,10 +27,7 @@ PREFIX ?= /usr/local DESTDIR ?= pkg PKGDIR ?= /pkg/usr/local -WIN_LIB_DIR = tools - LIN_LDFLAGS = -lcjson -lz -lnghttp2 $(TLS_LDFLAGS) -WIN_LDFLAGS = -lws2_32 -lz -lnghttp2 $(TLS_LDFLAGS) TEST_TARGET = $(BINDIR)/test_core TEST_SERVER_TARGET = $(BINDIR)/test_server TEST_SANITIZE_TARGET = $(BINDIR)/test_core_sanitize @@ -49,12 +45,10 @@ EXAMPLE_TARGETS = $(addprefix $(BINDIR)/example-,$(EXAMPLE_NAMES)) LIN_SRCS = $(wildcard src/*.c) FORMAT_SRCS = $(wildcard src/*.c src/*.h include/*.h example/*.c tests/*.c) -WIN_SRCS = $(wildcard src/*.c) lib/cjson/cJSON.c LIN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(LIN_SRCS)) -WIN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(WIN_SRCS)) -# LINux build +# Linux build # - lin: $(BINDIR)/$(TARGET) @@ -81,20 +75,13 @@ examples-tls: examples-compression: $(BINDIR)/example-compression -# WINdows build -# - - -win: - if not exist $(BINDIR) mkdir $(BINDIR) - $(CC) $(CFLAGS) -D_WIN32 -mconsole $(EXAMPLE_SRC) $(WIN_SRCS) -o $(BINDIR)/$(TARGET).exe $(WIN_LDFLAGS) - -# LINux shared library +# Linux shared library # - libchttpx.so: $(LIN_OBJS) $(CC) -shared -fPIC -o libchttpx.so $(LIN_OBJS) $(LIN_LDFLAGS) -pthread -# LINux lib install +# Linux lib install # - # before execution, you must run `make libchttpx.so` @@ -110,32 +97,7 @@ lib-install: libchttpx.so cp libchttpx.pc $(DESTDIR)$(PREFIX)/lib/pkgconfig/libchttpx.pc; \ fi -# WINdows lib compile -# - - -win-lib: - @echo "Building Windows DLL..." - if not exist $(BINDIR) mkdir $(BINDIR) - $(CC) $(CFLAGS) -std=c11 -D_WIN32 -shared -o $(BINDIR)/$(TARGET_DLL) $(WIN_SRCS) -Wl,--out-implib,$(BINDIR)/libchttpx.a $(WIN_LDFLAGS) - - @echo "Copying files to $(WIN_LIB_DIR)..." - if not exist $(WIN_LIB_DIR) mkdir $(WIN_LIB_DIR) - copy /Y $(BINDIR)\$(TARGET_DLL) $(WIN_LIB_DIR)\$(TARGET_DLL) - copy /Y $(BINDIR)\libchttpx.a $(WIN_LIB_DIR)\libchttpx.a - for /f "delims=" %%i in ('where zlib1.dll 2^>nul') do copy /Y "%%i" $(WIN_LIB_DIR)\zlib1.dll - if not exist $(WIN_LIB_DIR)\include mkdir $(WIN_LIB_DIR)\include - xcopy /E /I /Y include $(WIN_LIB_DIR)\include - - @echo "Files copied to $(WIN_LIB_DIR) successfully!" - -test-win: - if not exist $(BINDIR) mkdir $(BINDIR) - $(CC) $(CFLAGS) -std=c11 -D_WIN32 tests/test_core.c src/*.c lib/cjson/cJSON.c -Wl,--stack,8388608 -o $(TEST_TARGET).exe $(WIN_LDFLAGS) - $(TEST_TARGET).exe - $(CC) $(CFLAGS) -std=c11 -D_WIN32 tests/test_server.c src/*.c lib/cjson/cJSON.c -Wl,--stack,8388608 -o $(TEST_SERVER_TARGET).exe $(WIN_LDFLAGS) - $(TEST_SERVER_TARGET).exe - -# LINux tests +# Linux tests # - test: $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) @@ -194,7 +156,7 @@ $(TEST_METRICS_TARGET): tests/test_metrics.c $(LIN_SRCS) @mkdir -p $(BINDIR) $(CC) $(CFLAGS) -std=gnu11 -O2 -g tests/test_metrics.c $(LIN_SRCS) -o $@ $(LIN_LDFLAGS) -pthread -# LINux lib compile +# Linux lib compile # - lin-lib: clean libchttpx.so @@ -219,19 +181,13 @@ lin-lib: clean libchttpx.so @echo "Release package created: $(TAR)" -# LINux run +# Linux run # - lin-run: lin $(BINDIR)/$(TARGET) -# WINdows run -# - - -win-run: win - $(BINDIR)\$(TARGET).exe - -# LINux format +# Linux format # - lin-format: @@ -242,16 +198,9 @@ format-check: @echo ">> Checking clang-format" $(CLANG_FORMAT) --dry-run --Werror $(FORMAT_SRCS) -# WINdows format -# - - -win-format: - @echo ">> Formatting clang source files" - $(CLANG_FORMAT) -i $(WIN_SRCS) - -run: run-lin +run: lin-run clean: - rm -rf $(OBJDIR) $(BINDIR) *.a *.dll + rm -rf $(OBJDIR) $(BINDIR) *.a rm -rf $(RELEASE_DIR) rm -rf libchttpx.so diff --git a/README.md b/README.md index 574b948..03b184c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # libchttpx -`libchttpx` is a compact cross-platform HTTP/2 server library for C. It provides an App-based runtime, routing, middleware, request parsing, JSON binding and responses, uploads, request-scoped memory, CORS, cookies, i18n, logging, rate limiting, and graceful shutdown while keeping a direct C-style API. +`libchttpx` is a compact HTTP/2 server library for C on Linux. It provides an App-based runtime, routing, middleware, request parsing, JSON binding and responses, uploads, request-scoped memory, CORS, cookies, i18n, logging, rate limiting, and graceful shutdown while keeping a direct C-style API. The library is designed so handlers contain application logic instead of repetitive HTTP plumbing. ## Highlights -- Linux and Windows support +- Linux support - multiple independent HTTP servers inside one `cHTTPX_App` - local direct server-to-server calls and remote HTTP/HTTPS calls - route groups and `{parameter}` paths @@ -89,24 +89,14 @@ sudo apk add --allow-untrusted ./libchttpx-dev_*.apk Download the package for your distribution from [GitHub Releases](https://github.com/netcorelink/libchttpx/releases). -### Legacy installer scripts +### Legacy installer script -The existing shell and PowerShell installers are kept as compatibility fallbacks. - -Linux: +The existing shell installer is kept as a compatibility fallback: ```bash curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh ``` -Windows: - -```powershell -iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex -``` - -Restart the terminal after the Windows installation so environment changes are visible. - ### Docker Pull the published runtime image: @@ -164,20 +154,6 @@ make test-compression TLS remains independently optional: `make TLS=1 libchttpx.so`. See [Response compression](docs/compression/README.md). -### Build from source on Windows - -Use MinGW/GCC. The Windows build uses the bundled `lib/cjson` source and requires zlib and nghttp2 (for MSYS2/MinGW64: `mingw-w64-x86_64-zlib` and `mingw-w64-x86_64-nghttp2`). - -```powershell -git clone https://github.com/netcorelink/libchttpx.git -cd libchttpx - -make win-lib -make test-win -``` - -The generated DLL/import library and headers are copied into `tools/`. - ## Documentation Detailed documentation is split by functionality: diff --git a/README_RU.md b/README_RU.md index dcbd52d..ffe3b60 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,12 +1,12 @@ # libchttpx -`libchttpx` — компактная кроссплатформенная HTTP/2-библиотека для C. Она предоставляет App-runtime, routing, middleware, разбор запросов, JSON binding/response, uploads, request-scoped память, CORS, cookies, i18n, logging, rate limiting и graceful shutdown, сохраняя простой C-style API. +`libchttpx` — компактная HTTP/2-библиотека для C под Linux. Она предоставляет App-runtime, routing, middleware, разбор запросов, JSON binding/response, uploads, request-scoped память, CORS, cookies, i18n, logging, rate limiting и graceful shutdown, сохраняя простой C-style API. Идея библиотеки: handler должен содержать бизнес-логику приложения, а не повторяющийся HTTP boilerplate. ## Основные возможности -- Linux и Windows +- поддержка Linux - несколько независимых HTTP-серверов внутри одного `cHTTPX_App` - прямые local-вызовы между серверами и remote-вызовы по HTTP/HTTPS - route groups и пути с `{parameter}` @@ -89,24 +89,14 @@ sudo apk add --allow-untrusted ./libchttpx-dev_*.apk Нужный пакет можно скачать из [GitHub Releases](https://github.com/netcorelink/libchttpx/releases). -### Старые install-скрипты +### Старый install-скрипт -Bash- и PowerShell-скрипты пока остаются как запасной способ установки. - -Linux: +Bash-скрипт остаётся как запасной способ установки: ```bash curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh ``` -Windows: - -```powershell -iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex -``` - -После установки в Windows перезапустите терминал. - ### Docker ```bash @@ -160,20 +150,6 @@ make test-compression TLS остаётся отдельной опцией сборки: `make TLS=1 libchttpx.so`. Подробнее: [Сжатие HTTP-ответов](docs/compression/README_RU.md). -### Самостоятельная сборка на Windows - -Используется MinGW/GCC. Для Windows cJSON уже находится в `lib/cjson`, а zlib и nghttp2 должны быть установлены (для MSYS2/MinGW64: `mingw-w64-x86_64-zlib` и `mingw-w64-x86_64-nghttp2`). - -```powershell -git clone https://github.com/netcorelink/libchttpx.git -cd libchttpx - -make win-lib -make test-win -``` - -DLL, import library и headers копируются в `tools/`. - ## Документация Подробная документация разделена по функционалу: @@ -198,7 +174,6 @@ DLL, import library и headers копируются в `tools/`. - [Rate limiting](docs/rate-limiting/README_RU.md) - [WebSocket API — experimental](docs/websocket/README_RU.md) - ## Лицензия BSD 3-Clause. См. [LICENSE](LICENSE). diff --git a/docs/site/about.html b/docs/site/about.html index eaa50a1..d42997f 100644 --- a/docs/site/about.html +++ b/docs/site/about.html @@ -51,7 +51,7 @@

Project goal

  • Written primarily in C
  • -
  • Cross-platform Linux and Windows support
  • +
  • Linux-only runtime and build
  • Public issue tracker and source history
  • BSD 3-Clause license
  • Examples included in the repository
  • diff --git a/docs/site/index.html b/docs/site/index.html index 67ee385..caf3551 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -4,7 +4,7 @@ libchttpx — HTTP servers in C - + @@ -34,14 +34,14 @@

    HTTP/2 server library for C

    Build HTTP servers in C

    - A compact library with routing, middleware, JSON helpers, uploads, + A compact Linux library with routing, middleware, JSON helpers, uploads, TLS, compression, metrics and graceful shutdown.

    -

    Open source · BSD 3-Clause · Linux and Windows

    +

    Open source · BSD 3-Clause · Linux

    @@ -133,7 +133,7 @@

    Keep HTTP code small

  • Route groups and middleware pipelines
  • JSON validation and response builders
  • Multipart forms and file uploads
  • -
  • Linux and Windows development
  • +
  • Linux development and deployment
diff --git a/lib/cjson/cJSON.c b/lib/cjson/cJSON.c deleted file mode 100644 index 6e4fb0d..0000000 --- a/lib/cjson/cJSON.c +++ /dev/null @@ -1,3191 +0,0 @@ -/* - Copyright (c) 2009-2017 Dave Gamble and cJSON contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -/* cJSON */ -/* JSON parser in C. */ - -/* disable warnings about old C89 functions in MSVC */ -#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) -#define _CRT_SECURE_NO_DEPRECATE -#endif - -#ifdef __GNUC__ -#pragma GCC visibility push(default) -#endif -#if defined(_MSC_VER) -#pragma warning (push) -/* disable warning about single line comments in system headers */ -#pragma warning (disable : 4001) -#endif - -#include -#include -#include -#include -#include -#include -#include - -#ifdef ENABLE_LOCALES -#include -#endif - -#if defined(_MSC_VER) -#pragma warning (pop) -#endif -#ifdef __GNUC__ -#pragma GCC visibility pop -#endif - -#include "cJSON.h" - -/* define our own boolean type */ -#ifdef true -#undef true -#endif -#define true ((cJSON_bool)1) - -#ifdef false -#undef false -#endif -#define false ((cJSON_bool)0) - -/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ -#ifndef isinf -#define isinf(d) (isnan((d - d)) && !isnan(d)) -#endif -#ifndef isnan -#define isnan(d) (d != d) -#endif - -#ifndef NAN -#ifdef _WIN32 -#define NAN sqrt(-1.0) -#else -#define NAN 0.0/0.0 -#endif -#endif - -typedef struct { - const unsigned char *json; - size_t position; -} error; -static error global_error = { NULL, 0 }; - -CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) -{ - return (const char*) (global_error.json + global_error.position); -} - -CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) -{ - if (!cJSON_IsString(item)) - { - return NULL; - } - - return item->valuestring; -} - -CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) -{ - if (!cJSON_IsNumber(item)) - { - return (double) NAN; - } - - return item->valuedouble; -} - -/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ -#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19) - #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. -#endif - -CJSON_PUBLIC(const char*) cJSON_Version(void) -{ - static char version[15]; - sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); - - return version; -} - -/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ -static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) -{ - if ((string1 == NULL) || (string2 == NULL)) - { - return 1; - } - - if (string1 == string2) - { - return 0; - } - - for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) - { - if (*string1 == '\0') - { - return 0; - } - } - - return tolower(*string1) - tolower(*string2); -} - -typedef struct internal_hooks -{ - void *(CJSON_CDECL *allocate)(size_t size); - void (CJSON_CDECL *deallocate)(void *pointer); - void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); -} internal_hooks; - -#if defined(_MSC_VER) -/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ -static void * CJSON_CDECL internal_malloc(size_t size) -{ - return malloc(size); -} -static void CJSON_CDECL internal_free(void *pointer) -{ - free(pointer); -} -static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) -{ - return realloc(pointer, size); -} -#else -#define internal_malloc malloc -#define internal_free free -#define internal_realloc realloc -#endif - -/* strlen of character literals resolved at compile time */ -#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) - -static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; - -static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) -{ - size_t length = 0; - unsigned char *copy = NULL; - - if (string == NULL) - { - return NULL; - } - - length = strlen((const char*)string) + sizeof(""); - copy = (unsigned char*)hooks->allocate(length); - if (copy == NULL) - { - return NULL; - } - memcpy(copy, string, length); - - return copy; -} - -CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) -{ - if (hooks == NULL) - { - /* Reset hooks */ - global_hooks.allocate = malloc; - global_hooks.deallocate = free; - global_hooks.reallocate = realloc; - return; - } - - global_hooks.allocate = malloc; - if (hooks->malloc_fn != NULL) - { - global_hooks.allocate = hooks->malloc_fn; - } - - global_hooks.deallocate = free; - if (hooks->free_fn != NULL) - { - global_hooks.deallocate = hooks->free_fn; - } - - /* use realloc only if both free and malloc are used */ - global_hooks.reallocate = NULL; - if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) - { - global_hooks.reallocate = realloc; - } -} - -/* Internal constructor. */ -static cJSON *cJSON_New_Item(const internal_hooks * const hooks) -{ - cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); - if (node) - { - memset(node, '\0', sizeof(cJSON)); - } - - return node; -} - -/* Delete a cJSON structure. */ -CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) -{ - cJSON *next = NULL; - while (item != NULL) - { - next = item->next; - if (!(item->type & cJSON_IsReference) && (item->child != NULL)) - { - cJSON_Delete(item->child); - } - if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) - { - global_hooks.deallocate(item->valuestring); - item->valuestring = NULL; - } - if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) - { - global_hooks.deallocate(item->string); - item->string = NULL; - } - global_hooks.deallocate(item); - item = next; - } -} - -/* get the decimal point character of the current locale */ -static unsigned char get_decimal_point(void) -{ -#ifdef ENABLE_LOCALES - struct lconv *lconv = localeconv(); - return (unsigned char) lconv->decimal_point[0]; -#else - return '.'; -#endif -} - -typedef struct -{ - const unsigned char *content; - size_t length; - size_t offset; - size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ - internal_hooks hooks; -} parse_buffer; - -/* check if the given size is left to read in a given parse buffer (starting with 1) */ -#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) -/* check if the buffer can be accessed at the given index (starting with 0) */ -#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) -#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) -/* get a pointer to the buffer at the position */ -#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) - -/* Parse the input text to generate a number, and populate the result into item. */ -static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) -{ - double number = 0; - unsigned char *after_end = NULL; - unsigned char *number_c_string; - unsigned char decimal_point = get_decimal_point(); - size_t i = 0; - size_t number_string_length = 0; - cJSON_bool has_decimal_point = false; - - if ((input_buffer == NULL) || (input_buffer->content == NULL)) - { - return false; - } - - /* copy the number into a temporary buffer and replace '.' with the decimal point - * of the current locale (for strtod) - * This also takes care of '\0' not necessarily being available for marking the end of the input */ - for (i = 0; can_access_at_index(input_buffer, i); i++) - { - switch (buffer_at_offset(input_buffer)[i]) - { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '+': - case '-': - case 'e': - case 'E': - number_string_length++; - break; - - case '.': - number_string_length++; - has_decimal_point = true; - break; - - default: - goto loop_end; - } - } -loop_end: - /* malloc for temporary buffer, add 1 for '\0' */ - number_c_string = (unsigned char *) input_buffer->hooks.allocate(number_string_length + 1); - if (number_c_string == NULL) - { - return false; /* allocation failure */ - } - - memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length); - number_c_string[number_string_length] = '\0'; - - if (has_decimal_point) - { - for (i = 0; i < number_string_length; i++) - { - if (number_c_string[i] == '.') - { - /* replace '.' with the decimal point of the current locale (for strtod) */ - number_c_string[i] = decimal_point; - } - } - } - - number = strtod((const char*)number_c_string, (char**)&after_end); - if (number_c_string == after_end) - { - /* free the temporary buffer */ - input_buffer->hooks.deallocate(number_c_string); - return false; /* parse_error */ - } - - item->valuedouble = number; - - /* use saturation in case of overflow */ - if (number >= INT_MAX) - { - item->valueint = INT_MAX; - } - else if (number <= (double)INT_MIN) - { - item->valueint = INT_MIN; - } - else - { - item->valueint = (int)number; - } - - item->type = cJSON_Number; - - input_buffer->offset += (size_t)(after_end - number_c_string); - /* free the temporary buffer */ - input_buffer->hooks.deallocate(number_c_string); - return true; -} - -/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ -CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) -{ - if (number >= INT_MAX) - { - object->valueint = INT_MAX; - } - else if (number <= (double)INT_MIN) - { - object->valueint = INT_MIN; - } - else - { - object->valueint = (int)number; - } - - return object->valuedouble = number; -} - -/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ -CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) -{ - char *copy = NULL; - size_t v1_len; - size_t v2_len; - /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ - if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) - { - return NULL; - } - /* return NULL if the object is corrupted or valuestring is NULL */ - if (object->valuestring == NULL || valuestring == NULL) - { - return NULL; - } - - v1_len = strlen(valuestring); - v2_len = strlen(object->valuestring); - - if (v1_len <= v2_len) - { - /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */ - if (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring )) - { - return NULL; - } - strcpy(object->valuestring, valuestring); - return object->valuestring; - } - copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); - if (copy == NULL) - { - return NULL; - } - if (object->valuestring != NULL) - { - cJSON_free(object->valuestring); - } - object->valuestring = copy; - - return copy; -} - -typedef struct -{ - unsigned char *buffer; - size_t length; - size_t offset; - size_t depth; /* current nesting depth (for formatted printing) */ - cJSON_bool noalloc; - cJSON_bool format; /* is this print a formatted print */ - internal_hooks hooks; -} printbuffer; - -/* realloc printbuffer if necessary to have at least "needed" bytes more */ -static unsigned char* ensure(printbuffer * const p, size_t needed) -{ - unsigned char *newbuffer = NULL; - size_t newsize = 0; - - if ((p == NULL) || (p->buffer == NULL)) - { - return NULL; - } - - if ((p->length > 0) && (p->offset >= p->length)) - { - /* make sure that offset is valid */ - return NULL; - } - - if (needed > INT_MAX) - { - /* sizes bigger than INT_MAX are currently not supported */ - return NULL; - } - - needed += p->offset + 1; - if (needed <= p->length) - { - return p->buffer + p->offset; - } - - if (p->noalloc) { - return NULL; - } - - /* calculate new buffer size */ - if (needed > (INT_MAX / 2)) - { - /* overflow of int, use INT_MAX if possible */ - if (needed <= INT_MAX) - { - newsize = INT_MAX; - } - else - { - return NULL; - } - } - else - { - newsize = needed * 2; - } - - if (p->hooks.reallocate != NULL) - { - /* reallocate with realloc if available */ - newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); - if (newbuffer == NULL) - { - p->hooks.deallocate(p->buffer); - p->length = 0; - p->buffer = NULL; - - return NULL; - } - } - else - { - /* otherwise reallocate manually */ - newbuffer = (unsigned char*)p->hooks.allocate(newsize); - if (!newbuffer) - { - p->hooks.deallocate(p->buffer); - p->length = 0; - p->buffer = NULL; - - return NULL; - } - - memcpy(newbuffer, p->buffer, p->offset + 1); - p->hooks.deallocate(p->buffer); - } - p->length = newsize; - p->buffer = newbuffer; - - return newbuffer + p->offset; -} - -/* calculate the new length of the string in a printbuffer and update the offset */ -static void update_offset(printbuffer * const buffer) -{ - const unsigned char *buffer_pointer = NULL; - if ((buffer == NULL) || (buffer->buffer == NULL)) - { - return; - } - buffer_pointer = buffer->buffer + buffer->offset; - - buffer->offset += strlen((const char*)buffer_pointer); -} - -/* securely comparison of floating-point variables */ -static cJSON_bool compare_double(double a, double b) -{ - double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); - return (fabs(a - b) <= maxVal * DBL_EPSILON); -} - -/* Render the number nicely from the given item into a string. */ -static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - double d = item->valuedouble; - int length = 0; - size_t i = 0; - unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ - unsigned char decimal_point = get_decimal_point(); - double test = 0.0; - - if (output_buffer == NULL) - { - return false; - } - - /* This checks for NaN and Infinity */ - if (isnan(d) || isinf(d)) - { - length = sprintf((char*)number_buffer, "null"); - } - else if(d == (double)item->valueint) - { - length = sprintf((char*)number_buffer, "%d", item->valueint); - } - else - { - /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ - length = sprintf((char*)number_buffer, "%1.15g", d); - - /* Check whether the original double can be recovered */ - if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) - { - /* If not, print with 17 decimal places of precision */ - length = sprintf((char*)number_buffer, "%1.17g", d); - } - } - - /* sprintf failed or buffer overrun occurred */ - if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) - { - return false; - } - - /* reserve appropriate space in the output */ - output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); - if (output_pointer == NULL) - { - return false; - } - - /* copy the printed number to the output and replace locale - * dependent decimal point with '.' */ - for (i = 0; i < ((size_t)length); i++) - { - if (number_buffer[i] == decimal_point) - { - output_pointer[i] = '.'; - continue; - } - - output_pointer[i] = number_buffer[i]; - } - output_pointer[i] = '\0'; - - output_buffer->offset += (size_t)length; - - return true; -} - -/* parse 4 digit hexadecimal number */ -static unsigned parse_hex4(const unsigned char * const input) -{ - unsigned int h = 0; - size_t i = 0; - - for (i = 0; i < 4; i++) - { - /* parse digit */ - if ((input[i] >= '0') && (input[i] <= '9')) - { - h += (unsigned int) input[i] - '0'; - } - else if ((input[i] >= 'A') && (input[i] <= 'F')) - { - h += (unsigned int) 10 + input[i] - 'A'; - } - else if ((input[i] >= 'a') && (input[i] <= 'f')) - { - h += (unsigned int) 10 + input[i] - 'a'; - } - else /* invalid */ - { - return 0; - } - - if (i < 3) - { - /* shift left to make place for the next nibble */ - h = h << 4; - } - } - - return h; -} - -/* converts a UTF-16 literal to UTF-8 - * A literal can be one or two sequences of the form \uXXXX */ -static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) -{ - long unsigned int codepoint = 0; - unsigned int first_code = 0; - const unsigned char *first_sequence = input_pointer; - unsigned char utf8_length = 0; - unsigned char utf8_position = 0; - unsigned char sequence_length = 0; - unsigned char first_byte_mark = 0; - - if ((input_end - first_sequence) < 6) - { - /* input ends unexpectedly */ - goto fail; - } - - /* get the first utf16 sequence */ - first_code = parse_hex4(first_sequence + 2); - - /* check that the code is valid */ - if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) - { - goto fail; - } - - /* UTF16 surrogate pair */ - if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) - { - const unsigned char *second_sequence = first_sequence + 6; - unsigned int second_code = 0; - sequence_length = 12; /* \uXXXX\uXXXX */ - - if ((input_end - second_sequence) < 6) - { - /* input ends unexpectedly */ - goto fail; - } - - if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) - { - /* missing second half of the surrogate pair */ - goto fail; - } - - /* get the second utf16 sequence */ - second_code = parse_hex4(second_sequence + 2); - /* check that the code is valid */ - if ((second_code < 0xDC00) || (second_code > 0xDFFF)) - { - /* invalid second half of the surrogate pair */ - goto fail; - } - - - /* calculate the unicode codepoint from the surrogate pair */ - codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); - } - else - { - sequence_length = 6; /* \uXXXX */ - codepoint = first_code; - } - - /* encode as UTF-8 - * takes at maximum 4 bytes to encode: - * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ - if (codepoint < 0x80) - { - /* normal ascii, encoding 0xxxxxxx */ - utf8_length = 1; - } - else if (codepoint < 0x800) - { - /* two bytes, encoding 110xxxxx 10xxxxxx */ - utf8_length = 2; - first_byte_mark = 0xC0; /* 11000000 */ - } - else if (codepoint < 0x10000) - { - /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ - utf8_length = 3; - first_byte_mark = 0xE0; /* 11100000 */ - } - else if (codepoint <= 0x10FFFF) - { - /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ - utf8_length = 4; - first_byte_mark = 0xF0; /* 11110000 */ - } - else - { - /* invalid unicode codepoint */ - goto fail; - } - - /* encode as utf8 */ - for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) - { - /* 10xxxxxx */ - (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); - codepoint >>= 6; - } - /* encode first byte */ - if (utf8_length > 1) - { - (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); - } - else - { - (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); - } - - *output_pointer += utf8_length; - - return sequence_length; - -fail: - return 0; -} - -/* Parse the input text into an unescaped cinput, and populate item. */ -static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) -{ - const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; - const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; - unsigned char *output_pointer = NULL; - unsigned char *output = NULL; - - /* not a string */ - if (buffer_at_offset(input_buffer)[0] != '\"') - { - goto fail; - } - - { - /* calculate approximate size of the output (overestimate) */ - size_t allocation_length = 0; - size_t skipped_bytes = 0; - while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) - { - /* is escape sequence */ - if (input_end[0] == '\\') - { - if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) - { - /* prevent buffer overflow when last input character is a backslash */ - goto fail; - } - skipped_bytes++; - input_end++; - } - input_end++; - } - if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) - { - goto fail; /* string ended unexpectedly */ - } - - /* This is at most how much we need for the output */ - allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; - output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); - if (output == NULL) - { - goto fail; /* allocation failure */ - } - } - - output_pointer = output; - /* loop through the string literal */ - while (input_pointer < input_end) - { - if (*input_pointer != '\\') - { - *output_pointer++ = *input_pointer++; - } - /* escape sequence */ - else - { - unsigned char sequence_length = 2; - if ((input_end - input_pointer) < 1) - { - goto fail; - } - - switch (input_pointer[1]) - { - case 'b': - *output_pointer++ = '\b'; - break; - case 'f': - *output_pointer++ = '\f'; - break; - case 'n': - *output_pointer++ = '\n'; - break; - case 'r': - *output_pointer++ = '\r'; - break; - case 't': - *output_pointer++ = '\t'; - break; - case '\"': - case '\\': - case '/': - *output_pointer++ = input_pointer[1]; - break; - - /* UTF-16 literal */ - case 'u': - sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); - if (sequence_length == 0) - { - /* failed to convert UTF16-literal to UTF-8 */ - goto fail; - } - break; - - default: - goto fail; - } - input_pointer += sequence_length; - } - } - - /* zero terminate the output */ - *output_pointer = '\0'; - - item->type = cJSON_String; - item->valuestring = (char*)output; - - input_buffer->offset = (size_t) (input_end - input_buffer->content); - input_buffer->offset++; - - return true; - -fail: - if (output != NULL) - { - input_buffer->hooks.deallocate(output); - output = NULL; - } - - if (input_pointer != NULL) - { - input_buffer->offset = (size_t)(input_pointer - input_buffer->content); - } - - return false; -} - -/* Render the cstring provided to an escaped version that can be printed. */ -static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) -{ - const unsigned char *input_pointer = NULL; - unsigned char *output = NULL; - unsigned char *output_pointer = NULL; - size_t output_length = 0; - /* numbers of additional characters needed for escaping */ - size_t escape_characters = 0; - - if (output_buffer == NULL) - { - return false; - } - - /* empty string */ - if (input == NULL) - { - output = ensure(output_buffer, sizeof("\"\"")); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "\"\""); - - return true; - } - - /* set "flag" to 1 if something needs to be escaped */ - for (input_pointer = input; *input_pointer; input_pointer++) - { - switch (*input_pointer) - { - case '\"': - case '\\': - case '\b': - case '\f': - case '\n': - case '\r': - case '\t': - /* one character escape sequence */ - escape_characters++; - break; - default: - if (*input_pointer < 32) - { - /* UTF-16 escape sequence uXXXX */ - escape_characters += 5; - } - break; - } - } - output_length = (size_t)(input_pointer - input) + escape_characters; - - output = ensure(output_buffer, output_length + sizeof("\"\"")); - if (output == NULL) - { - return false; - } - - /* no characters have to be escaped */ - if (escape_characters == 0) - { - output[0] = '\"'; - memcpy(output + 1, input, output_length); - output[output_length + 1] = '\"'; - output[output_length + 2] = '\0'; - - return true; - } - - output[0] = '\"'; - output_pointer = output + 1; - /* copy the string */ - for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) - { - if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) - { - /* normal character, copy */ - *output_pointer = *input_pointer; - } - else - { - /* character needs to be escaped */ - *output_pointer++ = '\\'; - switch (*input_pointer) - { - case '\\': - *output_pointer = '\\'; - break; - case '\"': - *output_pointer = '\"'; - break; - case '\b': - *output_pointer = 'b'; - break; - case '\f': - *output_pointer = 'f'; - break; - case '\n': - *output_pointer = 'n'; - break; - case '\r': - *output_pointer = 'r'; - break; - case '\t': - *output_pointer = 't'; - break; - default: - /* escape and print as unicode codepoint */ - sprintf((char*)output_pointer, "u%04x", *input_pointer); - output_pointer += 4; - break; - } - } - } - output[output_length + 1] = '\"'; - output[output_length + 2] = '\0'; - - return true; -} - -/* Invoke print_string_ptr (which is useful) on an item. */ -static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) -{ - return print_string_ptr((unsigned char*)item->valuestring, p); -} - -/* Predeclare these prototypes. */ -static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); -static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); -static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); - -/* Utility to jump whitespace and cr/lf */ -static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) -{ - if ((buffer == NULL) || (buffer->content == NULL)) - { - return NULL; - } - - if (cannot_access_at_index(buffer, 0)) - { - return buffer; - } - - while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) - { - buffer->offset++; - } - - if (buffer->offset == buffer->length) - { - buffer->offset--; - } - - return buffer; -} - -/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ -static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) -{ - if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) - { - return NULL; - } - - if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) - { - buffer->offset += 3; - } - - return buffer; -} - -CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) -{ - size_t buffer_length; - - if (NULL == value) - { - return NULL; - } - - /* Adding null character size due to require_null_terminated. */ - buffer_length = strlen(value) + sizeof(""); - - return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); -} - -/* Parse an object - create a new root, and populate. */ -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) -{ - parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; - cJSON *item = NULL; - - /* reset error position */ - global_error.json = NULL; - global_error.position = 0; - - if (value == NULL || 0 == buffer_length) - { - goto fail; - } - - buffer.content = (const unsigned char*)value; - buffer.length = buffer_length; - buffer.offset = 0; - buffer.hooks = global_hooks; - - item = cJSON_New_Item(&global_hooks); - if (item == NULL) /* memory fail */ - { - goto fail; - } - - if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) - { - /* parse failure. ep is set. */ - goto fail; - } - - /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ - if (require_null_terminated) - { - buffer_skip_whitespace(&buffer); - if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') - { - goto fail; - } - } - if (return_parse_end) - { - *return_parse_end = (const char*)buffer_at_offset(&buffer); - } - - return item; - -fail: - if (item != NULL) - { - cJSON_Delete(item); - } - - if (value != NULL) - { - error local_error; - local_error.json = (const unsigned char*)value; - local_error.position = 0; - - if (buffer.offset < buffer.length) - { - local_error.position = buffer.offset; - } - else if (buffer.length > 0) - { - local_error.position = buffer.length - 1; - } - - if (return_parse_end != NULL) - { - *return_parse_end = (const char*)local_error.json + local_error.position; - } - - global_error = local_error; - } - - return NULL; -} - -/* Default options for cJSON_Parse */ -CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) -{ - return cJSON_ParseWithOpts(value, 0, 0); -} - -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) -{ - return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); -} - -#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) - -static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) -{ - static const size_t default_buffer_size = 256; - printbuffer buffer[1]; - unsigned char *printed = NULL; - - memset(buffer, 0, sizeof(buffer)); - - /* create buffer */ - buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); - buffer->length = default_buffer_size; - buffer->format = format; - buffer->hooks = *hooks; - if (buffer->buffer == NULL) - { - goto fail; - } - - /* print the value */ - if (!print_value(item, buffer)) - { - goto fail; - } - update_offset(buffer); - - /* check if reallocate is available */ - if (hooks->reallocate != NULL) - { - printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); - if (printed == NULL) { - goto fail; - } - buffer->buffer = NULL; - } - else /* otherwise copy the JSON over to a new buffer */ - { - printed = (unsigned char*) hooks->allocate(buffer->offset + 1); - if (printed == NULL) - { - goto fail; - } - memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); - printed[buffer->offset] = '\0'; /* just to be sure */ - - /* free the buffer */ - hooks->deallocate(buffer->buffer); - buffer->buffer = NULL; - } - - return printed; - -fail: - if (buffer->buffer != NULL) - { - hooks->deallocate(buffer->buffer); - buffer->buffer = NULL; - } - - if (printed != NULL) - { - hooks->deallocate(printed); - printed = NULL; - } - - return NULL; -} - -/* Render a cJSON item/entity/structure to text. */ -CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) -{ - return (char*)print(item, true, &global_hooks); -} - -CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) -{ - return (char*)print(item, false, &global_hooks); -} - -CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) -{ - printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; - - if (prebuffer < 0) - { - return NULL; - } - - p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); - if (!p.buffer) - { - return NULL; - } - - p.length = (size_t)prebuffer; - p.offset = 0; - p.noalloc = false; - p.format = fmt; - p.hooks = global_hooks; - - if (!print_value(item, &p)) - { - global_hooks.deallocate(p.buffer); - p.buffer = NULL; - return NULL; - } - - return (char*)p.buffer; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) -{ - printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; - - if ((length < 0) || (buffer == NULL)) - { - return false; - } - - p.buffer = (unsigned char*)buffer; - p.length = (size_t)length; - p.offset = 0; - p.noalloc = true; - p.format = format; - p.hooks = global_hooks; - - return print_value(item, &p); -} - -/* Parser core - when encountering text, process appropriately. */ -static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) -{ - if ((input_buffer == NULL) || (input_buffer->content == NULL)) - { - return false; /* no input */ - } - - /* parse the different types of values */ - /* null */ - if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) - { - item->type = cJSON_NULL; - input_buffer->offset += 4; - return true; - } - /* false */ - if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) - { - item->type = cJSON_False; - input_buffer->offset += 5; - return true; - } - /* true */ - if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) - { - item->type = cJSON_True; - item->valueint = 1; - input_buffer->offset += 4; - return true; - } - /* string */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) - { - return parse_string(item, input_buffer); - } - /* number */ - if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) - { - return parse_number(item, input_buffer); - } - /* array */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) - { - return parse_array(item, input_buffer); - } - /* object */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) - { - return parse_object(item, input_buffer); - } - - return false; -} - -/* Render a value to text. */ -static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output = NULL; - - if ((item == NULL) || (output_buffer == NULL)) - { - return false; - } - - switch ((item->type) & 0xFF) - { - case cJSON_NULL: - output = ensure(output_buffer, 5); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "null"); - return true; - - case cJSON_False: - output = ensure(output_buffer, 6); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "false"); - return true; - - case cJSON_True: - output = ensure(output_buffer, 5); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "true"); - return true; - - case cJSON_Number: - return print_number(item, output_buffer); - - case cJSON_Raw: - { - size_t raw_length = 0; - if (item->valuestring == NULL) - { - return false; - } - - raw_length = strlen(item->valuestring) + sizeof(""); - output = ensure(output_buffer, raw_length); - if (output == NULL) - { - return false; - } - memcpy(output, item->valuestring, raw_length); - return true; - } - - case cJSON_String: - return print_string(item, output_buffer); - - case cJSON_Array: - return print_array(item, output_buffer); - - case cJSON_Object: - return print_object(item, output_buffer); - - default: - return false; - } -} - -/* Build an array from input text. */ -static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) -{ - cJSON *head = NULL; /* head of the linked list */ - cJSON *current_item = NULL; - - if (input_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* to deeply nested */ - } - input_buffer->depth++; - - if (buffer_at_offset(input_buffer)[0] != '[') - { - /* not an array */ - goto fail; - } - - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) - { - /* empty array */ - goto success; - } - - /* check if we skipped to the end of the buffer */ - if (cannot_access_at_index(input_buffer, 0)) - { - input_buffer->offset--; - goto fail; - } - - /* step back to character in front of the first element */ - input_buffer->offset--; - /* loop through the comma separated array elements */ - do - { - /* allocate next item */ - cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); - if (new_item == NULL) - { - goto fail; /* allocation failure */ - } - - /* attach next item to list */ - if (head == NULL) - { - /* start the linked list */ - current_item = head = new_item; - } - else - { - /* add to the end and advance */ - current_item->next = new_item; - new_item->prev = current_item; - current_item = new_item; - } - - /* parse next value */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_value(current_item, input_buffer)) - { - goto fail; /* failed to parse value */ - } - buffer_skip_whitespace(input_buffer); - } - while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); - - if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') - { - goto fail; /* expected end of array */ - } - -success: - input_buffer->depth--; - - if (head != NULL) { - head->prev = current_item; - } - - item->type = cJSON_Array; - item->child = head; - - input_buffer->offset++; - - return true; - -fail: - if (head != NULL) - { - cJSON_Delete(head); - } - - return false; -} - -/* Render an array to text */ -static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - size_t length = 0; - cJSON *current_element = item->child; - - if (output_buffer == NULL) - { - return false; - } - - /* Compose the output array. */ - /* opening square bracket */ - output_pointer = ensure(output_buffer, 1); - if (output_pointer == NULL) - { - return false; - } - - *output_pointer = '['; - output_buffer->offset++; - output_buffer->depth++; - - while (current_element != NULL) - { - if (!print_value(current_element, output_buffer)) - { - return false; - } - update_offset(output_buffer); - if (current_element->next) - { - length = (size_t) (output_buffer->format ? 2 : 1); - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ','; - if(output_buffer->format) - { - *output_pointer++ = ' '; - } - *output_pointer = '\0'; - output_buffer->offset += length; - } - current_element = current_element->next; - } - - output_pointer = ensure(output_buffer, 2); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ']'; - *output_pointer = '\0'; - output_buffer->depth--; - - return true; -} - -/* Build an object from the text. */ -static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) -{ - cJSON *head = NULL; /* linked list head */ - cJSON *current_item = NULL; - - if (input_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* to deeply nested */ - } - input_buffer->depth++; - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) - { - goto fail; /* not an object */ - } - - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) - { - goto success; /* empty object */ - } - - /* check if we skipped to the end of the buffer */ - if (cannot_access_at_index(input_buffer, 0)) - { - input_buffer->offset--; - goto fail; - } - - /* step back to character in front of the first element */ - input_buffer->offset--; - /* loop through the comma separated array elements */ - do - { - /* allocate next item */ - cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); - if (new_item == NULL) - { - goto fail; /* allocation failure */ - } - - /* attach next item to list */ - if (head == NULL) - { - /* start the linked list */ - current_item = head = new_item; - } - else - { - /* add to the end and advance */ - current_item->next = new_item; - new_item->prev = current_item; - current_item = new_item; - } - - if (cannot_access_at_index(input_buffer, 1)) - { - goto fail; /* nothing comes after the comma */ - } - - /* parse the name of the child */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_string(current_item, input_buffer)) - { - goto fail; /* failed to parse name */ - } - buffer_skip_whitespace(input_buffer); - - /* swap valuestring and string, because we parsed the name */ - current_item->string = current_item->valuestring; - current_item->valuestring = NULL; - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) - { - goto fail; /* invalid object */ - } - - /* parse the value */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_value(current_item, input_buffer)) - { - goto fail; /* failed to parse value */ - } - buffer_skip_whitespace(input_buffer); - } - while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) - { - goto fail; /* expected end of object */ - } - -success: - input_buffer->depth--; - - if (head != NULL) { - head->prev = current_item; - } - - item->type = cJSON_Object; - item->child = head; - - input_buffer->offset++; - return true; - -fail: - if (head != NULL) - { - cJSON_Delete(head); - } - - return false; -} - -/* Render an object to text. */ -static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - size_t length = 0; - cJSON *current_item = item->child; - - if (output_buffer == NULL) - { - return false; - } - - /* Compose the output: */ - length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - - *output_pointer++ = '{'; - output_buffer->depth++; - if (output_buffer->format) - { - *output_pointer++ = '\n'; - } - output_buffer->offset += length; - - while (current_item) - { - if (output_buffer->format) - { - size_t i; - output_pointer = ensure(output_buffer, output_buffer->depth); - if (output_pointer == NULL) - { - return false; - } - for (i = 0; i < output_buffer->depth; i++) - { - *output_pointer++ = '\t'; - } - output_buffer->offset += output_buffer->depth; - } - - /* print key */ - if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) - { - return false; - } - update_offset(output_buffer); - - length = (size_t) (output_buffer->format ? 2 : 1); - output_pointer = ensure(output_buffer, length); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ':'; - if (output_buffer->format) - { - *output_pointer++ = '\t'; - } - output_buffer->offset += length; - - /* print value */ - if (!print_value(current_item, output_buffer)) - { - return false; - } - update_offset(output_buffer); - - /* print comma if not last */ - length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - if (current_item->next) - { - *output_pointer++ = ','; - } - - if (output_buffer->format) - { - *output_pointer++ = '\n'; - } - *output_pointer = '\0'; - output_buffer->offset += length; - - current_item = current_item->next; - } - - output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); - if (output_pointer == NULL) - { - return false; - } - if (output_buffer->format) - { - size_t i; - for (i = 0; i < (output_buffer->depth - 1); i++) - { - *output_pointer++ = '\t'; - } - } - *output_pointer++ = '}'; - *output_pointer = '\0'; - output_buffer->depth--; - - return true; -} - -/* Get Array size/item / object item. */ -CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) -{ - cJSON *child = NULL; - size_t size = 0; - - if (array == NULL) - { - return 0; - } - - child = array->child; - - while(child != NULL) - { - size++; - child = child->next; - } - - /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ - - return (int)size; -} - -static cJSON* get_array_item(const cJSON *array, size_t index) -{ - cJSON *current_child = NULL; - - if (array == NULL) - { - return NULL; - } - - current_child = array->child; - while ((current_child != NULL) && (index > 0)) - { - index--; - current_child = current_child->next; - } - - return current_child; -} - -CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) -{ - if (index < 0) - { - return NULL; - } - - return get_array_item(array, (size_t)index); -} - -static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) -{ - cJSON *current_element = NULL; - - if ((object == NULL) || (name == NULL)) - { - return NULL; - } - - current_element = object->child; - if (case_sensitive) - { - while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) - { - current_element = current_element->next; - } - } - else - { - while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) - { - current_element = current_element->next; - } - } - - if ((current_element == NULL) || (current_element->string == NULL)) { - return NULL; - } - - return current_element; -} - -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) -{ - return get_object_item(object, string, false); -} - -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) -{ - return get_object_item(object, string, true); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) -{ - return cJSON_GetObjectItem(object, string) ? 1 : 0; -} - -/* Utility for array list handling. */ -static void suffix_object(cJSON *prev, cJSON *item) -{ - prev->next = item; - item->prev = prev; -} - -/* Utility for handling references. */ -static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) -{ - cJSON *reference = NULL; - if (item == NULL) - { - return NULL; - } - - reference = cJSON_New_Item(hooks); - if (reference == NULL) - { - return NULL; - } - - memcpy(reference, item, sizeof(cJSON)); - reference->string = NULL; - reference->type |= cJSON_IsReference; - reference->next = reference->prev = NULL; - return reference; -} - -static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) -{ - cJSON *child = NULL; - - if ((item == NULL) || (array == NULL) || (array == item)) - { - return false; - } - - child = array->child; - /* - * To find the last item in array quickly, we use prev in array - */ - if (child == NULL) - { - /* list is empty, start new one */ - array->child = item; - item->prev = item; - item->next = NULL; - } - else - { - /* append to the end */ - if (child->prev) - { - suffix_object(child->prev, item); - array->child->prev = item; - } - } - - return true; -} - -/* Add item to array/object. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) -{ - return add_item_to_array(array, item); -} - -#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) - #pragma GCC diagnostic push -#endif -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wcast-qual" -#endif -/* helper function to cast away const */ -static void* cast_away_const(const void* string) -{ - return (void*)string; -} -#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) - #pragma GCC diagnostic pop -#endif - - -static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) -{ - char *new_key = NULL; - int new_type = cJSON_Invalid; - - if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) - { - return false; - } - - if (constant_key) - { - new_key = (char*)cast_away_const(string); - new_type = item->type | cJSON_StringIsConst; - } - else - { - new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); - if (new_key == NULL) - { - return false; - } - - new_type = item->type & ~cJSON_StringIsConst; - } - - if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) - { - hooks->deallocate(item->string); - } - - item->string = new_key; - item->type = new_type; - - return add_item_to_array(object, item); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) -{ - return add_item_to_object(object, string, item, &global_hooks, false); -} - -/* Add an item to an object with constant string as key */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) -{ - return add_item_to_object(object, string, item, &global_hooks, true); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) -{ - if (array == NULL) - { - return false; - } - - return add_item_to_array(array, create_reference(item, &global_hooks)); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) -{ - if ((object == NULL) || (string == NULL)) - { - return false; - } - - return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); -} - -CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) -{ - cJSON *null = cJSON_CreateNull(); - if (add_item_to_object(object, name, null, &global_hooks, false)) - { - return null; - } - - cJSON_Delete(null); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) -{ - cJSON *true_item = cJSON_CreateTrue(); - if (add_item_to_object(object, name, true_item, &global_hooks, false)) - { - return true_item; - } - - cJSON_Delete(true_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) -{ - cJSON *false_item = cJSON_CreateFalse(); - if (add_item_to_object(object, name, false_item, &global_hooks, false)) - { - return false_item; - } - - cJSON_Delete(false_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) -{ - cJSON *bool_item = cJSON_CreateBool(boolean); - if (add_item_to_object(object, name, bool_item, &global_hooks, false)) - { - return bool_item; - } - - cJSON_Delete(bool_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) -{ - cJSON *number_item = cJSON_CreateNumber(number); - if (add_item_to_object(object, name, number_item, &global_hooks, false)) - { - return number_item; - } - - cJSON_Delete(number_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) -{ - cJSON *string_item = cJSON_CreateString(string); - if (add_item_to_object(object, name, string_item, &global_hooks, false)) - { - return string_item; - } - - cJSON_Delete(string_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) -{ - cJSON *raw_item = cJSON_CreateRaw(raw); - if (add_item_to_object(object, name, raw_item, &global_hooks, false)) - { - return raw_item; - } - - cJSON_Delete(raw_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) -{ - cJSON *object_item = cJSON_CreateObject(); - if (add_item_to_object(object, name, object_item, &global_hooks, false)) - { - return object_item; - } - - cJSON_Delete(object_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) -{ - cJSON *array = cJSON_CreateArray(); - if (add_item_to_object(object, name, array, &global_hooks, false)) - { - return array; - } - - cJSON_Delete(array); - return NULL; -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) -{ - if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)) - { - return NULL; - } - - if (item != parent->child) - { - /* not the first element */ - item->prev->next = item->next; - } - if (item->next != NULL) - { - /* not the last element */ - item->next->prev = item->prev; - } - - if (item == parent->child) - { - /* first element */ - parent->child = item->next; - } - else if (item->next == NULL) - { - /* last element */ - parent->child->prev = item->prev; - } - - /* make sure the detached item doesn't point anywhere anymore */ - item->prev = NULL; - item->next = NULL; - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) -{ - if (which < 0) - { - return NULL; - } - - return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) -{ - cJSON_Delete(cJSON_DetachItemFromArray(array, which)); -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) -{ - cJSON *to_detach = cJSON_GetObjectItem(object, string); - - return cJSON_DetachItemViaPointer(object, to_detach); -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) -{ - cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); - - return cJSON_DetachItemViaPointer(object, to_detach); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) -{ - cJSON_Delete(cJSON_DetachItemFromObject(object, string)); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) -{ - cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); -} - -/* Replace array/object items with new ones. */ -CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) -{ - cJSON *after_inserted = NULL; - - if (which < 0 || newitem == NULL) - { - return false; - } - - after_inserted = get_array_item(array, (size_t)which); - if (after_inserted == NULL) - { - return add_item_to_array(array, newitem); - } - - if (after_inserted != array->child && after_inserted->prev == NULL) { - /* return false if after_inserted is a corrupted array item */ - return false; - } - - newitem->next = after_inserted; - newitem->prev = after_inserted->prev; - after_inserted->prev = newitem; - if (after_inserted == array->child) - { - array->child = newitem; - } - else - { - newitem->prev->next = newitem; - } - return true; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) -{ - if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) - { - return false; - } - - if (replacement == item) - { - return true; - } - - replacement->next = item->next; - replacement->prev = item->prev; - - if (replacement->next != NULL) - { - replacement->next->prev = replacement; - } - if (parent->child == item) - { - if (parent->child->prev == parent->child) - { - replacement->prev = replacement; - } - parent->child = replacement; - } - else - { /* - * To find the last item in array quickly, we use prev in array. - * We can't modify the last item's next pointer where this item was the parent's child - */ - if (replacement->prev != NULL) - { - replacement->prev->next = replacement; - } - if (replacement->next == NULL) - { - parent->child->prev = replacement; - } - } - - item->next = NULL; - item->prev = NULL; - cJSON_Delete(item); - - return true; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) -{ - if (which < 0) - { - return false; - } - - return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); -} - -static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) -{ - if ((replacement == NULL) || (string == NULL)) - { - return false; - } - - /* replace the name in the replacement */ - if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) - { - cJSON_free(replacement->string); - } - replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); - if (replacement->string == NULL) - { - return false; - } - - replacement->type &= ~cJSON_StringIsConst; - - return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) -{ - return replace_item_in_object(object, string, newitem, false); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) -{ - return replace_item_in_object(object, string, newitem, true); -} - -/* Create basic types: */ -CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_NULL; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_True; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_False; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = boolean ? cJSON_True : cJSON_False; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_Number; - item->valuedouble = num; - - /* use saturation in case of overflow */ - if (num >= INT_MAX) - { - item->valueint = INT_MAX; - } - else if (num <= (double)INT_MIN) - { - item->valueint = INT_MIN; - } - else - { - item->valueint = (int)num; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_String; - item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); - if(!item->valuestring) - { - cJSON_Delete(item); - return NULL; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) - { - item->type = cJSON_String | cJSON_IsReference; - item->valuestring = (char*)cast_away_const(string); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) { - item->type = cJSON_Object | cJSON_IsReference; - item->child = (cJSON*)cast_away_const(child); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) { - item->type = cJSON_Array | cJSON_IsReference; - item->child = (cJSON*)cast_away_const(child); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_Raw; - item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); - if(!item->valuestring) - { - cJSON_Delete(item); - return NULL; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type=cJSON_Array; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item) - { - item->type = cJSON_Object; - } - - return item; -} - -/* Create Arrays: */ -CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber(numbers[i]); - if (!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber((double)numbers[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber(numbers[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (strings == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for (i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateString(strings[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p,n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -/* Duplication */ -cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse); - -CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) -{ - return cJSON_Duplicate_rec(item, 0, recurse ); -} - -cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse) -{ - cJSON *newitem = NULL; - cJSON *child = NULL; - cJSON *next = NULL; - cJSON *newchild = NULL; - - /* Bail on bad ptr */ - if (!item) - { - goto fail; - } - /* Create new item */ - newitem = cJSON_New_Item(&global_hooks); - if (!newitem) - { - goto fail; - } - /* Copy over all vars */ - newitem->type = item->type & (~cJSON_IsReference); - newitem->valueint = item->valueint; - newitem->valuedouble = item->valuedouble; - if (item->valuestring) - { - newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); - if (!newitem->valuestring) - { - goto fail; - } - } - if (item->string) - { - newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); - if (!newitem->string) - { - goto fail; - } - } - /* If non-recursive, then we're done! */ - if (!recurse) - { - return newitem; - } - /* Walk the ->next chain for the child. */ - child = item->child; - while (child != NULL) - { - if(depth >= CJSON_CIRCULAR_LIMIT) { - goto fail; - } - newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */ - if (!newchild) - { - goto fail; - } - if (next != NULL) - { - /* If newitem->child already set, then crosswire ->prev and ->next and move on */ - next->next = newchild; - newchild->prev = next; - next = newchild; - } - else - { - /* Set newitem->child and move to it */ - newitem->child = newchild; - next = newchild; - } - child = child->next; - } - if (newitem && newitem->child) - { - newitem->child->prev = newchild; - } - - return newitem; - -fail: - if (newitem != NULL) - { - cJSON_Delete(newitem); - } - - return NULL; -} - -static void skip_oneline_comment(char **input) -{ - *input += static_strlen("//"); - - for (; (*input)[0] != '\0'; ++(*input)) - { - if ((*input)[0] == '\n') { - *input += static_strlen("\n"); - return; - } - } -} - -static void skip_multiline_comment(char **input) -{ - *input += static_strlen("/*"); - - for (; (*input)[0] != '\0'; ++(*input)) - { - if (((*input)[0] == '*') && ((*input)[1] == '/')) - { - *input += static_strlen("*/"); - return; - } - } -} - -static void minify_string(char **input, char **output) { - (*output)[0] = (*input)[0]; - *input += static_strlen("\""); - *output += static_strlen("\""); - - - for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { - (*output)[0] = (*input)[0]; - - if ((*input)[0] == '\"') { - (*output)[0] = '\"'; - *input += static_strlen("\""); - *output += static_strlen("\""); - return; - } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { - (*output)[1] = (*input)[1]; - *input += static_strlen("\""); - *output += static_strlen("\""); - } - } -} - -CJSON_PUBLIC(void) cJSON_Minify(char *json) -{ - char *into = json; - - if (json == NULL) - { - return; - } - - while (json[0] != '\0') - { - switch (json[0]) - { - case ' ': - case '\t': - case '\r': - case '\n': - json++; - break; - - case '/': - if (json[1] == '/') - { - skip_oneline_comment(&json); - } - else if (json[1] == '*') - { - skip_multiline_comment(&json); - } else { - json++; - } - break; - - case '\"': - minify_string(&json, (char**)&into); - break; - - default: - into[0] = json[0]; - json++; - into++; - } - } - - /* and null-terminate. */ - *into = '\0'; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Invalid; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_False; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xff) == cJSON_True; -} - - -CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & (cJSON_True | cJSON_False)) != 0; -} -CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_NULL; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Number; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_String; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Array; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Object; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Raw; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) -{ - if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) - { - return false; - } - - /* check if type is valid */ - switch (a->type & 0xFF) - { - case cJSON_False: - case cJSON_True: - case cJSON_NULL: - case cJSON_Number: - case cJSON_String: - case cJSON_Raw: - case cJSON_Array: - case cJSON_Object: - break; - - default: - return false; - } - - /* identical objects are equal */ - if (a == b) - { - return true; - } - - switch (a->type & 0xFF) - { - /* in these cases and equal type is enough */ - case cJSON_False: - case cJSON_True: - case cJSON_NULL: - return true; - - case cJSON_Number: - if (compare_double(a->valuedouble, b->valuedouble)) - { - return true; - } - return false; - - case cJSON_String: - case cJSON_Raw: - if ((a->valuestring == NULL) || (b->valuestring == NULL)) - { - return false; - } - if (strcmp(a->valuestring, b->valuestring) == 0) - { - return true; - } - - return false; - - case cJSON_Array: - { - cJSON *a_element = a->child; - cJSON *b_element = b->child; - - for (; (a_element != NULL) && (b_element != NULL);) - { - if (!cJSON_Compare(a_element, b_element, case_sensitive)) - { - return false; - } - - a_element = a_element->next; - b_element = b_element->next; - } - - /* one of the arrays is longer than the other */ - if (a_element != b_element) { - return false; - } - - return true; - } - - case cJSON_Object: - { - cJSON *a_element = NULL; - cJSON *b_element = NULL; - cJSON_ArrayForEach(a_element, a) - { - /* TODO This has O(n^2) runtime, which is horrible! */ - b_element = get_object_item(b, a_element->string, case_sensitive); - if (b_element == NULL) - { - return false; - } - - if (!cJSON_Compare(a_element, b_element, case_sensitive)) - { - return false; - } - } - - /* doing this twice, once on a and b to prevent true comparison if a subset of b - * TODO: Do this the proper way, this is just a fix for now */ - cJSON_ArrayForEach(b_element, b) - { - a_element = get_object_item(a, b_element->string, case_sensitive); - if (a_element == NULL) - { - return false; - } - - if (!cJSON_Compare(b_element, a_element, case_sensitive)) - { - return false; - } - } - - return true; - } - - default: - return false; - } -} - -CJSON_PUBLIC(void *) cJSON_malloc(size_t size) -{ - return global_hooks.allocate(size); -} - -CJSON_PUBLIC(void) cJSON_free(void *object) -{ - global_hooks.deallocate(object); - object = NULL; -} diff --git a/lib/cjson/cJSON.h b/lib/cjson/cJSON.h deleted file mode 100644 index cab5feb..0000000 --- a/lib/cjson/cJSON.h +++ /dev/null @@ -1,306 +0,0 @@ -/* - Copyright (c) 2009-2017 Dave Gamble and cJSON contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -#ifndef cJSON__h -#define cJSON__h - -#ifdef __cplusplus -extern "C" -{ -#endif - -#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) -#define __WINDOWS__ -#endif - -#ifdef __WINDOWS__ - -/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: - -CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols -CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) -CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol - -For *nix builds that support visibility attribute, you can define similar behavior by - -setting default visibility to hidden by adding --fvisibility=hidden (for gcc) -or --xldscope=hidden (for sun cc) -to CFLAGS - -then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does - -*/ - -#define CJSON_CDECL __cdecl -#define CJSON_STDCALL __stdcall - -/* export symbols by default, this is necessary for copy pasting the C and header file */ -#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) -#define CJSON_EXPORT_SYMBOLS -#endif - -#if defined(CJSON_HIDE_SYMBOLS) -#define CJSON_PUBLIC(type) type CJSON_STDCALL -#elif defined(CJSON_EXPORT_SYMBOLS) -#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL -#elif defined(CJSON_IMPORT_SYMBOLS) -#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL -#endif -#else /* !__WINDOWS__ */ -#define CJSON_CDECL -#define CJSON_STDCALL - -#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) -#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type -#else -#define CJSON_PUBLIC(type) type -#endif -#endif - -/* project version */ -#define CJSON_VERSION_MAJOR 1 -#define CJSON_VERSION_MINOR 7 -#define CJSON_VERSION_PATCH 19 - -#include - -/* cJSON Types: */ -#define cJSON_Invalid (0) -#define cJSON_False (1 << 0) -#define cJSON_True (1 << 1) -#define cJSON_NULL (1 << 2) -#define cJSON_Number (1 << 3) -#define cJSON_String (1 << 4) -#define cJSON_Array (1 << 5) -#define cJSON_Object (1 << 6) -#define cJSON_Raw (1 << 7) /* raw json */ - -#define cJSON_IsReference 256 -#define cJSON_StringIsConst 512 - -/* The cJSON structure: */ -typedef struct cJSON -{ - /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ - struct cJSON *next; - struct cJSON *prev; - /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ - struct cJSON *child; - - /* The type of the item, as above. */ - int type; - - /* The item's string, if type==cJSON_String and type == cJSON_Raw */ - char *valuestring; - /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ - int valueint; - /* The item's number, if type==cJSON_Number */ - double valuedouble; - - /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ - char *string; -} cJSON; - -typedef struct cJSON_Hooks -{ - /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ - void *(CJSON_CDECL *malloc_fn)(size_t sz); - void (CJSON_CDECL *free_fn)(void *ptr); -} cJSON_Hooks; - -typedef int cJSON_bool; - -/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. - * This is to prevent stack overflows. */ -#ifndef CJSON_NESTING_LIMIT -#define CJSON_NESTING_LIMIT 1000 -#endif - -/* Limits the length of circular references can be before cJSON rejects to parse them. - * This is to prevent stack overflows. */ -#ifndef CJSON_CIRCULAR_LIMIT -#define CJSON_CIRCULAR_LIMIT 10000 -#endif - -/* returns the version of cJSON as a string */ -CJSON_PUBLIC(const char*) cJSON_Version(void); - -/* Supply malloc, realloc and free functions to cJSON */ -CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); - -/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ -/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ -CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); -/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ -/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ -CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); - -/* Render a cJSON entity to text for transfer/storage. */ -CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); -/* Render a cJSON entity to text for transfer/storage without any formatting. */ -CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); -/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ -CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); -/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ -/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ -CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); -/* Delete a cJSON entity and all subentities. */ -CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); - -/* Returns the number of items in an array (or object). */ -CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); -/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ -CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); -/* Get item "string" from object. Case insensitive. */ -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); -CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); -/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ -CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); - -/* Check item type and return its value */ -CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); -CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); - -/* These functions check the type of an item */ -CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); - -/* These calls create a cJSON item of the appropriate type. */ -CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); -CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); -CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); -/* raw json */ -CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); -CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); - -/* Create a string where valuestring references a string so - * it will not be freed by cJSON_Delete */ -CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); -/* Create an object/array that only references it's elements so - * they will not be freed by cJSON_Delete */ -CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); -CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); - -/* These utilities create an Array of count items. - * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ -CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); - -/* Append item to the specified array/object. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); -/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. - * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before - * writing to `item->string` */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); -/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); - -/* Remove/Detach items from Arrays/Objects. */ -CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); -CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); -CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); -CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); - -/* Update array items. */ -CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); - -/* Duplicate a cJSON item */ -CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); -/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will - * need to be released. With recurse!=0, it will duplicate any children connected to the item. - * The item->next and ->prev pointers are always zero on return from Duplicate. */ -/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. - * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ -CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); - -/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. - * The input pointer json cannot point to a read-only address area, such as a string constant, - * but should point to a readable and writable address area. */ -CJSON_PUBLIC(void) cJSON_Minify(char *json); - -/* Helper functions for creating and adding items to an object at the same time. - * They return the added item or NULL on failure. */ -CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); -CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); -CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); -CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); -CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); - -/* When assigning an integer value, it needs to be propagated to valuedouble too. */ -#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) -/* helper for the cJSON_SetNumberValue macro */ -CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); -#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) -/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ -CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); - -/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ -#define cJSON_SetBoolValue(object, boolValue) ( \ - (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ - (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ - cJSON_Invalid\ -) - -/* Macro for iterating over an array or object */ -#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) - -/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ -CJSON_PUBLIC(void *) cJSON_malloc(size_t size); -CJSON_PUBLIC(void) cJSON_free(void *object); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/scripts/install.ps1 b/scripts/install.ps1 deleted file mode 100644 index f45594f..0000000 --- a/scripts/install.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -$tmp = "$env:TEMP\libchttpx.zip" -$installDir = "$env:ProgramFiles\libchttpx" -$url = "https://github.com/netcorelink/libchttpx/releases/latest/download/libchttpx-win64.zip" - -Write-Host "Installing libchttpx to $installDir" - -Invoke-WebRequest $url -OutFile $tmp -Expand-Archive $tmp $installDir -Force - -# Set environment variables -[Environment]::SetEnvironmentVariable( - "CPATH", - "$installDir\include;$installDir\lib\cjson", - "Machine" -) - -[Environment]::SetEnvironmentVariable( - "LIBRARY_PATH", - "$installDir", - "Machine" -) - -Write-Host "" -Write-Host "libchttpx installed successfully!" -Write-Host "" -Write-Host "Usage:" -Write-Host " gcc server.c -lchttpx -lws2_32" -Write-Host "" -Write-Host "Includes:" -Write-Host " #include " -Write-Host " #include " -Write-Host "" -Write-Host "Restart terminal to apply environment variables." diff --git a/src/cHTTPX_crosspltm.h b/src/cHTTPX_crosspltm.h index 85d216d..94c7f33 100644 --- a/src/cHTTPX_crosspltm.h +++ b/src/cHTTPX_crosspltm.h @@ -6,142 +6,49 @@ extern "C" { #endif -#include - -#if defined(_WIN32) || defined(_WIN64) -#define CHTTPX_PLATFORM_WINDOWS -#else -#define CHTTPX_PLATFORM_POSIX +#if !defined(__linux__) +#error "libchttpx supports Linux only" #endif -#ifdef CHTTPX_PLATFORM_WINDOWS -#define strdup _strdup -#else -#define strdup strdup -#endif +#include +#include +#include +#include +#include +#include -#ifdef CHTTPX_PLATFORM_WINDOWS -#define chttpx_close(s) closesocket(s) -#else +#define CHTTPX_PLATFORM_POSIX #define chttpx_close(s) close(s) -#endif - -#ifdef CHTTPX_PLATFORM_WINDOWS -#include -#include -#include -#include -#endif -#ifdef _WIN32 -/** Socket handle type (Windows SOCKET, POSIX int). */ -typedef SOCKET chttpx_socket_t; -#else typedef int chttpx_socket_t; -#endif -#ifdef CHTTPX_PLATFORM_WINDOWS - /** - * Thread-safe localtime (POSIX localtime_r on Windows). - * - * @param timep Input time value. - * @param result Output struct tm buffer. - * @return result on success. - */ - static inline struct tm* localtime_r(const time_t* timep, struct tm* result) - { - memset(result, 0, sizeof(*result)); - localtime_s(result, timep); - return result; - } +/** + * Find a byte substring within a buffer (POSIX memmem when unavailable). + * + * @param haystack Buffer to search. + * @param haystacklen Length of haystack. + * @param needle Substring to find. + * @param needlelen Length of needle. + * @return Pointer into haystack, or NULL if not found. + */ +static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) +{ + if (!needlelen) + return (void*)haystack; + if (needlelen > haystacklen) + return NULL; - /** - * Thread-safe UTC breakdown (POSIX gmtime_r on Windows). - * - * @param timep Input time value. - * @param result Output struct tm buffer. - * @return result on success. - */ - static inline struct tm* gmtime_r(const time_t* timep, struct tm* result) - { - memset(result, 0, sizeof(*result)); - gmtime_s(result, timep); - return result; - } + const unsigned char* h = haystack; + const unsigned char* n = needle; - /** - * clock_gettime compatibility for Windows. - * - * @param clock_id CLOCK_MONOTONIC or real-time clock id. - * @param value Output timespec. - * @return 0 on success, -1 on error. - */ - static inline int chttpx_clock_gettime(int clock_id, struct timespec* value) + for (size_t i = 0; i <= haystacklen - needlelen; i++) { - if (!value) - return -1; - if (clock_id == CLOCK_MONOTONIC) - { - LARGE_INTEGER frequency; - LARGE_INTEGER counter; - QueryPerformanceFrequency(&frequency); - QueryPerformanceCounter(&counter); - value->tv_sec = (time_t)(counter.QuadPart / frequency.QuadPart); - value->tv_nsec = (long)(((counter.QuadPart % frequency.QuadPart) * 1000000000LL) / frequency.QuadPart); - return 0; - } - FILETIME file_time; - ULARGE_INTEGER ticks; - GetSystemTimeAsFileTime(&file_time); - ticks.LowPart = file_time.dwLowDateTime; - ticks.HighPart = file_time.dwHighDateTime; - unsigned long long unix_ticks = ticks.QuadPart - 116444736000000000ULL; - value->tv_sec = (time_t)(unix_ticks / 10000000ULL); - value->tv_nsec = (long)((unix_ticks % 10000000ULL) * 100ULL); - return 0; + if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) + return (void*)(h + i); } -#define clock_gettime chttpx_clock_gettime -#endif - -#ifdef CHTTPX_PLATFORM_POSIX -#include -#include -#include -#include -#endif - -#ifdef CHTTPX_PLATFORM_WINDOWS -#define strcasecmp _stricmp -#endif - - /** - * Find a byte substring within a buffer (POSIX memmem when unavailable). - * - * @param haystack Buffer to search. - * @param haystacklen Length of haystack. - * @param needle Substring to find. - * @param needlelen Length of needle. - * @return Pointer into haystack, or NULL if not found. - */ - static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) - { - if (!needlelen) - return (void*)haystack; - if (needlelen > haystacklen) - return NULL; - - const unsigned char* h = haystack; - const unsigned char* n = needle; - - for (size_t i = 0; i <= haystacklen - needlelen; i++) - { - if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) - return (void*)(h + i); - } - - return NULL; - } + return NULL; +} #ifdef __cplusplus } diff --git a/src/cHTTPX_utils.h b/src/cHTTPX_utils.h index 459564b..d747fda 100644 --- a/src/cHTTPX_utils.h +++ b/src/cHTTPX_utils.h @@ -1,6 +1,8 @@ #ifndef UTILS_H #define UTILS_H +#include +#include #include #ifdef __cplusplus @@ -8,71 +10,6 @@ extern "C" { #endif -/* Threads */ -#if defined(_WIN32) || defined(_WIN64) -#include - - typedef HANDLE thread_t; - - /** Arguments passed to a newly created Windows worker thread. */ - typedef struct - { - void* (*function)(void*); - void* argument; - } chttpx_thread_start_t; - - /** - * Windows thread entry that invokes the user start routine. - * - * @param value Heap-allocated chttpx_thread_start_t (freed here). - * @return Always 0. - */ - static DWORD WINAPI _thread_start(LPVOID value) - { - chttpx_thread_start_t* start = value; - void* (*function)(void*) = start->function; - void* argument = start->argument; - free(start); - function(argument); - return 0; - } - - /** - * Create a worker thread (Windows). - * - * @param thread Output thread handle. - * @param func User function accepting void* argument. - * @param arg Argument passed to func. - * @return 0 on success, -1 on error. - */ - static inline int _thread_create(thread_t* thread, void* (*func)(void*), void* arg) - { - chttpx_thread_start_t* start = malloc(sizeof(*start)); - if (!start) - return -1; - start->function = func; - start->argument = arg; - *thread = CreateThread(NULL, 0, _thread_start, start, 0, NULL); - if (!*thread) - free(start); - return *thread ? 0 : -1; - } - - /** - * Wait for a worker thread to finish (Windows). - * - * @param thread Thread handle from _thread_create. - * @return 0 on success. - */ - static inline int _thread_join(thread_t thread) - { - WaitForSingleObject(thread, INFINITE); - CloseHandle(thread); - return 0; - } -#else -#include - typedef pthread_t thread_t; /** @@ -98,7 +35,6 @@ static inline int _thread_join(thread_t thread) { return pthread_join(thread, NULL); } -#endif #ifdef __cplusplus } @@ -106,8 +42,6 @@ static inline int _thread_join(thread_t thread) #define ARRAY_LEN(arr) (sizeof(arr) / sizeof((arr)[0])) -#include - /** * Case-insensitive memmem over a bounded haystack. * From 1a3b77ec738708e10e4c5a6a30291c13353314e8 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Sun, 27 Sep 2026 20:24:57 +0500 Subject: [PATCH 46/48] feat: add WebSocket support over HTTP/2 (#65) * feat(websocket): add server websocket state * feat(websocket): define HTTP/2 websocket API * feat(websocket): implement RFC 6455 frame handling * feat(websocket): clean up websocket routes on shutdown * feat(websocket): add websocket stream state to HTTP/2 * feat(websocket): bridge websocket frames over HTTP/2 DATA * feat(websocket): support RFC 8441 extended CONNECT * feat(websocket): expose websocket API in public header * docs(websocket): add echo server example * build: include websocket example * test(websocket): cover frames fragmentation ping and close * test(websocket): run websocket parser tests in make test * fix(websocket): harden close callback lifecycle * fix(websocket): keep public API C++ compatible * docs(websocket): document HTTP/2 websocket API * docs(websocket): add Russian HTTP/2 websocket guide * fix(websocket): use AppShutdown in example * feat: refactor BindJSON response code, remove static return text * fix: restore platform-specific event loop guards --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/pull_request_template.md | 2 +- CONTRIBUTING.md | 2 +- Makefile | 10 +- docs/json/README.md | 15 +- docs/json/README_RU.md | 15 +- docs/server/README.md | 2 +- docs/server/README_RU.md | 2 +- docs/site/examples.html | 3 +- docs/websocket/README.md | 89 +-- docs/websocket/README_RU.md | 89 +-- example/README.md | 2 +- example/json.c | 10 +- example/websocket.c | 50 ++ include/libchttpx.h | 373 +++++++----- src/cHTTPX_app.c | 63 -- src/cHTTPX_body.c | 10 - src/cHTTPX_http2.c | 300 +++++++++- src/cHTTPX_i18n.c | 4 - src/cHTTPX_json.c | 4 - src/cHTTPX_media.c | 10 - src/cHTTPX_metrics.c | 22 - src/cHTTPX_metrics.h | 1 - src/cHTTPX_middlewares.c | 20 - src/cHTTPX_request.c | 171 ++---- src/cHTTPX_request.h | 41 +- src/cHTTPX_response.c | 14 - src/cHTTPX_runtime.c | 50 -- src/cHTTPX_serv.c | 26 +- src/cHTTPX_serv.h | 2 +- src/cHTTPX_tls.c | 24 - src/cHTTPX_websocket.c | 833 +++++++++++++++++++++++++- src/cHTTPX_websocket.h | 245 ++++++-- src/cHTTPX_worker.c | 39 -- tests/test_compression.c | 4 - tests/test_core.c | 2 +- tests/test_server.c | 9 - tests/test_websocket.c | 101 ++++ 38 files changed, 1899 insertions(+), 764 deletions(-) create mode 100644 example/websocket.c create mode 100644 tests/test_websocket.c diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 99c5c04..3146c7f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,8 +14,6 @@ body: label: Operating system options: - Linux - - Windows - - macOS - Other validations: required: true @@ -33,7 +31,7 @@ body: id: compiler attributes: label: Compiler - placeholder: gcc 13, MSVC 2022, clang 18, etc. + placeholder: gcc 13, clang 18, etc. validations: required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b1a32ed..54fd238 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,7 +19,7 @@ Use a **conventional commit** title — it becomes the release version on merge: - [ ] PR title follows the table above - [ ] Code builds on Linux (`make lin` and `make libchttpx.so`) -- [ ] Code builds on Windows (`make win`) if applicable +- [ ] Code builds on Linux (`make lin`) if applicable - [ ] Source follows `.clang-format` and keeps logical blocks readable - [ ] ASan/UBSan pass; TSan passes for concurrency-sensitive changes - [ ] Parser/network changes include negative-path tests and fuzz coverage where applicable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8bfdb7..1d38c81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ Changes must pass: - formatting check; - Linux build and unit/integration tests; -- Windows build/tests when the change is cross-platform; +- Linux build/tests for behavioral changes; - ASan/UBSan; - TSan for concurrency-sensitive code; - parser fuzz smoke tests; diff --git a/Makefile b/Makefile index 35effab..eeb47e8 100644 --- a/Makefile +++ b/Makefile @@ -37,10 +37,11 @@ FUZZ_HEADERS_TARGET = $(BINDIR)/fuzz_headers TEST_TLS_TARGET = $(BINDIR)/test_tls TEST_COMPRESSION_TARGET = $(BINDIR)/test_compression TEST_METRICS_TARGET = $(BINDIR)/test_metrics +TEST_WEBSOCKET_TARGET = $(BINDIR)/test_websocket EXAMPLE_SRC = example/basic.c EXAMPLE_OBJ = $(OBJDIR)/example/basic.o -EXAMPLE_NAMES = basic multiple_servers local_call remote_call middleware json upload metrics +EXAMPLE_NAMES = basic multiple_servers local_call remote_call middleware json upload metrics websocket EXAMPLE_TARGETS = $(addprefix $(BINDIR)/example-,$(EXAMPLE_NAMES)) LIN_SRCS = $(wildcard src/*.c) @@ -100,10 +101,11 @@ lib-install: libchttpx.so # Linux tests # - -test: $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) +test: $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) $(TEST_WEBSOCKET_TARGET) $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) + $(TEST_WEBSOCKET_TARGET) $(TEST_TARGET): tests/test_core.c $(LIN_SRCS) @mkdir -p $(BINDIR) @@ -156,6 +158,10 @@ $(TEST_METRICS_TARGET): tests/test_metrics.c $(LIN_SRCS) @mkdir -p $(BINDIR) $(CC) $(CFLAGS) -std=gnu11 -O2 -g tests/test_metrics.c $(LIN_SRCS) -o $@ $(LIN_LDFLAGS) -pthread +$(TEST_WEBSOCKET_TARGET): tests/test_websocket.c $(LIN_SRCS) + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -std=gnu11 -O2 -g tests/test_websocket.c $(LIN_SRCS) -o $@ $(LIN_LDFLAGS) -pthread + # Linux lib compile # - diff --git a/docs/json/README.md b/docs/json/README.md index 2fc5840..71d3e16 100644 --- a/docs/json/README.md +++ b/docs/json/README.md @@ -52,12 +52,15 @@ static void create_user( ), }; - if (!cHTTPX_BindJSON( - req, - res, - fields, - CHTTPX_ARRAY_LEN(fields))) + int bind = cHTTPX_BindJSON(req, fields, CHTTPX_ARRAY_LEN(fields)); + if (bind != cHTTPX_BIND_OK) + { + if (bind == cHTTPX_BIND_REQUIRED) + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, req->error_field); + else + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, "invalid request body"); return; + } *res = cHTTPX_ResMessage( cHTTPX_StatusCreated, @@ -66,7 +69,7 @@ static void create_user( } ``` -On failure, `BindJSON` returns 0 and builds a safe HTTP 400 response. +`BindJSON` does not write the HTTP response. `0` is success. On failure it returns a code such as `cHTTPX_BIND_REQUIRED` (`1`) and sets `req->error_field` / `req->error_num`. ## Normalizers diff --git a/docs/json/README_RU.md b/docs/json/README_RU.md index 338bf90..c5755c0 100644 --- a/docs/json/README_RU.md +++ b/docs/json/README_RU.md @@ -38,12 +38,15 @@ static void create_user( ), }; - if (!cHTTPX_BindJSON( - req, - res, - fields, - CHTTPX_ARRAY_LEN(fields))) + int bind = cHTTPX_BindJSON(req, fields, CHTTPX_ARRAY_LEN(fields)); + if (bind != cHTTPX_BIND_OK) + { + if (bind == cHTTPX_BIND_REQUIRED) + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, req->error_field); + else + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, "invalid request body"); return; + } *res = cHTTPX_ResMessage( cHTTPX_StatusCreated, @@ -52,7 +55,7 @@ static void create_user( } ``` -При ошибке `BindJSON` возвращает 0 и формирует безопасный 400 response. +`BindJSON` сам HTTP-ответ не пишет. `0` — успех. При ошибке возвращается код, например `cHTTPX_BIND_REQUIRED` (`1`), плюс `req->error_field` / `req->error_num`. ## Normalizers diff --git a/docs/server/README.md b/docs/server/README.md index 2ea17c3..78c99a3 100644 --- a/docs/server/README.md +++ b/docs/server/README.md @@ -136,7 +136,7 @@ Trigger shutdown from an appropriate control thread for SIGINT/SIGTERM. Avoid co ## Event-driven runtime and worker pool -Sockets are non-blocking and stay on the server event loop instead of occupying worker threads while waiting for network I/O. Linux uses `epoll`, macOS/BSD uses `kqueue`, and Windows uses the non-blocking `WSAPoll` backend. +Sockets are non-blocking and stay on the server event loop instead of occupying worker threads while waiting for network I/O. Linux uses `epoll`. A connection is submitted to the internal fixed pool of **32 worker threads** only after the complete HTTP request has been received. Workers execute middleware, routing and the application handler; they do not call `recv()` or `send()`. The serialized response is returned to the event loop and written when the socket is ready. diff --git a/docs/server/README_RU.md b/docs/server/README_RU.md index 71ec194..61ed64d 100644 --- a/docs/server/README_RU.md +++ b/docs/server/README_RU.md @@ -125,7 +125,7 @@ cHTTPX_AppShutdown(&app); ## Event-driven runtime и worker pool -Sockets работают в non-blocking режиме и остаются на server event loop, поэтому worker thread больше не занят ожиданием сетевого I/O. На Linux используется `epoll`, на macOS/BSD — `kqueue`, на Windows — non-blocking backend на `WSAPoll`. +Sockets работают в non-blocking режиме и остаются на server event loop, поэтому worker thread больше не занят ожиданием сетевого I/O. На Linux используется `epoll`. Connection попадает во внутренний фиксированный пул из **32 worker threads** только после того, как HTTP request полностью принят. Worker выполняет middleware, routing и application handler и не вызывает `recv()` или `send()`. Готовый сериализованный response возвращается в event loop и отправляется только когда socket готов к записи. diff --git a/docs/site/examples.html b/docs/site/examples.html index 6b090c9..50fa100 100644 --- a/docs/site/examples.html +++ b/docs/site/examples.html @@ -111,7 +111,8 @@

Bind and validate JSON

cHTTPX_StringField("email", &payload.email, true, 3, 254, cHTTPX_TRIM | cHTTPX_LOWERCASE, NULL) }; -if (!cHTTPX_BindJSON(req, res, fields, CHTTPX_ARRAY_LEN(fields))) +int bind = cHTTPX_BindJSON(req, fields, CHTTPX_ARRAY_LEN(fields)); +if (bind != cHTTPX_BIND_OK) return; chttpx_json_t* json = cHTTPX_JsonObject(req); diff --git a/docs/websocket/README.md b/docs/websocket/README.md index 1c98fde..bd17ec5 100644 --- a/docs/websocket/README.md +++ b/docs/websocket/README.md @@ -1,53 +1,62 @@ -# WebSocket API — experimental +# WebSocket -> **Status: experimental / beta. Do not treat the current implementation as production-ready.** +libchttpx supports WebSocket over HTTP/2 using the Extended CONNECT mechanism from RFC 8441. The server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL=1`, accepts `:method = CONNECT` with `:protocol = websocket`, and then carries normal RFC 6455 WebSocket frames inside HTTP/2 DATA frames. -The repository contains `include/websocket.h` and `src/websocket.c`, but the current implementation is incomplete. +There is intentionally no HTTP/1.1 `Upgrade: websocket` fallback in the HTTP/2-only server. -Current limitations: +## Server route -- `src/websocket.c` is explicitly marked beta; -- the handshake does not currently calculate SHA-1 + Base64 for `Sec-WebSocket-Accept`; -- frame parsing/send/receive functionality is incomplete; -- `websocket.h` is not included by the umbrella `libchttpx.h` header. +```c +#include + +static void on_message(chttpx_wsocket_t* ws, const unsigned char* data, size_t len) +{ + if (ws->opcode == CHTTPX_WSOCKET_OPCODE_TEXT) + cHTTPX_WSocketSend(ws, data, len); + else + cHTTPX_WSocketSendBinary(ws, data, len); +} + +static void on_close(chttpx_wsocket_t* ws, uint16_t code, const unsigned char* reason, size_t len) +{ + (void)ws; + (void)code; + (void)reason; + (void)len; +} + +static void chat_socket(chttpx_wsocket_t* ws) +{ + /* ws->request contains the HTTP/2 handshake headers. */ + cHTTPX_WSocketOnMessage(ws, on_message); + cHTTPX_WSocketOnClose(ws, on_close); +} + +chttpx_router_t router = cHTTPX_RoutePathPrefix(server, "/api"); +cHTTPX_WSocketRegisterRoute(&router, "/chat", chat_socket); +``` -This page documents the current surface only so users do not mistake it for stable functionality. +The resulting endpoint is `/api/chat`. Query parameters are ignored only for route matching; the complete path remains available through `ws->request->path`. -## Current types +## API -`wsocket_frame_t` contains FIN, opcode, mask information, payload length, mask bytes, and payload. +- `cHTTPX_WSocketRegisterRoute()` registers a WebSocket endpoint. +- `cHTTPX_WSocketOnMessage()` installs the text/binary message callback. +- `cHTTPX_WSocketOnClose()` installs the close callback. +- `cHTTPX_WSocketSend()` sends one text message. +- `cHTTPX_WSocketSendBinary()` sends one binary message. +- `cHTTPX_WSocketPing()` sends a ping control frame. +- `cHTTPX_WSocketClose()` starts an orderly close and ends the HTTP/2 stream. +- `cHTTPX_WSocketSetData()` / `cHTTPX_WSocketData()` attach application state to a connection. -`chttpx_wsocket_t` currently stores socket/connection state. +Inside the message callback, `ws->opcode` is `CHTTPX_WSOCKET_OPCODE_TEXT` or `CHTTPX_WSOCKET_OPCODE_BINARY`. -## Current declarations +## Protocol handling -```c -void cHTTPX_WSocketRegisterRoute( - chttpx_router_t* r, - const char* path, - chttpx_wsocket_route_t handler -); - -int cHTTPX_WSocketUpgrade( - int client_socket, - const char* sec_wsocket_key -); - -int cHTTPX_WSocketSend( - chttpx_wsocket_t* wsocket, - const unsigned char* data, - size_t len -); - -int cHTTPX_WSocketRecv( - chttpx_wsocket_t* wsocket, - unsigned char* buffer, - size_t len -); -``` +The implementation supports masked client frames, 16-bit and 64-bit payload lengths, fragmented text/binary messages, automatic PONG replies, close frames, UTF-8 validation for text messages and close reasons, and configured message-size limits. Invalid protocol input is closed with the appropriate WebSocket close code. -Opcode constants include continuation, text, binary, close, ping, and pong. +A WebSocket handshake must use `Sec-WebSocket-Version: 13`. The HTTP/1.1-oriented `Sec-WebSocket-Key` / `Sec-WebSocket-Accept` exchange is not used by RFC 8441 Extended CONNECT. -## Recommendation +## Legacy functions -Use normal HTTP routes for production. WebSocket should be considered unfinished until handshake validation, frame parsing, masking, fragmentation, control frames, close lifecycle, tests, and public-header integration are completed. +`cHTTPX_WSocketUpgrade()` and synchronous `cHTTPX_WSocketRecv()` are retained for source compatibility but return `cHTTPX_ERR_UNAVAILABLE`. WebSocket handling is event-driven so one open socket does not block other multiplexed HTTP/2 streams. diff --git a/docs/websocket/README_RU.md b/docs/websocket/README_RU.md index a6807af..0185488 100644 --- a/docs/websocket/README_RU.md +++ b/docs/websocket/README_RU.md @@ -1,41 +1,62 @@ -# WebSocket API — experimental +# WebSocket -> **Статус: experimental / beta. Текущую реализацию нельзя считать production-ready.** +libchttpx поддерживает WebSocket поверх HTTP/2 через Extended CONNECT из RFC 8441. Сервер объявляет `SETTINGS_ENABLE_CONNECT_PROTOCOL=1`, принимает `:method = CONNECT` вместе с `:protocol = websocket`, после чего обычные WebSocket-фреймы RFC 6455 передаются внутри HTTP/2 DATA. -В репозитории есть `websocket.h` и `websocket.c`, но функционал пока неполный. +Fallback на HTTP/1.1 `Upgrade: websocket` специально не добавляется, потому что сервер в `develop` работает как HTTP/2-only. -Текущие ограничения: - -- implementation прямо помечена как beta; -- handshake пока не вычисляет SHA-1 + Base64 для `Sec-WebSocket-Accept`; -- frame parsing/send/recv реализованы не полностью; -- WebSocket API не входит в обычный umbrella public header. - -Существующие declarations: +## WebSocket-маршрут ```c -void cHTTPX_WSocketRegisterRoute( - chttpx_router_t* r, - const char* path, - chttpx_wsocket_route_t handler -); - -int cHTTPX_WSocketUpgrade( - int client_socket, - const char* sec_wsocket_key -); - -int cHTTPX_WSocketSend( - chttpx_wsocket_t* wsocket, - const unsigned char* data, - size_t len -); - -int cHTTPX_WSocketRecv( - chttpx_wsocket_t* wsocket, - unsigned char* buffer, - size_t len -); +#include + +static void on_message(chttpx_wsocket_t* ws, const unsigned char* data, size_t len) +{ + if (ws->opcode == CHTTPX_WSOCKET_OPCODE_TEXT) + cHTTPX_WSocketSend(ws, data, len); + else + cHTTPX_WSocketSendBinary(ws, data, len); +} + +static void on_close(chttpx_wsocket_t* ws, uint16_t code, const unsigned char* reason, size_t len) +{ + (void)ws; + (void)code; + (void)reason; + (void)len; +} + +static void chat_socket(chttpx_wsocket_t* ws) +{ + /* В ws->request доступны заголовки HTTP/2 handshake. */ + cHTTPX_WSocketOnMessage(ws, on_message); + cHTTPX_WSocketOnClose(ws, on_close); +} + +chttpx_router_t router = cHTTPX_RoutePathPrefix(server, "/api"); +cHTTPX_WSocketRegisterRoute(&router, "/chat", chat_socket); ``` -Перед production use нужно завершить handshake validation, frame parser, masking, fragmentation, control frames, close lifecycle и tests. +Итоговый endpoint — `/api/chat`. Query string не участвует только в поиске маршрута; полный путь остаётся доступен через `ws->request->path`. + +## API + +- `cHTTPX_WSocketRegisterRoute()` — регистрация WebSocket endpoint. +- `cHTTPX_WSocketOnMessage()` — callback для text/binary сообщений. +- `cHTTPX_WSocketOnClose()` — callback закрытия. +- `cHTTPX_WSocketSend()` — отправка text message. +- `cHTTPX_WSocketSendBinary()` — отправка binary message. +- `cHTTPX_WSocketPing()` — отправка PING. +- `cHTTPX_WSocketClose()` — корректное закрытие WebSocket и HTTP/2 stream. +- `cHTTPX_WSocketSetData()` / `cHTTPX_WSocketData()` — пользовательский context соединения. + +В `OnMessage` поле `ws->opcode` содержит `CHTTPX_WSOCKET_OPCODE_TEXT` или `CHTTPX_WSOCKET_OPCODE_BINARY`. + +## Что обрабатывает библиотека + +Реализованы client masking, payload lengths 16/64 bit, fragmentation, автоматический PONG, close frames, UTF-8 validation для text/close reason и ограничение размера сообщения через настройки сервера. При нарушении протокола библиотека закрывает WebSocket соответствующим close code. + +Handshake должен передавать `Sec-WebSocket-Version: 13`. HTTP/1.1-механизм с `Sec-WebSocket-Key` / `Sec-WebSocket-Accept` в RFC 8441 Extended CONNECT не используется. + +## Старый API + +`cHTTPX_WSocketUpgrade()` и синхронный `cHTTPX_WSocketRecv()` оставлены для source compatibility, но возвращают `cHTTPX_ERR_UNAVAILABLE`. Основной API событийный, чтобы одно WebSocket-соединение не блокировало остальные HTTP/2 streams. diff --git a/example/README.md b/example/README.md index 56f3828..7395b63 100644 --- a/example/README.md +++ b/example/README.md @@ -22,4 +22,4 @@ Generated binaries are placed in `.build/`: - `example-json` - `example-upload` -The normal `make lin` / `make win` demo target uses `example/basic.c`. +The normal `make lin` demo target uses `example/basic.c`. diff --git a/example/json.c b/example/json.c index 5b2f183..fef8f2c 100644 --- a/example/json.c +++ b/example/json.c @@ -16,8 +16,16 @@ static void create_user(chttpx_request_t* req, chttpx_response_t* res) cHTTPX_StringField("name", &payload.name, true, 1, 64, cHTTPX_TRIM, NULL), }; - if (!cHTTPX_BindJSON(req, res, fields, CHTTPX_ARRAY_LEN(fields))) + int bind = cHTTPX_BindJSON(req, fields, CHTTPX_ARRAY_LEN(fields)); + if (bind != cHTTPX_BIND_OK) + { + if (bind == cHTTPX_BIND_REQUIRED) + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, req->error_field); + else + *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, "invalid request body"); + return; + } chttpx_json_t* json = cHTTPX_JsonObject(req); cHTTPX_JsonString(json, "email", payload.email); diff --git a/example/websocket.c b/example/websocket.c new file mode 100644 index 0000000..382c5e9 --- /dev/null +++ b/example/websocket.c @@ -0,0 +1,50 @@ +#include + +#include + +static void on_message(chttpx_wsocket_t* websocket, const unsigned char* data, size_t len) +{ + if (websocket->opcode == CHTTPX_WSOCKET_OPCODE_TEXT) + cHTTPX_WSocketSend(websocket, data, len); + else + cHTTPX_WSocketSendBinary(websocket, data, len); +} + +static void on_close(chttpx_wsocket_t* websocket, uint16_t code, const unsigned char* reason, size_t len) +{ + (void)websocket; + (void)reason; + (void)len; + printf("websocket closed: %u\n", code); +} + +static void websocket_route(chttpx_wsocket_t* websocket) +{ + cHTTPX_WSocketOnMessage(websocket, on_message); + cHTTPX_WSocketOnClose(websocket, on_close); +} + +int main(void) +{ + chttpx_app_t app; + if (cHTTPX_AppInit(&app) != cHTTPX_OK) + return 1; + + chttpx_config_t config = cHTTPX_DefaultConfig(); + config.port = 8080; + + chttpx_serv_t* server = cHTTPX_AppServer(&app, "main", &config); + if (!server) + { + cHTTPX_AppShutdown(&app); + return 1; + } + + chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); + cHTTPX_WSocketRegisterRoute(&router, "/ws", websocket_route); + + printf("WebSocket endpoint: /ws\n"); + int result = cHTTPX_AppRun(&app); + cHTTPX_AppShutdown(&app); + return result == cHTTPX_OK ? 0 : 1; +} diff --git a/include/libchttpx.h b/include/libchttpx.h index 151ba69..14a9033 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -10,7 +10,6 @@ #ifndef LIBCHTTPX_H #define LIBCHTTPX_H - /* ========================================================================== */ /* cHTTPX_crosspltm.h */ /* ========================================================================== */ @@ -22,111 +21,49 @@ extern "C" { #endif -#include - -#if defined(_WIN32) || defined(_WIN64) -#define CHTTPX_PLATFORM_WINDOWS -#else -#define CHTTPX_PLATFORM_POSIX +#if !defined(__linux__) +#error "libchttpx supports Linux only" #endif -#ifdef CHTTPX_PLATFORM_WINDOWS -#define strdup _strdup -#else -#define strdup strdup -#endif +#include +#include +#include +#include +#include +#include -#ifdef CHTTPX_PLATFORM_WINDOWS -#define chttpx_close(s) closesocket(s) -#else +#define CHTTPX_PLATFORM_POSIX #define chttpx_close(s) close(s) -#endif - -#ifdef CHTTPX_PLATFORM_WINDOWS -#include -#include -#include -#include -#endif -#ifdef _WIN32 - typedef SOCKET chttpx_socket_t; -#else typedef int chttpx_socket_t; -#endif -#ifdef CHTTPX_PLATFORM_WINDOWS - static inline struct tm* localtime_r(const time_t* timep, struct tm* result) - { - memset(result, 0, sizeof(*result)); - localtime_s(result, timep); - return result; - } +/** + * Find a byte substring within a buffer (POSIX memmem when unavailable). + * + * @param haystack Buffer to search. + * @param haystacklen Length of haystack. + * @param needle Substring to find. + * @param needlelen Length of needle. + * @return Pointer into haystack, or NULL if not found. + */ +static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) +{ + if (!needlelen) + return (void*)haystack; + if (needlelen > haystacklen) + return NULL; - static inline struct tm* gmtime_r(const time_t* timep, struct tm* result) - { - memset(result, 0, sizeof(*result)); - gmtime_s(result, timep); - return result; - } + const unsigned char* h = haystack; + const unsigned char* n = needle; - static inline int chttpx_clock_gettime(int clock_id, struct timespec* value) + for (size_t i = 0; i <= haystacklen - needlelen; i++) { - if (!value) - return -1; - if (clock_id == CLOCK_MONOTONIC) - { - LARGE_INTEGER frequency; - LARGE_INTEGER counter; - QueryPerformanceFrequency(&frequency); - QueryPerformanceCounter(&counter); - value->tv_sec = (time_t)(counter.QuadPart / frequency.QuadPart); - value->tv_nsec = (long)(((counter.QuadPart % frequency.QuadPart) * 1000000000LL) / frequency.QuadPart); - return 0; - } - FILETIME file_time; - ULARGE_INTEGER ticks; - GetSystemTimeAsFileTime(&file_time); - ticks.LowPart = file_time.dwLowDateTime; - ticks.HighPart = file_time.dwHighDateTime; - unsigned long long unix_ticks = ticks.QuadPart - 116444736000000000ULL; - value->tv_sec = (time_t)(unix_ticks / 10000000ULL); - value->tv_nsec = (long)((unix_ticks % 10000000ULL) * 100ULL); - return 0; + if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) + return (void*)(h + i); } -#define clock_gettime chttpx_clock_gettime -#endif - -#ifdef CHTTPX_PLATFORM_POSIX -#include -#include -#include -#include -#endif - -#ifdef CHTTPX_PLATFORM_WINDOWS -#define strcasecmp _stricmp -#endif - - static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) - { - if (!needlelen) - return (void*)haystack; - if (needlelen > haystacklen) - return NULL; - - const unsigned char* h = haystack; - const unsigned char* n = needle; - - for (size_t i = 0; i <= haystacklen - needlelen; i++) - { - if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) - return (void*)(h + i); - } - - return NULL; - } + return NULL; +} #ifdef __cplusplus } @@ -134,7 +71,6 @@ typedef int chttpx_socket_t; #endif - /* ========================================================================== */ /* cHTTPX_http.h */ /* ========================================================================== */ @@ -319,7 +255,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_request.h */ /* ========================================================================== */ @@ -338,7 +273,6 @@ extern "C" { #endif - #include #include #include @@ -409,6 +343,26 @@ extern "C" FIELD_NUMBER_ARRAY } validation_t; + /** + * Result of cHTTPX_Parse, cHTTPX_Validate, and cHTTPX_BindJSON. + * + * Zero means success. Positive values name the failure. The failing + * field, if any, is stored in req->error_field. + */ + typedef enum + { + cHTTPX_BIND_OK = 0, + cHTTPX_BIND_REQUIRED = 1, + cHTTPX_BIND_MIN_LENGTH = 2, + cHTTPX_BIND_MAX_LENGTH = 3, + cHTTPX_BIND_INVALID_EMAIL = 4, + cHTTPX_BIND_GENERIC = 5, + cHTTPX_BIND_INVALID_JSON = 6, + cHTTPX_BIND_TYPE = 7, + cHTTPX_BIND_MEMORY = 8, + cHTTPX_BIND_INVALID_ARGUMENT = 9 + } chttpx_bind_error_t; + typedef struct { char** items; @@ -522,6 +476,11 @@ extern "C" /* Error REQuest message */ char error_msg[BUFFER_SIZE]; + /* Bind/validate failure: code, field name, and optional numeric detail. */ + int error_code; + char error_field[MAX_PARAM_NAME]; + size_t error_num; + /* Request metadata */ char request_id[65]; char language[16]; @@ -683,7 +642,7 @@ extern "C" * @param req Current HTTP request. * @param fields Field definitions and output targets. * @param field_count Number of entries in fields. - * @return 1 on success or 0 on parse/type/allocation failure. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); @@ -693,25 +652,23 @@ extern "C" * @param req Current HTTP request. * @param fields Field definitions and parsed targets. * @param field_count Number of entries in fields. - * @param l Language code used for validation messages. - * @return 1 when all values pass validation, otherwise 0. + * @param l Unused; kept for source compatibility. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count, const char* l); - struct chttpx_response; /** * Parse and validate a JSON request body. * - * Parsed strings and arrays are request-owned. On failure this function - * creates a safe JSON 400 response in res. + * Does not write an HTTP response. On failure inspect req->error_code, + * req->error_field, and req->error_num, then build the response yourself. * * @param req Current HTTP request. - * @param res Response populated when binding fails. * @param fields Field definitions and output targets. * @param field_count Number of field definitions. - * @return 1 on success, 0 on parsing or validation failure. + * @return cHTTPX_BIND_OK or a positive bind error code. */ - int cHTTPX_BindJSON(chttpx_request_t* req, struct chttpx_response* res, chttpx_validation_t* fields, size_t field_count); + int cHTTPX_BindJSON(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); /** * Macro to define a string field for JSON request validation. @@ -777,7 +734,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_response.h */ /* ========================================================================== */ @@ -796,7 +752,6 @@ extern "C" { #endif - #include struct chttpx_serv; @@ -938,7 +893,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_middlewares.h */ /* ========================================================================== */ @@ -957,8 +911,6 @@ extern "C" { #endif - - #include #include @@ -1069,7 +1021,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_cors.h */ /* ========================================================================== */ @@ -1130,7 +1081,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_serv.h */ /* ========================================================================== */ @@ -1149,9 +1099,6 @@ extern "C" { #endif - - - #include #include #include @@ -1199,7 +1146,6 @@ extern "C" #define CHTTPX_ERR_COMPRESSION cHTTPX_ERR_COMPRESSION #endif - typedef enum { cHTTPX_LOG_DEBUG, @@ -1336,6 +1282,7 @@ extern "C" void* compression_state; void* metrics_state; void* runtime_state; + void* websocket_state; chttpx_cors_t cors; } chttpx_serv_t; @@ -1443,14 +1390,12 @@ extern "C" /** Enable or disable compression for one response. */ void cHTTPX_ResponseCompression(chttpx_response_t* response, bool enabled); - #ifdef __cplusplus } #endif #endif - /* ========================================================================== */ /* cHTTPX_metrics.h */ /* ========================================================================== */ @@ -1507,7 +1452,6 @@ extern "C" uint64_t queue_wait_nanoseconds_total; } chttpx_runtime_metrics_t; - /** * Copy the current server metrics into a caller-owned snapshot. * @@ -1548,7 +1492,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_app.h */ /* ========================================================================== */ @@ -1566,7 +1509,6 @@ extern "C" { #endif - #include #include @@ -1663,7 +1605,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_inet.h */ /* ========================================================================== */ @@ -1682,7 +1623,6 @@ extern "C" { #endif - /** * Get client IP address from the underlying socket connection. * @@ -1708,7 +1648,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_params.h */ /* ========================================================================== */ @@ -1727,7 +1666,6 @@ extern "C" { #endif - /** * Get a route parameter value by its name. * @param req Pointer to the current HTTP request structure. @@ -1752,7 +1690,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_queries.h */ /* ========================================================================== */ @@ -1771,7 +1708,6 @@ extern "C" { #endif - /** * Get a query parameter value by name. * @@ -1822,7 +1758,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_headers.h */ /* ========================================================================== */ @@ -1841,8 +1776,6 @@ extern "C" { #endif - - /** * Get a request header by name. * @param req Pointer to the HTTP request. @@ -1894,7 +1827,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_cookies.h */ /* ========================================================================== */ @@ -1906,8 +1838,6 @@ extern "C" { #endif - - /* Parse cookie in request */ void _parse_req_cookies(chttpx_request_t* req); @@ -1952,7 +1882,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_i18n.h */ /* ========================================================================== */ @@ -2070,7 +1999,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_media.h */ /* ========================================================================== */ @@ -2089,8 +2017,6 @@ extern "C" { #endif - - #define FILE_BUFFER 65536 typedef struct @@ -2135,7 +2061,6 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_json.h */ /* ========================================================================== */ @@ -2147,8 +2072,6 @@ extern "C" { #endif - - typedef struct chttpx_json chttpx_json_t; /** Escape a string for JSON. The returned string is owned by the request arena. */ @@ -2199,62 +2122,192 @@ extern "C" #endif - /* ========================================================================== */ /* cHTTPX_websocket.h */ /* ========================================================================== */ /** - * Copyright (c) 2026 netcorelink - * - * This library is free software; you can redistribute it and/or modify it - * under the terms of the MIT license. See `libchttpx.c` for details. + * WebSocket over HTTP/2 (RFC 8441) API. */ +#ifdef __cplusplus +extern "C" +{ +#endif -#include - +/** WebSocket continuation frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_CONTINUATION 0x0 +/** WebSocket text frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_TEXT 0x1 +/** WebSocket binary frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_BINARY 0x2 +/** WebSocket close frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_CLOSE 0x8 +/** WebSocket ping frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_PING 0x9 +/** WebSocket pong frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_PONG 0xA +/** + * Parsed WebSocket frame metadata and payload. + * + * Used by legacy or diagnostic paths that expose a decoded frame. + */ typedef struct { - /* FIN - final fragment - * 1 eq. this is the last frame of the message - * 0 eq. the message is divided into several parts - */ int fin; - /* Check define CHTTPX_WSOCKET_OPCODE */ int opcode; int masked; - /* Length data in payload */ uint64_t payload_len; - /* Mask for XOR payload */ unsigned char mask[4]; - /* Data in socket */ unsigned char* payload; } wsocket_frame_t; -typedef struct +/** + * Active WebSocket connection handle. + * + * Created by the HTTP/2 Extended CONNECT bridge. Application code registers + * message and close callbacks, then sends frames through the public helpers. + */ +typedef struct chttpx_wsocket { - int socket; + chttpx_socket_t socket; int connected; + int opcode; + chttpx_request_t* request; + void* user_data; + void* _internal; } chttpx_wsocket_t; +/** + * Callback invoked for a complete text or binary message. + * + * @param wsocket Active WebSocket connection. + * @param data Message payload bytes. + * @param len Payload length in bytes. + */ typedef void (*chttpx_wsocket_handler_t)(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); +/** + * Callback invoked when the peer closes or the connection is aborted. + * + * @param wsocket Active WebSocket connection. + * @param code Close status code. + * @param reason Optional UTF-8 close reason. + * @param len Reason length in bytes. + */ +typedef void (*chttpx_wsocket_close_handler_t)(chttpx_wsocket_t* wsocket, uint16_t code, const unsigned char* reason, size_t len); + +/** + * Route entry callback invoked after a successful WebSocket handshake. + * + * @param wsocket Newly accepted WebSocket connection. + */ typedef void (*chttpx_wsocket_route_t)(chttpx_wsocket_t* wsocket); -void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, chttpx_wsocket_route_t handler); +/** + * Register a WebSocket route on a router path prefix. + * + * @param router Target router. + * @param path Path relative to the router prefix. + * @param handler Callback invoked after handshake success. + */ +void cHTTPX_WSocketRegisterRoute(chttpx_router_t* router, const char* path, chttpx_wsocket_route_t handler); -int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); +/** + * Set the message handler for an active WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param handler Callback for complete text or binary messages. + */ +void cHTTPX_WSocketOnMessage(chttpx_wsocket_t* wsocket, chttpx_wsocket_handler_t handler); + +/** + * Set the close handler for an active WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param handler Callback for peer close or abrupt abort. + */ +void cHTTPX_WSocketOnClose(chttpx_wsocket_t* wsocket, chttpx_wsocket_close_handler_t handler); +/** + * Attach opaque application data to a WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param user_data Pointer stored on the connection. + */ +void cHTTPX_WSocketSetData(chttpx_wsocket_t* wsocket, void* user_data); + +/** + * Return opaque application data previously attached to a WebSocket. + * + * @param wsocket Active WebSocket connection. + * @return Stored user data pointer, or NULL. + */ +void* cHTTPX_WSocketData(chttpx_wsocket_t* wsocket); + +/** + * Send a text WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data UTF-8 payload bytes. + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ int cHTTPX_WSocketSend(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); +/** + * Send a binary WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data Binary payload bytes. + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketSendBinary(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + +/** + * Send a WebSocket ping frame. + * + * @param wsocket Active WebSocket connection. + * @param data Optional ping payload. + * @param len Payload length in bytes (at most 125). + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketPing(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + +/** + * Send a WebSocket close frame and mark the connection closing. + * + * @param wsocket Active WebSocket connection. + * @param code Close status code, or 0 for 1000. + * @param reason Optional UTF-8 close reason. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketClose(chttpx_wsocket_t* wsocket, uint16_t code, const char* reason); + +/** + * Legacy HTTP/1.1 upgrade helper retained for source compatibility. + * + * Always returns unavailable: WebSockets are served over HTTP/2 Extended CONNECT. + * + * @param client_socket Unused client socket. + * @param sec_wsocket_key Unused Sec-WebSocket-Key value. + * @return Always cHTTPX_ERR_UNAVAILABLE. + */ +int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); + +/** + * Legacy synchronous receive helper retained for source compatibility. + * + * @param wsocket Unused connection handle. + * @param buffer Unused output buffer. + * @param len Unused buffer capacity. + * @return Always cHTTPX_ERR_UNAVAILABLE. + */ int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len); +#ifdef __cplusplus +} +#endif #endif /* LIBCHTTPX_H */ diff --git a/src/cHTTPX_app.c b/src/cHTTPX_app.c index 1fb923f..9d0ea70 100644 --- a/src/cHTTPX_app.c +++ b/src/cHTTPX_app.c @@ -19,10 +19,8 @@ #include #include -#ifdef CHTTPX_PLATFORM_POSIX #include #include -#endif #define CHTTPX_CALL_TIMEOUT_SEC 30 @@ -177,12 +175,6 @@ int cHTTPX_AppInit(chttpx_app_t* app) memset(app, 0, sizeof(*app)); -#ifdef CHTTPX_PLATFORM_WINDOWS - WSADATA wsa; - if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) - return cHTTPX_ERR_SOCKET; -#endif - app->_network_initialized = true; app->_initialized = true; return cHTTPX_OK; @@ -223,7 +215,6 @@ chttpx_serv_t* cHTTPX_AppServer(chttpx_app_t* app, const char* name, const chttp return server; } - /** * Return default outbound TLS client settings. * @@ -450,11 +441,6 @@ void cHTTPX_AppShutdown(chttpx_app_t* app) } free(app->_remotes); -#ifdef CHTTPX_PLATFORM_WINDOWS - if (app->_network_initialized) - WSACleanup(); -#endif - memset(app, 0, sizeof(*app)); } @@ -516,11 +502,7 @@ static int local_call(chttpx_request_t* source, chttpx_serv_t* target, const cha chttpx_request_t internal = {0}; internal._server = target; -#ifdef CHTTPX_PLATFORM_WINDOWS - internal.client_fd = INVALID_SOCKET; -#else internal.client_fd = -1; -#endif internal.method = strdup(method); internal.path = strdup(path); @@ -694,11 +676,7 @@ static int parse_remote_url(const char* url, chttpx_remote_url_t* parsed) */ static int socket_last_error(void) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return WSAGetLastError(); -#else return errno; -#endif } /** @@ -709,11 +687,7 @@ static int socket_last_error(void) */ static bool socket_error_is_timeout(int error) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return error == WSAETIMEDOUT || error == WSAEWOULDBLOCK; -#else return error == ETIMEDOUT || error == EAGAIN || error == EWOULDBLOCK; -#endif } /** @@ -724,11 +698,7 @@ static bool socket_error_is_timeout(int error) */ static bool socket_error_is_in_progress(int error) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return error == WSAEWOULDBLOCK || error == WSAEINPROGRESS; -#else return error == EINPROGRESS || error == EWOULDBLOCK; -#endif } /** @@ -740,10 +710,6 @@ static bool socket_error_is_in_progress(int error) */ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) { -#ifdef CHTTPX_PLATFORM_WINDOWS - u_long mode = enabled ? 1UL : 0UL; - return ioctlsocket(socket_fd, FIONBIO, &mode) == 0 ? cHTTPX_OK : cHTTPX_ERR_UNAVAILABLE; -#else int flags = fcntl(socket_fd, F_GETFL, 0); if (flags < 0) return cHTTPX_ERR_UNAVAILABLE; @@ -754,7 +720,6 @@ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) flags &= ~O_NONBLOCK; return fcntl(socket_fd, F_SETFL, flags) == 0 ? cHTTPX_OK : cHTTPX_ERR_UNAVAILABLE; -#endif } /** @@ -765,19 +730,11 @@ static int socket_set_nonblocking(chttpx_socket_t socket_fd, bool enabled) */ static int socket_set_call_timeouts(chttpx_socket_t socket_fd) { -#ifdef CHTTPX_PLATFORM_WINDOWS - DWORD timeout_ms = CHTTPX_CALL_TIMEOUT_SEC * 1000U; - if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) - return cHTTPX_ERR_UNAVAILABLE; - if (setsockopt(socket_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) - return cHTTPX_ERR_UNAVAILABLE; -#else struct timeval timeout = {.tv_sec = CHTTPX_CALL_TIMEOUT_SEC, .tv_usec = 0}; if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0) return cHTTPX_ERR_UNAVAILABLE; if (setsockopt(socket_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) return cHTTPX_ERR_UNAVAILABLE; -#endif return cHTTPX_OK; } @@ -798,11 +755,7 @@ static int wait_for_connect(chttpx_socket_t socket_fd) struct timeval timeout = {.tv_sec = CHTTPX_CALL_TIMEOUT_SEC, .tv_usec = 0}; -#ifdef CHTTPX_PLATFORM_WINDOWS - int ready = select(0, NULL, &write_set, &error_set, &timeout); -#else int ready = select(socket_fd + 1, NULL, &write_set, &error_set, &timeout); -#endif if (ready == 0) return cHTTPX_ERR_TIMEOUT; @@ -811,18 +764,10 @@ static int wait_for_connect(chttpx_socket_t socket_fd) return socket_error_is_timeout(socket_last_error()) ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_UNAVAILABLE; int socket_error = 0; -#ifdef CHTTPX_PLATFORM_WINDOWS - int error_size = sizeof(socket_error); -#else socklen_t error_size = sizeof(socket_error); -#endif if (getsockopt(socket_fd, SOL_SOCKET, SO_ERROR, -#ifdef CHTTPX_PLATFORM_WINDOWS - (char*)&socket_error, -#else &socket_error, -#endif &error_size) != 0) return cHTTPX_ERR_UNAVAILABLE; @@ -844,11 +789,7 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co if (!remote || !connected) return cHTTPX_ERR_INVALID_ARGUMENT; -#ifdef CHTTPX_PLATFORM_WINDOWS - *connected = INVALID_SOCKET; -#else *connected = -1; -#endif struct addrinfo hints; struct addrinfo* result = NULL; @@ -865,11 +806,7 @@ static int connect_remote(const chttpx_remote_url_t* remote, chttpx_socket_t* co for (struct addrinfo* current = result; current; current = current->ai_next) { chttpx_socket_t socket_fd = socket(current->ai_family, current->ai_socktype, current->ai_protocol); -#ifdef CHTTPX_PLATFORM_WINDOWS - if (socket_fd == INVALID_SOCKET) -#else if (socket_fd < 0) -#endif continue; if (socket_set_nonblocking(socket_fd, true) != cHTTPX_OK) diff --git a/src/cHTTPX_body.c b/src/cHTTPX_body.c index b9d866d..db4859c 100644 --- a/src/cHTTPX_body.c +++ b/src/cHTTPX_body.c @@ -226,10 +226,6 @@ static int chunked_reader_read(chunked_reader_t* reader, unsigned char* output, } size_t wanted = output_size; -#ifdef CHTTPX_PLATFORM_WINDOWS - if (wanted > INT_MAX) - wanted = INT_MAX; -#endif int result = _chttpx_io_recv(reader->client_fd, reader->tls_session, output, wanted); if (result == cHTTPX_ERR_TLS) _chttpx_tls_log_error(reader->server, reader->request_id, "TLS chunked-body read failed"); @@ -620,19 +616,13 @@ void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buf while (remaining > 0) { size_t wanted = remaining; -#ifdef CHTTPX_PLATFORM_WINDOWS - if (wanted > INT_MAX) - wanted = INT_MAX; -#endif int n = _chttpx_io_recv(client_fd, req->_tls_session, (char*)req->body + total_read, wanted); if (n == cHTTPX_ERR_TLS) _chttpx_tls_log_error(req->_server, req->request_id, "TLS request-body read failed"); if (n < 0) { -#ifdef CHTTPX_PLATFORM_POSIX if (errno == EINTR) continue; -#endif break; } if (n == 0) diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c index 072eb57..5f7a4a0 100644 --- a/src/cHTTPX_http2.c +++ b/src/cHTTPX_http2.c @@ -5,6 +5,7 @@ #include "cHTTPX_http.h" #include "cHTTPX_response.h" #include "cHTTPX_tls.h" +#include "cHTTPX_websocket.h" #include @@ -13,10 +14,8 @@ #include #include #include -#ifdef CHTTPX_PLATFORM_POSIX #include #include -#endif #define CHTTPX_H2_PROTOCOL "HTTP/2" #define CHTTPX_H2_CALL_TIMEOUT_SEC 30 @@ -25,10 +24,14 @@ /** * Per-stream HTTP/2 request/response state on the server. */ +struct chttpx_h2_server; +typedef struct chttpx_h2_ws_send chttpx_h2_ws_send_t; + typedef struct chttpx_h2_stream { int32_t id; char method[16]; + char protocol[32]; char path[CHTTPX_MAX_PATH]; char authority[CHTTPX_H2_MAX_AUTHORITY]; chttpx_header_t headers[MAX_HEADERS]; @@ -43,19 +46,35 @@ typedef struct chttpx_h2_stream size_t response_size; size_t response_body_offset; size_t response_body_sent; + struct chttpx_h2_server* connection; + chttpx_wsocket_t* websocket; + chttpx_request_t* websocket_request; } chttpx_h2_stream_t; /** * Server-side HTTP/2 session bound to one connection. */ -typedef struct +typedef struct chttpx_h2_server { chttpx_serv_t* server; chttpx_socket_t fd; void* tls_session; nghttp2_session* session; + chttpx_h2_ws_send_t* ws_outgoing; } chttpx_h2_server_t; +struct chttpx_h2_ws_send +{ + chttpx_h2_server_t* connection; + int32_t stream_id; + unsigned char* data; + size_t size; + size_t offset; + bool end_stream; + bool completed; + chttpx_h2_ws_send_t* next; +}; + /** * Client-side HTTP/2 session state for outbound calls. */ @@ -152,6 +171,15 @@ static void h2_stream_free(chttpx_h2_stream_t* stream) return; free(stream->body); free(stream->response); + if (stream->websocket) + _chttpx_websocket_destroy(stream->websocket); + if (stream->websocket_request) + { + cHTTPX_RequestCleanup(stream->websocket_request); + free(stream->websocket_request->method); + free(stream->websocket_request->path); + free(stream->websocket_request); + } free(stream); } @@ -206,6 +234,18 @@ static int h2_stream_add_header(chttpx_h2_stream_t* stream, const uint8_t* name, return 0; } + if (namelen == 9 && memcmp(name, ":protocol", 9) == 0) + { + if (valuelen >= sizeof(stream->protocol)) + { + stream->error_status = cHTTPX_StatusBadRequest; + return 0; + } + memcpy(stream->protocol, value, valuelen); + stream->protocol[valuelen] = '\0'; + return 0; + } + if (namelen > 0 && name[0] == ':') return 0; @@ -481,6 +521,194 @@ static int h2_submit_text_response(chttpx_h2_server_t* connection, chttpx_h2_str return rv; } +static ssize_t h2_websocket_data_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, + uint32_t* data_flags, nghttp2_data_source* source, void* user_data) +{ + (void)session; + (void)stream_id; + (void)user_data; + chttpx_h2_ws_send_t* send = source ? source->ptr : NULL; + if (!send || !data_flags) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + size_t remaining = send->size - send->offset; + size_t take = remaining < length ? remaining : length; + if (take) + memcpy(buf, send->data + send->offset, take); + send->offset += take; + + if (send->offset >= send->size) + { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + if (!send->end_stream) + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + send->completed = true; + } + return (ssize_t)take; +} + +static void h2_websocket_cleanup_outgoing(chttpx_h2_server_t* connection, bool all) +{ + if (!connection) + return; + + chttpx_h2_ws_send_t** current = &connection->ws_outgoing; + while (*current) + { + chttpx_h2_ws_send_t* send = *current; + if (all || send->completed) + { + *current = send->next; + free(send->data); + free(send); + continue; + } + current = &send->next; + } +} + +static int h2_websocket_transport_send(void* context, const unsigned char* data, size_t len, bool end_stream) +{ + chttpx_h2_stream_t* stream = context; + chttpx_h2_server_t* connection = stream ? stream->connection : NULL; + if (!stream || !connection || !connection->session || (!data && len)) + return cHTTPX_ERR_STATE; + + chttpx_h2_ws_send_t* send = calloc(1, sizeof(*send)); + if (!send) + return cHTTPX_ERR_MEMORY; + + if (len) + { + send->data = malloc(len); + if (!send->data) + { + free(send); + return cHTTPX_ERR_MEMORY; + } + memcpy(send->data, data, len); + } + + send->connection = connection; + send->stream_id = stream->id; + send->size = len; + send->end_stream = end_stream; + send->next = connection->ws_outgoing; + connection->ws_outgoing = send; + + nghttp2_data_provider provider = { + .source = {.ptr = send}, + .read_callback = h2_websocket_data_read, + }; + + int rv = nghttp2_submit_data(connection->session, NGHTTP2_FLAG_NONE, stream->id, &provider); + if (rv != 0) + { + connection->ws_outgoing = send->next; + free(send->data); + free(send); + return cHTTPX_ERR_IO; + } + return cHTTPX_OK; +} + +static chttpx_request_t* h2_websocket_request_create(chttpx_h2_server_t* connection, const chttpx_h2_stream_t* stream) +{ + if (!connection || !stream) + return NULL; + + chttpx_request_t* request = calloc(1, sizeof(*request)); + if (!request) + return NULL; + + request->method = strdup("CONNECT"); + request->path = strdup(stream->path); + if (!request->method || !request->path) + { + free(request->method); + free(request->path); + free(request); + return NULL; + } + + request->client_fd = connection->fd; + request->_server = connection->server; + request->_tls_session = connection->tls_session; + snprintf(request->protocol, sizeof(request->protocol), "%s", CHTTPX_H2_PROTOCOL); + + request->headers_count = stream->headers_count; + if (stream->headers_count) + memcpy(request->headers, stream->headers, stream->headers_count * sizeof(stream->headers[0])); + + if (stream->authority[0] && request->headers_count < MAX_HEADERS) + { + bool has_host = false; + for (size_t i = 0; i < request->headers_count; i++) + if (strcasecmp(request->headers[i].name, "host") == 0) + has_host = true; + if (!has_host) + { + snprintf(request->headers[request->headers_count].name, MAX_HEADER_NAME, "Host"); + snprintf(request->headers[request->headers_count].value, MAX_HEADER_VALUE, "%s", stream->authority); + request->headers_count++; + } + } + + return request; +} + +static int h2_open_websocket(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream) +{ + if (!connection || !stream || stream->websocket || stream->responded) + return 0; + + chttpx_wsocket_route_t handler = _chttpx_websocket_find_route(connection->server, stream->path); + if (!handler) + return h2_submit_text_response(connection, stream, cHTTPX_StatusNotFound, cHTTPX_CTYPE_TEXT); + + const char* version = h2_request_header(stream, "sec-websocket-version"); + if (!version || strcmp(version, "13") != 0) + return h2_submit_text_response(connection, stream, cHTTPX_StatusBadRequest, cHTTPX_CTYPE_TEXT); + + chttpx_request_t* request = h2_websocket_request_create(connection, stream); + if (!request) + return h2_submit_text_response(connection, stream, cHTTPX_StatusInternalServerError, cHTTPX_CTYPE_TEXT); + + chttpx_wsocket_t* websocket = _chttpx_websocket_create(connection->server, connection->fd, request, + h2_websocket_transport_send, stream); + if (!websocket) + { + cHTTPX_RequestCleanup(request); + free(request->method); + free(request->path); + free(request); + return h2_submit_text_response(connection, stream, cHTTPX_StatusInternalServerError, cHTTPX_CTYPE_TEXT); + } + + nghttp2_nv headers[] = { + h2_nv(":status", "200"), + }; + int rv = nghttp2_submit_headers(connection->session, NGHTTP2_FLAG_NONE, stream->id, NULL, + headers, CHTTPX_ARRAY_LEN(headers), NULL); + if (rv != 0) + { + _chttpx_websocket_destroy(websocket); + cHTTPX_RequestCleanup(request); + free(request->method); + free(request->path); + free(request); + return rv; + } + + stream->connection = connection; + stream->websocket = websocket; + stream->websocket_request = request; + stream->responded = true; + _chttpx_websocket_mark_connected(websocket); + handler(websocket); + return 0; +} + /** * Submit a full response parsed from HTTP/1 text. * @@ -609,7 +837,6 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t */ static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { - (void)user_data; if (!frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) return 0; @@ -617,6 +844,7 @@ static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame if (!stream) return NGHTTP2_ERR_CALLBACK_FAILURE; stream->id = frame->hd.stream_id; + stream->connection = user_data; int rv = nghttp2_session_set_stream_user_data(session, stream->id, stream); if (rv != 0) @@ -666,6 +894,17 @@ static int h2_server_data(nghttp2_session* session, uint8_t flags, int32_t strea (void)flags; chttpx_h2_server_t* connection = user_data; chttpx_h2_stream_t* stream = h2_stream(session, stream_id); + if (!stream) + return 0; + + if (stream->websocket) + { + int result = _chttpx_websocket_feed(stream->websocket, data, len); + if (result == cHTTPX_OK || result == cHTTPX_ERR_PROTOCOL || result == cHTTPX_ERR_STATE) + return 0; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return h2_append_body(connection, stream, data, len); } @@ -683,16 +922,35 @@ static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* f if (!connection || !frame) return NGHTTP2_ERR_CALLBACK_FAILURE; - if (frame->hd.stream_id <= 0 || !(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) - return 0; - - if (frame->hd.type != NGHTTP2_HEADERS && frame->hd.type != NGHTTP2_DATA) + if (frame->hd.stream_id <= 0) return 0; chttpx_h2_stream_t* stream = h2_stream(session, frame->hd.stream_id); if (!stream) return 0; + if (frame->hd.type == NGHTTP2_HEADERS && + frame->headers.cat == NGHTTP2_HCAT_REQUEST && + (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS) && + strcmp(stream->method, "CONNECT") == 0 && + strcmp(stream->protocol, "websocket") == 0) + { + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) + return h2_submit_text_response(connection, stream, cHTTPX_StatusBadRequest, cHTTPX_CTYPE_TEXT) == 0 + ? 0 + : NGHTTP2_ERR_CALLBACK_FAILURE; + + int rv = h2_open_websocket(connection, stream); + return rv == 0 ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; + } + + if (!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) + return 0; + if (frame->hd.type != NGHTTP2_HEADERS && frame->hd.type != NGHTTP2_DATA) + return 0; + if (stream->websocket) + return 0; + int rv = h2_process_request(connection, stream); return rv == 0 ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; } @@ -709,7 +967,12 @@ static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* f static int h2_server_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) { (void)error_code; - (void)user_data; + chttpx_h2_server_t* connection = user_data; + if (connection) + for (chttpx_h2_ws_send_t* send = connection->ws_outgoing; send; send = send->next) + if (send->stream_id == stream_id) + send->completed = true; + chttpx_h2_stream_t* stream = h2_stream(session, stream_id); if (stream) { @@ -756,6 +1019,7 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* nghttp2_settings_entry settings[] = { {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 128}, + {NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, 1}, }; if (nghttp2_submit_settings(connection.session, NGHTTP2_FLAG_NONE, settings, CHTTPX_ARRAY_LEN(settings)) != 0 || nghttp2_session_send(connection.session) != 0) @@ -800,10 +1064,13 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void* result = cHTTPX_ERR_IO; break; } + h2_websocket_cleanup_outgoing(&connection, false); } done: nghttp2_session_del(connection.session); + connection.session = NULL; + h2_websocket_cleanup_outgoing(&connection, true); return result; } @@ -878,17 +1145,10 @@ static int h2_parse_url(const char* base_url, chttpx_h2_url_t* parsed) */ static int h2_set_call_timeouts(chttpx_socket_t fd) { -#ifdef CHTTPX_PLATFORM_WINDOWS - DWORD timeout_ms = CHTTPX_H2_CALL_TIMEOUT_SEC * 1000U; - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0 || - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms)) != 0) - return cHTTPX_ERR_UNAVAILABLE; -#else struct timeval timeout = {.tv_sec = CHTTPX_H2_CALL_TIMEOUT_SEC, .tv_usec = 0}; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 || setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) return cHTTPX_ERR_UNAVAILABLE; -#endif return cHTTPX_OK; } @@ -917,11 +1177,7 @@ static int h2_connect(const chttpx_h2_url_t* remote, chttpx_socket_t* connected) for (struct addrinfo* current = result; current; current = current->ai_next) { chttpx_socket_t fd = socket(current->ai_family, current->ai_socktype, current->ai_protocol); -#ifdef CHTTPX_PLATFORM_WINDOWS - if (fd == INVALID_SOCKET) -#else if (fd < 0) -#endif continue; if (h2_set_call_timeouts(fd) != cHTTPX_OK) @@ -1166,11 +1422,7 @@ int _chttpx_http2_call(chttpx_request_t* source, const char* base_url, const cht if (!h2_parse_url(base_url, &remote)) return cHTTPX_ERR_PROTOCOL; -#ifdef CHTTPX_PLATFORM_WINDOWS - chttpx_socket_t fd = INVALID_SOCKET; -#else chttpx_socket_t fd = -1; -#endif int result = h2_connect(&remote, &fd); if (result != cHTTPX_OK) return result; diff --git a/src/cHTTPX_i18n.c b/src/cHTTPX_i18n.c index 0a8e368..18c5084 100644 --- a/src/cHTTPX_i18n.c +++ b/src/cHTTPX_i18n.c @@ -28,11 +28,7 @@ #include #include #include -#if defined(_WIN32) || defined(_WIN64) -#include "../lib/cjson/cJSON.h" -#else #include -#endif static i18n_manager_t* i18n_manager = NULL; diff --git a/src/cHTTPX_json.c b/src/cHTTPX_json.c index 1a48676..00383ae 100644 --- a/src/cHTTPX_json.c +++ b/src/cHTTPX_json.c @@ -24,11 +24,7 @@ #include "cHTTPX_http.h" -#if defined(_WIN32) || defined(_WIN64) -#include "../lib/cjson/cJSON.h" -#else #include -#endif #include #include diff --git a/src/cHTTPX_media.c b/src/cHTTPX_media.c index bba8861..7867df3 100644 --- a/src/cHTTPX_media.c +++ b/src/cHTTPX_media.c @@ -32,9 +32,7 @@ #include #include -#ifdef CHTTPX_PLATFORM_POSIX #include -#endif #define MULTIPART_LINE_LIMIT 4096 #define MULTIPART_FORM_VALUE_LIMIT (1024 * 1024) @@ -106,13 +104,6 @@ static void remove_temporary_file(void* resource) */ static FILE* create_temporary_file(char* path, size_t path_size) { -#ifdef CHTTPX_PLATFORM_WINDOWS - (void)path_size; - char directory[MAX_PATH]; - if (!GetTempPathA(sizeof(directory), directory) || !GetTempFileNameA(directory, "chx", 0, path)) - return NULL; - return fopen(path, "w+b"); -#else if (path_size < 24) return NULL; snprintf(path, path_size, "/tmp/chttpx-upload-XXXXXX"); @@ -126,7 +117,6 @@ static FILE* create_temporary_file(char* path, size_t path_size) remove(path); } return file; -#endif } /** diff --git a/src/cHTTPX_metrics.c b/src/cHTTPX_metrics.c index 6f47974..9c299fd 100644 --- a/src/cHTTPX_metrics.c +++ b/src/cHTTPX_metrics.c @@ -18,9 +18,7 @@ #include #include -#ifdef CHTTPX_PLATFORM_POSIX #include -#endif /** Per-route request and status-class counters. */ typedef struct @@ -34,11 +32,7 @@ typedef struct /** Internal metrics state (atomics, route table, mutex). */ typedef struct { -#ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION routes_mutex; -#else pthread_mutex_t routes_mutex; -#endif chttpx_metrics_t metrics; uint64_t request_duration_nanoseconds_total; chttpx_route_metrics_entry_t* routes; @@ -67,11 +61,7 @@ static const double duration_bounds_seconds[] = { */ static void routes_lock(chttpx_metrics_state_t* state) { -#ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&state->routes_mutex); -#else pthread_mutex_lock(&state->routes_mutex); -#endif } /** @@ -81,11 +71,7 @@ static void routes_lock(chttpx_metrics_state_t* state) */ static void routes_unlock(chttpx_metrics_state_t* state) { -#ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&state->routes_mutex); -#else pthread_mutex_unlock(&state->routes_mutex); -#endif } /** @@ -354,15 +340,11 @@ int _chttpx_metrics_server_init(chttpx_serv_t* server, int enabled) if (!state) return cHTTPX_ERR_MEMORY; -#ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&state->routes_mutex); -#else if (pthread_mutex_init(&state->routes_mutex, NULL) != 0) { free(state); return cHTTPX_ERR_STATE; } -#endif server->metrics_state = state; return cHTTPX_OK; @@ -379,11 +361,7 @@ void _chttpx_metrics_server_cleanup(chttpx_serv_t* server) if (!state) return; -#ifdef CHTTPX_PLATFORM_WINDOWS - DeleteCriticalSection(&state->routes_mutex); -#else pthread_mutex_destroy(&state->routes_mutex); -#endif free(state->routes); free(state); server->metrics_state = NULL; diff --git a/src/cHTTPX_metrics.h b/src/cHTTPX_metrics.h index fe8cf70..8476429 100644 --- a/src/cHTTPX_metrics.h +++ b/src/cHTTPX_metrics.h @@ -70,7 +70,6 @@ extern "C" uint64_t queue_wait_nanoseconds_total; } chttpx_runtime_metrics_t; - /** * Copy the current server metrics into a caller-owned snapshot. * diff --git a/src/cHTTPX_middlewares.c b/src/cHTTPX_middlewares.c index 4582fcd..bb46b72 100644 --- a/src/cHTTPX_middlewares.c +++ b/src/cHTTPX_middlewares.c @@ -30,11 +30,7 @@ typedef struct char ips[MAX_MIDDLEWARE_RATE_LIMIT_TABLE_SIZE][64]; uint32_t max_requests; uint32_t window_sec; -#ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION mutex; -#else pthread_mutex_t mutex; -#endif } chttpx_rate_limiter_state_t; /** @@ -98,11 +94,7 @@ static uint32_t rate_limiter_hash(const char* ip) */ static void rate_limiter_lock(chttpx_rate_limiter_state_t* state) { -#ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&state->mutex); -#else pthread_mutex_lock(&state->mutex); -#endif } /** @@ -112,11 +104,7 @@ static void rate_limiter_lock(chttpx_rate_limiter_state_t* state) */ static void rate_limiter_unlock(chttpx_rate_limiter_state_t* state) { -#ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&state->mutex); -#else pthread_mutex_unlock(&state->mutex); -#endif } /** @@ -178,15 +166,11 @@ void cHTTPX_MiddlewareRateLimiter(chttpx_serv_t* server, uint32_t max_requests, if (!state) return; -#ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&state->mutex); -#else if (pthread_mutex_init(&state->mutex, NULL) != 0) { free(state); return; } -#endif server->rate_limiter_state = state; } @@ -267,11 +251,7 @@ void _chttpx_middleware_server_cleanup(chttpx_serv_t* server) return; chttpx_rate_limiter_state_t* state = (chttpx_rate_limiter_state_t*)server->rate_limiter_state; -#ifdef CHTTPX_PLATFORM_WINDOWS - DeleteCriticalSection(&state->mutex); -#else pthread_mutex_destroy(&state->mutex); -#endif free(state); server->rate_limiter_state = NULL; } diff --git a/src/cHTTPX_request.c b/src/cHTTPX_request.c index b214653..552f3ea 100644 --- a/src/cHTTPX_request.c +++ b/src/cHTTPX_request.c @@ -22,17 +22,12 @@ #include "cHTTPX_request.h" -#include "cHTTPX_i18n.h" #include "cHTTPX_crosspltm.h" #include "cHTTPX_headers.h" #include "cHTTPX_response.h" #include "cHTTPX_http.h" -#if defined(_WIN32) || defined(_WIN64) -#include "../lib/cjson/cJSON.h" -#else #include -#endif #include #include @@ -514,36 +509,43 @@ int cHTTPX_OnBodyChunk(chttpx_request_t* req, chttpx_body_chunk_fn callback, voi return 0; } -/** Localized validation error message templates. */ -typedef struct +/** + * Record a bind/validate failure on the request. + * + * @param req Current HTTP request. + * @param code Bind error code. + * @param field Failing field name, or NULL. + * @param num Optional numeric detail such as min/max length. + * @return The same code, for direct return from callers. + */ +static int set_bind_error(chttpx_request_t* req, int code, const char* field, size_t num) { - const char* required; - const char* min_length; - const char* max_length; - const char* invalid_email; - const char* generic; -} validation_messages_t; - -validation_messages_t messages_en = {"field '%s' is required", "field '%s' min length is %zu", "field '%s' max length is %zu", - "field '%s' is not a valid email", "field '%s' validation error"}; + if (!req) + return code; -validation_messages_t messages_ru = {"поле '%s' обязательно", "минимальная длина поля '%s' — %zu", "максимальная длина поля '%s' — %zu", - "поле '%s' имеет неверный формат email", "ошибка валидации поля '%s'"}; + req->error_code = code; + req->error_num = num; + req->error_field[0] = '\0'; + req->error_msg[0] = '\0'; -validation_messages_t* messages[LANG_COUNT] = {&messages_en, &messages_ru, &messages_en, &messages_en}; + if (field) + snprintf(req->error_field, sizeof(req->error_field), "%s", field); + + return code; +} /** - * Parse a JSON body and validate fields according to the provided definitions. - * @param req Pointer to the HTTP request. - * @param fields Array of field validation definitions (cHTTPX_FieldValidation). - * @param field_count Number of fields in the array. - * @return 1 if parsing and validation succeed, 0 if there is an error. - * This function automatically checks required fields, string length, boolean types, etc. + * Parse a JSON body into validation targets. + * + * @param req Current HTTP request. + * @param fields Field definitions and output targets. + * @param field_count Number of entries in fields. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count) { if (!req || !fields || !req->body) - return 0; + return set_bind_error(req, cHTTPX_BIND_INVALID_ARGUMENT, NULL, 0); /* * cJSON can parse a bounded byte range directly. Avoid duplicating the @@ -552,14 +554,14 @@ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t fiel cJSON* json = cJSON_ParseWithLengthOpts((const char*)req->body, req->body_size, NULL, false); if (!json) - { - snprintf(req->error_msg, sizeof(req->error_msg), "Invalid JSON"); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_INVALID_JSON, NULL, 0); + + const char* failed_field = NULL; for (size_t i = 0; i < field_count; i++) { chttpx_validation_t* f = &fields[i]; + failed_field = f->name; cJSON* item = cJSON_GetObjectItem(json, f->name); if (!item) @@ -664,17 +666,15 @@ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t fiel } cJSON_Delete(json); - return 1; + return cHTTPX_BIND_OK; memory_error: cJSON_Delete(json); - snprintf(req->error_msg, sizeof(req->error_msg), "Out of memory"); - return 0; + return set_bind_error(req, cHTTPX_BIND_MEMORY, failed_field, 0); type_error: - snprintf(req->error_msg, sizeof(req->error_msg), "Invalid JSON field type"); cJSON_Delete(json); - return 0; + return set_bind_error(req, cHTTPX_BIND_TYPE, failed_field, 0); } /* Validator email string */ @@ -707,57 +707,26 @@ static int is_valid_email(const char* email) } /** - * Set error. + * Validate already parsed field values. * - * @param error_msg Parameter `error_msg`. - * @param error_size Parameter `error_size`. - * @param lang Parameter `lang`. - * @param key Parameter `key`. - * @param field_name Parameter `field_name`. - * @param num Parameter `num`. - */ -static void set_error(char* error_msg, size_t error_size, i18n_language_t lang, int key, const char* field_name, size_t num) -{ - validation_messages_t* msg = messages[lang]; - - switch (key) - { - case 0: - snprintf(error_msg, error_size, msg->required, field_name); - break; - case 1: - snprintf(error_msg, error_size, msg->min_length, field_name, num); - break; - case 2: - snprintf(error_msg, error_size, msg->max_length, field_name, num); - break; - case 3: - snprintf(error_msg, error_size, msg->invalid_email, field_name); - break; - default: - snprintf(error_msg, error_size, msg->generic, field_name); - break; - } -} - -/* - * Validates an array of cHTTPX_FieldValidation structures. - * This function ensures that required fields are present, string lengths are within limits, - * and basic validation for integers and boolean fields is performed. + * @param req Current HTTP request. + * @param fields Field definitions and parsed targets. + * @param field_count Number of entries in fields. + * @param l Unused; kept for source compatibility. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count, const char* l) { - i18n_language_t lang = i18n_lang_from_string(l ? l : "en"); + (void)l; + if (!req || !fields) + return set_bind_error(req, cHTTPX_BIND_INVALID_ARGUMENT, NULL, 0); for (size_t i = 0; i < field_count; i++) { chttpx_validation_t* f = &fields[i]; if (f->required && !f->present) - { - set_error(req->error_msg, sizeof(req->error_msg), lang, 0, f->name, 0); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_REQUIRED, f->name, 0); if (!f->present) continue; @@ -766,10 +735,7 @@ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t f { char* v = *(char**)f->target; if (!v) - { - set_error(req->error_msg, sizeof(req->error_msg), lang, 4, f->name, 0); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_GENERIC, f->name, 0); if (f->normalizers & cHTTPX_TRIM) { @@ -797,54 +763,39 @@ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t f size_t len = strlen(v); if (f->min_length && len < f->min_length) - { - set_error(req->error_msg, sizeof(req->error_msg), lang, 1, f->name, f->min_length); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_MIN_LENGTH, f->name, f->min_length); if (f->max_length && len > f->max_length) - { - set_error(req->error_msg, sizeof(req->error_msg), lang, 2, f->name, f->max_length); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_MAX_LENGTH, f->name, f->max_length); if (f->validator == VALIDATOR_EMAIL && !is_valid_email(v)) - { - set_error(req->error_msg, sizeof(req->error_msg), lang, 3, f->name, 0); - return 0; - } + return set_bind_error(req, cHTTPX_BIND_INVALID_EMAIL, f->name, 0); if (f->custom_validator && !f->custom_validator(v, req->error_msg, sizeof(req->error_msg))) { - if (!req->error_msg[0]) - set_error(req->error_msg, sizeof(req->error_msg), lang, 4, f->name, 0); - return 0; + req->error_code = cHTTPX_BIND_GENERIC; + req->error_num = 0; + snprintf(req->error_field, sizeof(req->error_field), "%s", f->name ? f->name : ""); + return cHTTPX_BIND_GENERIC; } } } - return 1; + return cHTTPX_BIND_OK; } /** - * Parse and validate a JSON request body, writing a 400 response on failure. + * Parse and validate a JSON request body. * * @param req Current HTTP request. - * @param res Response populated when binding fails. * @param fields Field definitions and output targets. * @param field_count Number of field definitions. - * @return 1 on success, 0 on parsing or validation failure. + * @return cHTTPX_BIND_OK or a positive bind error code. */ -int cHTTPX_BindJSON(chttpx_request_t* req, chttpx_response_t* res, chttpx_validation_t* fields, size_t field_count) +int cHTTPX_BindJSON(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count) { - if (!req || !res || !fields) - return 0; - - if (!cHTTPX_Parse(req, fields, field_count) || !cHTTPX_Validate(req, fields, field_count, req->language[0] ? req->language : "en")) - { - *res = cHTTPX_ResError(cHTTPX_StatusBadRequest, req->error_msg[0] ? req->error_msg : "invalid request body"); - return 0; - } - - return 1; + int result = cHTTPX_Parse(req, fields, field_count); + if (result != cHTTPX_BIND_OK) + return result; + return cHTTPX_Validate(req, fields, field_count, NULL); } diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h index 4f497b8..66dfa4f 100644 --- a/src/cHTTPX_request.h +++ b/src/cHTTPX_request.h @@ -85,6 +85,26 @@ extern "C" FIELD_NUMBER_ARRAY } validation_t; + /** + * Result of cHTTPX_Parse, cHTTPX_Validate, and cHTTPX_BindJSON. + * + * Zero means success. Positive values name the failure. The failing + * field, if any, is stored in req->error_field. + */ + typedef enum + { + cHTTPX_BIND_OK = 0, + cHTTPX_BIND_REQUIRED = 1, + cHTTPX_BIND_MIN_LENGTH = 2, + cHTTPX_BIND_MAX_LENGTH = 3, + cHTTPX_BIND_INVALID_EMAIL = 4, + cHTTPX_BIND_GENERIC = 5, + cHTTPX_BIND_INVALID_JSON = 6, + cHTTPX_BIND_TYPE = 7, + cHTTPX_BIND_MEMORY = 8, + cHTTPX_BIND_INVALID_ARGUMENT = 9 + } chttpx_bind_error_t; + /** Parsed JSON array of strings (request-owned items). */ typedef struct { @@ -207,6 +227,11 @@ extern "C" /* Error REQuest message */ char error_msg[BUFFER_SIZE]; + /* Bind/validate failure: code, field name, and optional numeric detail. */ + int error_code; + char error_field[MAX_PARAM_NAME]; + size_t error_num; + /* Request metadata */ char request_id[65]; char language[16]; @@ -368,7 +393,7 @@ extern "C" * @param req Current HTTP request. * @param fields Field definitions and output targets. * @param field_count Number of entries in fields. - * @return 1 on success or 0 on parse/type/allocation failure. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Parse(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); @@ -378,25 +403,23 @@ extern "C" * @param req Current HTTP request. * @param fields Field definitions and parsed targets. * @param field_count Number of entries in fields. - * @param l Language code used for validation messages. - * @return 1 when all values pass validation, otherwise 0. + * @param l Unused; kept for source compatibility. + * @return cHTTPX_BIND_OK or a positive bind error code. */ int cHTTPX_Validate(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count, const char* l); - struct chttpx_response; /** * Parse and validate a JSON request body. * - * Parsed strings and arrays are request-owned. On failure this function - * creates a safe JSON 400 response in res. + * Does not write an HTTP response. On failure inspect req->error_code, + * req->error_field, and req->error_num, then build the response yourself. * * @param req Current HTTP request. - * @param res Response populated when binding fails. * @param fields Field definitions and output targets. * @param field_count Number of field definitions. - * @return 1 on success, 0 on parsing or validation failure. + * @return cHTTPX_BIND_OK or a positive bind error code. */ - int cHTTPX_BindJSON(chttpx_request_t* req, struct chttpx_response* res, chttpx_validation_t* fields, size_t field_count); + int cHTTPX_BindJSON(chttpx_request_t* req, chttpx_validation_t* fields, size_t field_count); /** * Macro to define a string field for JSON request validation. diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c index 9e421c5..658a7fb 100644 --- a/src/cHTTPX_response.c +++ b/src/cHTTPX_response.c @@ -300,12 +300,7 @@ static chttpx_route_t* find_route(chttpx_request_t* req) */ static int socket_read_timed_out(void) { -#ifdef CHTTPX_PLATFORM_WINDOWS - int error = WSAGetLastError(); - return error == WSAETIMEDOUT || error == WSAEWOULDBLOCK; -#else return errno == EAGAIN || errno == EWOULDBLOCK || errno == ETIMEDOUT; -#endif } /** @@ -333,10 +328,8 @@ static ssize_t read_req(chttpx_serv_t* server, chttpx_socket_t fd, void* tls_ses return cHTTPX_ERR_TLS; if (n < 0) { -#ifdef CHTTPX_PLATFORM_POSIX if (errno == EINTR) continue; -#endif if (socket_read_timed_out()) return cHTTPX_ERR_TIMEOUT; return -1; @@ -376,12 +369,6 @@ static void set_client_timeout(chttpx_serv_t* server, chttpx_socket_t client_fd) if (!server) return; -#ifdef CHTTPX_PLATFORM_WINDOWS - DWORD read_timeout_ms = (DWORD)server->read_timeout_sec * 1000U; - DWORD write_timeout_ms = (DWORD)server->write_timeout_sec * 1000U; - setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&read_timeout_ms, sizeof(read_timeout_ms)); - setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&write_timeout_ms, sizeof(write_timeout_ms)); -#else struct timeval tv; tv.tv_usec = 0; @@ -390,7 +377,6 @@ static void set_client_timeout(chttpx_serv_t* server, chttpx_socket_t client_fd) tv.tv_sec = server->write_timeout_sec; setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); -#endif } /** diff --git a/src/cHTTPX_runtime.c b/src/cHTTPX_runtime.c index 7b0d3ee..734af81 100644 --- a/src/cHTTPX_runtime.c +++ b/src/cHTTPX_runtime.c @@ -14,9 +14,7 @@ #include #include #include -#ifdef CHTTPX_PLATFORM_POSIX #include -#endif #define CHTTPX_RUNTIME_EVENTS 256 #define CHTTPX_CHUNK_LINE_MAX 128 @@ -102,16 +100,11 @@ typedef struct chttpx_runtime bool stopping; bool shutdown_started; bool listener_registered; -#ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION completion_mutex; -#else pthread_mutex_t completion_mutex; -#endif chttpx_connection_t* completions; chttpx_connection_t* connections; } chttpx_runtime_t; - /** * Return whether the runtime shutdown flag is set. * @@ -131,11 +124,7 @@ static bool runtime_is_stopping(chttpx_runtime_t* runtime) */ static bool socket_valid(chttpx_socket_t fd) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return fd != INVALID_SOCKET; -#else return fd >= 0; -#endif } /** @@ -149,17 +138,6 @@ static int connection_set_blocking(chttpx_connection_t* connection) if (!connection) return -1; -#ifdef CHTTPX_PLATFORM_WINDOWS - u_long mode = 0; - if (ioctlsocket(connection->fd, FIONBIO, &mode) != 0) - return -1; - - DWORD read_timeout_ms = (DWORD)connection->server->read_timeout_sec * 1000U; - DWORD write_timeout_ms = (DWORD)connection->server->write_timeout_sec * 1000U; - if (setsockopt(connection->fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&read_timeout_ms, sizeof(read_timeout_ms)) != 0 || - setsockopt(connection->fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&write_timeout_ms, sizeof(write_timeout_ms)) != 0) - return -1; -#else int flags = fcntl(connection->fd, F_GETFL, 0); if (flags < 0 || fcntl(connection->fd, F_SETFL, flags & ~O_NONBLOCK) != 0) return -1; @@ -171,7 +149,6 @@ static int connection_set_blocking(chttpx_connection_t* connection) timeout.tv_sec = connection->server->write_timeout_sec; if (setsockopt(connection->fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) return -1; -#endif return 0; } @@ -182,13 +159,9 @@ static int connection_set_blocking(chttpx_connection_t* connection) */ static uint64_t monotonic_ms(void) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return GetTickCount64(); -#else struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); return (uint64_t)now.tv_sec * 1000ULL + (uint64_t)now.tv_nsec / 1000000ULL; -#endif } /** @@ -198,11 +171,7 @@ static uint64_t monotonic_ms(void) */ static void completion_lock(chttpx_runtime_t* runtime) { -#ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&runtime->completion_mutex); -#else pthread_mutex_lock(&runtime->completion_mutex); -#endif } /** @@ -212,11 +181,7 @@ static void completion_lock(chttpx_runtime_t* runtime) */ static void completion_unlock(chttpx_runtime_t* runtime) { -#ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&runtime->completion_mutex); -#else pthread_mutex_unlock(&runtime->completion_mutex); -#endif } /** @@ -227,12 +192,7 @@ static void completion_unlock(chttpx_runtime_t* runtime) */ static int sync_init(chttpx_runtime_t* runtime) { -#ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&runtime->completion_mutex); - return 0; -#else return pthread_mutex_init(&runtime->completion_mutex, NULL) == 0 ? 0 : -1; -#endif } /** @@ -242,11 +202,7 @@ static int sync_init(chttpx_runtime_t* runtime) */ static void sync_destroy(chttpx_runtime_t* runtime) { -#ifdef CHTTPX_PLATFORM_WINDOWS - DeleteCriticalSection(&runtime->completion_mutex); -#else pthread_mutex_destroy(&runtime->completion_mutex); -#endif } /** @@ -1081,14 +1037,8 @@ static void accept_connections(chttpx_runtime_t* runtime) chttpx_socket_t fd = accept(server->server_fd, NULL, NULL); if (!socket_valid(fd)) { -#ifdef CHTTPX_PLATFORM_WINDOWS - int error = WSAGetLastError(); - if (error == WSAEWOULDBLOCK || error == WSAEINTR) - return; -#else if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return; -#endif return; } diff --git a/src/cHTTPX_serv.c b/src/cHTTPX_serv.c index 8873313..dfbea20 100644 --- a/src/cHTTPX_serv.c +++ b/src/cHTTPX_serv.c @@ -18,6 +18,7 @@ #include "cHTTPX_compression.h" #include "cHTTPX_metrics.h" #include "cHTTPX_runtime.h" +#include "cHTTPX_websocket.h" #include @@ -45,11 +46,7 @@ static void default_logger(chttpx_log_level_t level, const char* request_id, con */ static void server_sleep_ms(unsigned int milliseconds) { -#ifdef CHTTPX_PLATFORM_WINDOWS - Sleep(milliseconds); -#else usleep(milliseconds * 1000U); -#endif } /** @@ -60,11 +57,7 @@ static void server_sleep_ms(unsigned int milliseconds) */ static bool socket_valid(chttpx_socket_t fd) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return fd != INVALID_SOCKET; -#else return fd >= 0; -#endif } /** @@ -74,11 +67,7 @@ static bool socket_valid(chttpx_socket_t fd) */ static void invalidate_socket(chttpx_serv_t* server) { -#ifdef CHTTPX_PLATFORM_WINDOWS - server->server_fd = INVALID_SOCKET; -#else server->server_fd = -1; -#endif } /** @@ -250,11 +239,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha { int ipv6_only = config->network_mode == cHTTPX_NETWORK_IPV6 ? 1 : 0; if (setsockopt(server->server_fd, IPPROTO_IPV6, IPV6_V6ONLY, -#ifdef CHTTPX_PLATFORM_WINDOWS - (const char*)&ipv6_only, -#else &ipv6_only, -#endif sizeof(ipv6_only)) < 0) { chttpx_close(server->server_fd); @@ -265,11 +250,7 @@ int _chttpx_server_init(chttpx_serv_t* server, struct chttpx_app* app, const cha int opt = 1; setsockopt(server->server_fd, SOL_SOCKET, SO_REUSEADDR, -#ifdef CHTTPX_PLATFORM_WINDOWS - (const char*)&opt, -#else &opt, -#endif sizeof(opt)); struct sockaddr_storage addr = {0}; @@ -704,11 +685,7 @@ void _chttpx_server_shutdown(chttpx_serv_t* server) if (socket_valid(server->server_fd)) { -#ifdef CHTTPX_PLATFORM_WINDOWS - shutdown(server->server_fd, SD_BOTH); -#else shutdown(server->server_fd, SHUT_RDWR); -#endif chttpx_close(server->server_fd); invalidate_socket(server); } @@ -733,6 +710,7 @@ void _chttpx_server_shutdown(chttpx_serv_t* server) server->routes_count = 0; server->routes_capacity = 0; + _chttpx_websocket_server_cleanup(server); _chttpx_compression_server_cleanup(server); _chttpx_middleware_server_cleanup(server); diff --git a/src/cHTTPX_serv.h b/src/cHTTPX_serv.h index 8bef8d1..800e21b 100644 --- a/src/cHTTPX_serv.h +++ b/src/cHTTPX_serv.h @@ -67,7 +67,6 @@ extern "C" #define CHTTPX_ERR_COMPRESSION cHTTPX_ERR_COMPRESSION #endif - /** * Server logging verbosity. */ @@ -228,6 +227,7 @@ extern "C" void* compression_state; void* metrics_state; void* runtime_state; + void* websocket_state; chttpx_cors_t cors; } chttpx_serv_t; diff --git a/src/cHTTPX_tls.c b/src/cHTTPX_tls.c index b542219..269203d 100644 --- a/src/cHTTPX_tls.c +++ b/src/cHTTPX_tls.c @@ -508,10 +508,8 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t int error = SSL_get_error((SSL*)tls_session, result); if (error == SSL_ERROR_ZERO_RETURN) return 0; -#ifdef CHTTPX_PLATFORM_POSIX if (error == SSL_ERROR_SYSCALL && errno == EINTR) continue; -#endif return cHTTPX_ERR_TLS; } } @@ -524,10 +522,8 @@ int _chttpx_io_recv(chttpx_socket_t fd, void* tls_session, void* buffer, size_t int result = recv(fd, (char*)buffer, (int)wanted, 0); if (result >= 0) return result; -#ifdef CHTTPX_PLATFORM_POSIX if (errno == EINTR) continue; -#endif return -1; } } @@ -567,18 +563,10 @@ int _chttpx_io_recv_nonblocking(chttpx_socket_t fd, void* tls_session, void* buf int result = recv(fd, (char*)buffer, (int)wanted, 0); if (result >= 0) return result; -#ifdef CHTTPX_PLATFORM_WINDOWS - int error = WSAGetLastError(); - if (error == WSAEWOULDBLOCK) - return CHTTPX_IO_WANT_READ; - if (error == WSAEINTR) - return CHTTPX_IO_WANT_READ; -#else if (errno == EAGAIN || errno == EWOULDBLOCK) return CHTTPX_IO_WANT_READ; if (errno == EINTR) return CHTTPX_IO_WANT_READ; -#endif return cHTTPX_ERR_IO; } @@ -621,18 +609,10 @@ int _chttpx_io_send_nonblocking(chttpx_socket_t fd, void* tls_session, const voi int result = send(fd, (const char*)data, (int)wanted, flags); if (result >= 0) return result; -#ifdef CHTTPX_PLATFORM_WINDOWS - int error = WSAGetLastError(); - if (error == WSAEWOULDBLOCK) - return CHTTPX_IO_WANT_WRITE; - if (error == WSAEINTR) - return CHTTPX_IO_WANT_WRITE; -#else if (errno == EAGAIN || errno == EWOULDBLOCK) return CHTTPX_IO_WANT_WRITE; if (errno == EINTR) return CHTTPX_IO_WANT_WRITE; -#endif return cHTTPX_ERR_IO; } @@ -678,10 +658,8 @@ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, if (result <= 0) { int error = SSL_get_error((SSL*)tls_session, result); -#ifdef CHTTPX_PLATFORM_POSIX if (error == SSL_ERROR_SYSCALL && errno == EINTR) continue; -#endif return cHTTPX_ERR_TLS; } sent += (size_t)result; @@ -696,10 +674,8 @@ int _chttpx_io_send_all(chttpx_socket_t fd, void* tls_session, const void* data, int result = send(fd, (const char*)cursor + sent, (int)wanted, flags); if (result < 0) { -#ifdef CHTTPX_PLATFORM_POSIX if (errno == EINTR) continue; -#endif return cHTTPX_ERR_IO; } if (result == 0) diff --git a/src/cHTTPX_websocket.c b/src/cHTTPX_websocket.c index 7a58669..ff1cc44 100644 --- a/src/cHTTPX_websocket.c +++ b/src/cHTTPX_websocket.c @@ -5,35 +5,607 @@ * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: + * sell copies of the Software. + */ + +#include "cHTTPX_websocket.h" + +#include +#include +#include +#include + +/** + * One registered WebSocket route entry. + */ +typedef struct chttpx_wsocket_route_entry +{ + char* path; + chttpx_wsocket_route_t handler; + struct chttpx_wsocket_route_entry* next; +} chttpx_wsocket_route_entry_t; + +/** + * Per-server list of registered WebSocket routes. + */ +typedef struct +{ + chttpx_wsocket_route_entry_t* routes; +} chttpx_wsocket_server_state_t; + +/** + * Private WebSocket connection state owned by chttpx_wsocket_t::_internal. + */ +typedef struct +{ + chttpx_serv_t* server; + chttpx_wsocket_transport_send_fn transport_send; + void* transport_context; + chttpx_wsocket_handler_t message_handler; + chttpx_wsocket_close_handler_t close_handler; + unsigned char* input; + size_t input_size; + size_t input_capacity; + unsigned char* message; + size_t message_size; + size_t message_capacity; + int fragment_opcode; + bool close_sent; + bool close_received; +} chttpx_wsocket_internal_t; + +/** + * Return the private state pointer for a WebSocket handle. * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. + * @param wsocket Public WebSocket handle. + * @return Internal state, or NULL when wsocket is NULL. + */ +static chttpx_wsocket_internal_t* websocket_internal(chttpx_wsocket_t* wsocket) +{ + return wsocket ? (chttpx_wsocket_internal_t*)wsocket->_internal : NULL; +} + +/** + * Validate that a byte sequence is well-formed UTF-8. * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. + * @param data Bytes to inspect. + * @param len Number of bytes in data. + * @return True when the sequence is valid UTF-8. */ +static bool websocket_valid_utf8(const unsigned char* data, size_t len) +{ + size_t i = 0; + while (i < len) + { + unsigned char ch = data[i++]; + if (ch <= 0x7F) + continue; -#include "cHTTPX_websocket.h" + size_t continuation = 0; + uint32_t codepoint = 0; + if ((ch & 0xE0) == 0xC0) + { + continuation = 1; + codepoint = ch & 0x1F; + if (codepoint < 2) + return false; + } + else if ((ch & 0xF0) == 0xE0) + { + continuation = 2; + codepoint = ch & 0x0F; + } + else if ((ch & 0xF8) == 0xF0) + { + continuation = 3; + codepoint = ch & 0x07; + } + else + return false; + + if (continuation > len - i) + return false; + + for (size_t j = 0; j < continuation; j++) + { + unsigned char next = data[i++]; + if ((next & 0xC0) != 0x80) + return false; + codepoint = (codepoint << 6) | (uint32_t)(next & 0x3F); + } + + if ((continuation == 2 && codepoint < 0x800) || + (continuation == 3 && codepoint < 0x10000) || + codepoint > 0x10FFFF || + (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + return false; + } + return true; +} + +/** + * Grow a dynamic buffer until it can hold the required number of bytes. + * + * @param buffer In/out buffer pointer. + * @param capacity In/out allocated capacity. + * @param required Minimum number of bytes needed. + * @param limit Hard upper bound for the buffer size. + * @return Zero on success or a negative error code. + */ +static int websocket_ensure(unsigned char** buffer, size_t* capacity, size_t required, size_t limit) +{ + if (required > limit) + return cHTTPX_ERR_LIMIT; + if (required <= *capacity) + return cHTTPX_OK; + + size_t next = *capacity ? *capacity : 4096; + while (next < required) + { + if (next > limit / 2) + { + next = limit; + break; + } + next *= 2; + } + + unsigned char* resized = realloc(*buffer, next); + if (!resized) + return cHTTPX_ERR_MEMORY; + *buffer = resized; + *capacity = next; + return cHTTPX_OK; +} + +/** + * Encode and send one outbound WebSocket frame through the transport. + * + * @param wsocket Active WebSocket connection. + * @param opcode Frame opcode. + * @param data Optional payload bytes. + * @param len Payload length in bytes. + * @param end_stream Whether the transport should end the HTTP/2 stream. + * @return Zero on success or a negative error code. + */ +static int websocket_send_frame(chttpx_wsocket_t* wsocket, int opcode, const unsigned char* data, size_t len, bool end_stream) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal || !internal->transport_send || (!data && len)) + return cHTTPX_ERR_INVALID_ARGUMENT; + if (!wsocket->connected && opcode != CHTTPX_WSOCKET_OPCODE_CLOSE) + return cHTTPX_ERR_STATE; + if ((opcode & 0x08) && len > 125) + return cHTTPX_ERR_LIMIT; + + size_t header_size = len <= 125 ? 2 : (len <= 65535 ? 4 : 10); + if (len > SIZE_MAX - header_size) + return cHTTPX_ERR_LIMIT; + + size_t frame_size = header_size + len; + unsigned char* frame = malloc(frame_size ? frame_size : 1); + if (!frame) + return cHTTPX_ERR_MEMORY; + + frame[0] = (unsigned char)(0x80 | (opcode & 0x0F)); + size_t offset = 2; + if (len <= 125) + frame[1] = (unsigned char)len; + else if (len <= 65535) + { + frame[1] = 126; + frame[2] = (unsigned char)((len >> 8) & 0xFF); + frame[3] = (unsigned char)(len & 0xFF); + offset = 4; + } + else + { + frame[1] = 127; + uint64_t value = (uint64_t)len; + for (int i = 0; i < 8; i++) + frame[2 + i] = (unsigned char)(value >> (56 - i * 8)); + offset = 10; + } + + if (len) + memcpy(frame + offset, data, len); + + int result = internal->transport_send(internal->transport_context, frame, frame_size, end_stream); + free(frame); + return result; +} + +/** + * Return whether a WebSocket close status code is permitted by RFC 6455. + * + * @param code Candidate close status code. + * @return True when the code may be sent or accepted. + */ +static bool websocket_close_code_valid(uint16_t code) +{ + if (code < 1000 || code >= 5000) + return false; + if (code == 1004 || code == 1005 || code == 1006 || code == 1015) + return false; + if (code >= 1016 && code <= 2999) + return false; + return true; +} + +/** + * Send a close frame once and mark the local side as disconnected. + * + * @param wsocket Active WebSocket connection. + * @param data Optional close payload (status code and reason). + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ +static int websocket_send_close_payload(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal) + return cHTTPX_ERR_INVALID_ARGUMENT; + if (internal->close_sent) + return cHTTPX_OK; + + int result = websocket_send_frame(wsocket, CHTTPX_WSOCKET_OPCODE_CLOSE, data, len, true); + if (result == cHTTPX_OK) + { + internal->close_sent = true; + wsocket->connected = 0; + } + return result; +} + +/** + * Respond to a protocol violation with a close frame. + * + * @param wsocket Active WebSocket connection. + * @param code Protocol close status code to send. + * @return Always cHTTPX_ERR_PROTOCOL. + */ +static int websocket_protocol_close(chttpx_wsocket_t* wsocket, uint16_t code) +{ + unsigned char payload[2] = {(unsigned char)(code >> 8), (unsigned char)(code & 0xFF)}; + (void)websocket_send_close_payload(wsocket, payload, sizeof(payload)); + return cHTTPX_ERR_PROTOCOL; +} + +/** + * Append bytes to the current fragmented message buffer. + * + * @param wsocket Active WebSocket connection. + * @param data Payload fragment bytes. + * @param len Number of bytes to append. + * @return Zero on success or a negative error code. + */ +static int websocket_append_message(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal) + return cHTTPX_ERR_INVALID_ARGUMENT; + + size_t limit = internal->server && internal->server->max_body_size ? internal->server->max_body_size : MAX_BUFFER_BODY; + if (len > limit - internal->message_size) + return cHTTPX_ERR_LIMIT; + + size_t required = internal->message_size + len; + int result = websocket_ensure(&internal->message, &internal->message_capacity, required ? required : 1, limit ? limit : 1); + if (result != cHTTPX_OK) + return result; + if (len) + memcpy(internal->message + internal->message_size, data, len); + internal->message_size += len; + return cHTTPX_OK; +} + +/** + * Invoke the application message handler for a complete message. + * + * @param wsocket Active WebSocket connection. + * @param opcode Message opcode (text or binary). + * @param data Complete message payload. + * @param len Payload length in bytes. + */ +static void websocket_emit_message(chttpx_wsocket_t* wsocket, int opcode, const unsigned char* data, size_t len) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal || !internal->message_handler) + return; + wsocket->opcode = opcode; + internal->message_handler(wsocket, data, len); +} + +/** + * Handle one fully decoded inbound WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param fin FIN bit from the frame header. + * @param opcode Frame opcode. + * @param payload Unmasked payload bytes. + * @param payload_len Payload length in bytes. + * @return Zero on success or a negative protocol/error code. + */ +static int websocket_process_frame(chttpx_wsocket_t* wsocket, bool fin, int opcode, unsigned char* payload, size_t payload_len) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal) + return cHTTPX_ERR_INVALID_ARGUMENT; + + if (opcode == CHTTPX_WSOCKET_OPCODE_CLOSE) + { + if (!fin || payload_len == 1) + return websocket_protocol_close(wsocket, 1002); + + uint16_t code = 1005; + const unsigned char* reason = NULL; + size_t reason_len = 0; + if (payload_len >= 2) + { + code = (uint16_t)(((uint16_t)payload[0] << 8) | payload[1]); + reason = payload + 2; + reason_len = payload_len - 2; + if (!websocket_close_code_valid(code)) + return websocket_protocol_close(wsocket, 1002); + if (!websocket_valid_utf8(reason, reason_len)) + return websocket_protocol_close(wsocket, 1007); + } + + internal->close_received = true; + wsocket->connected = 0; + if (internal->close_handler) + internal->close_handler(wsocket, code, reason, reason_len); + if (!internal->close_sent) + (void)websocket_send_close_payload(wsocket, payload, payload_len); + return cHTTPX_OK; + } + + if (opcode == CHTTPX_WSOCKET_OPCODE_PING) + { + if (!fin || payload_len > 125) + return websocket_protocol_close(wsocket, 1002); + return websocket_send_frame(wsocket, CHTTPX_WSOCKET_OPCODE_PONG, payload, payload_len, false); + } + + if (opcode == CHTTPX_WSOCKET_OPCODE_PONG) + return fin && payload_len <= 125 ? cHTTPX_OK : websocket_protocol_close(wsocket, 1002); + + if (opcode == CHTTPX_WSOCKET_OPCODE_CONTINUATION) + { + if (!internal->fragment_opcode) + return websocket_protocol_close(wsocket, 1002); + int result = websocket_append_message(wsocket, payload, payload_len); + if (result != cHTTPX_OK) + return websocket_protocol_close(wsocket, result == cHTTPX_ERR_LIMIT ? 1009 : 1011); + + if (fin) + { + int message_opcode = internal->fragment_opcode; + if (message_opcode == CHTTPX_WSOCKET_OPCODE_TEXT && !websocket_valid_utf8(internal->message, internal->message_size)) + return websocket_protocol_close(wsocket, 1007); + internal->fragment_opcode = 0; + websocket_emit_message(wsocket, message_opcode, internal->message, internal->message_size); + internal->message_size = 0; + } + return cHTTPX_OK; + } + + if (opcode != CHTTPX_WSOCKET_OPCODE_TEXT && opcode != CHTTPX_WSOCKET_OPCODE_BINARY) + return websocket_protocol_close(wsocket, 1002); + if (internal->fragment_opcode) + return websocket_protocol_close(wsocket, 1002); + + if (fin) + { + if (opcode == CHTTPX_WSOCKET_OPCODE_TEXT && !websocket_valid_utf8(payload, payload_len)) + return websocket_protocol_close(wsocket, 1007); + websocket_emit_message(wsocket, opcode, payload, payload_len); + return cHTTPX_OK; + } + + internal->fragment_opcode = opcode; + internal->message_size = 0; + int result = websocket_append_message(wsocket, payload, payload_len); + if (result != cHTTPX_OK) + return websocket_protocol_close(wsocket, result == cHTTPX_ERR_LIMIT ? 1009 : 1011); + return cHTTPX_OK; +} + +/** + * Register a WebSocket route on a router path prefix. + * + * @param router Target router. + * @param path Path relative to the router prefix. + * @param handler Callback invoked after handshake success. + */ +void cHTTPX_WSocketRegisterRoute(chttpx_router_t* router, const char* path, chttpx_wsocket_route_t handler) +{ + if (!router || !router->serv || !router->serv->initialized || !path || !handler) + return; + + char full_path[CHTTPX_MAX_PATH]; + int written = snprintf(full_path, sizeof(full_path), "%s%s", router->prefix, path); + if (written < 0 || (size_t)written >= sizeof(full_path)) + return; + + chttpx_wsocket_server_state_t* state = router->serv->websocket_state; + if (!state) + { + state = calloc(1, sizeof(*state)); + if (!state) + return; + router->serv->websocket_state = state; + } + + for (chttpx_wsocket_route_entry_t* current = state->routes; current; current = current->next) + { + if (strcmp(current->path, full_path) == 0) + { + current->handler = handler; + return; + } + } + + chttpx_wsocket_route_entry_t* entry = calloc(1, sizeof(*entry)); + if (!entry) + return; + entry->path = strdup(full_path); + if (!entry->path) + { + free(entry); + return; + } + entry->handler = handler; + entry->next = state->routes; + state->routes = entry; +} + +/** + * Look up a registered WebSocket route handler for a request path. + * + * @param server Owning server instance. + * @param path Request path, optionally with a query string. + * @return Matching route handler, or NULL when none is registered. + */ +chttpx_wsocket_route_t _chttpx_websocket_find_route(chttpx_serv_t* server, const char* path) +{ + chttpx_wsocket_server_state_t* state = server ? server->websocket_state : NULL; + if (!state || !path) + return NULL; + + size_t path_len = strcspn(path, "?"); + for (chttpx_wsocket_route_entry_t* current = state->routes; current; current = current->next) + { + size_t registered_len = strlen(current->path); + if (registered_len == path_len && memcmp(current->path, path, path_len) == 0) + return current->handler; + } + return NULL; +} + +/** + * Set the message handler for an active WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param handler Callback for complete text or binary messages. + */ +void cHTTPX_WSocketOnMessage(chttpx_wsocket_t* wsocket, chttpx_wsocket_handler_t handler) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (internal) + internal->message_handler = handler; +} + +/** + * Set the close handler for an active WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param handler Callback for peer close or abrupt abort. + */ +void cHTTPX_WSocketOnClose(chttpx_wsocket_t* wsocket, chttpx_wsocket_close_handler_t handler) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (internal) + internal->close_handler = handler; +} + +/** + * Attach opaque application data to a WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param user_data Pointer stored on the connection. + */ +void cHTTPX_WSocketSetData(chttpx_wsocket_t* wsocket, void* user_data) +{ + if (wsocket) + wsocket->user_data = user_data; +} + +/** + * Return opaque application data previously attached to a WebSocket. + * + * @param wsocket Active WebSocket connection. + * @return Stored user data pointer, or NULL. + */ +void* cHTTPX_WSocketData(chttpx_wsocket_t* wsocket) +{ + return wsocket ? wsocket->user_data : NULL; +} + +/** + * Send a text WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data UTF-8 payload bytes. + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketSend(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + return websocket_send_frame(wsocket, CHTTPX_WSOCKET_OPCODE_TEXT, data, len, false); +} -/* Beta version */ +/** + * Send a binary WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data Binary payload bytes. + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketSendBinary(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + return websocket_send_frame(wsocket, CHTTPX_WSOCKET_OPCODE_BINARY, data, len, false); +} /** - * Perform the HTTP Upgrade handshake for WebSocket (legacy API). + * Send a WebSocket ping frame. * - * The legacy raw-socket upgrade API cannot identify an HTTP/2 stream. The - * symbol is kept for source compatibility and fails explicitly instead of - * emitting an invalid connection-level upgrade response. + * @param wsocket Active WebSocket connection. + * @param data Optional ping payload. + * @param len Payload length in bytes (at most 125). + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketPing(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + return websocket_send_frame(wsocket, CHTTPX_WSOCKET_OPCODE_PING, data, len, false); +} + +/** + * Send a WebSocket close frame and mark the connection closing. + * + * @param wsocket Active WebSocket connection. + * @param code Close status code, or 0 for 1000. + * @param reason Optional UTF-8 close reason. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketClose(chttpx_wsocket_t* wsocket, uint16_t code, const char* reason) +{ + if (!wsocket) + return cHTTPX_ERR_INVALID_ARGUMENT; + if (!code) + code = 1000; + if (!websocket_close_code_valid(code)) + return cHTTPX_ERR_INVALID_ARGUMENT; + + size_t reason_len = reason ? strlen(reason) : 0; + if (reason_len > 123 || (reason_len && !websocket_valid_utf8((const unsigned char*)reason, reason_len))) + return cHTTPX_ERR_INVALID_ARGUMENT; + + unsigned char payload[125]; + payload[0] = (unsigned char)(code >> 8); + payload[1] = (unsigned char)(code & 0xFF); + if (reason_len) + memcpy(payload + 2, reason, reason_len); + return websocket_send_close_payload(wsocket, payload, reason_len + 2); +} + +/** + * Legacy HTTP/1.1 upgrade helper retained for source compatibility. * - * @param client_socket Connected client socket. - * @param sec_wsocket_key Sec-WebSocket-Key header value. - * @return cHTTPX_ERR_UNAVAILABLE. + * @param client_socket Unused client socket. + * @param sec_wsocket_key Unused Sec-WebSocket-Key value. + * @return Always cHTTPX_ERR_UNAVAILABLE. */ int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key) { @@ -42,13 +614,218 @@ int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key) return cHTTPX_ERR_UNAVAILABLE; } -// static wsocket_read_frame(chttpx_socket_t client_fd, wsocket_frame_t* out) -// { -// unsigned char hdr[2]; +/** + * Legacy synchronous receive helper retained for source compatibility. + * + * @param wsocket Unused connection handle. + * @param buffer Unused output buffer. + * @param len Unused buffer capacity. + * @return Always cHTTPX_ERR_UNAVAILABLE. + */ +int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len) +{ + (void)wsocket; + (void)buffer; + (void)len; + return cHTTPX_ERR_UNAVAILABLE; +} + +/** + * Allocate a WebSocket handle bound to an HTTP/2 transport send callback. + * + * @param server Owning server instance. + * @param socket Client socket associated with the connection. + * @param request Handshake request metadata. + * @param transport_send Callback that writes encoded frames. + * @param transport_context Opaque context for transport_send. + * @return New WebSocket handle, or NULL on allocation failure. + */ +chttpx_wsocket_t* _chttpx_websocket_create(chttpx_serv_t* server, chttpx_socket_t socket, chttpx_request_t* request, + chttpx_wsocket_transport_send_fn transport_send, void* transport_context) +{ + if (!server || !request || !transport_send) + return NULL; + + chttpx_wsocket_t* wsocket = calloc(1, sizeof(*wsocket)); + chttpx_wsocket_internal_t* internal = calloc(1, sizeof(*internal)); + if (!wsocket || !internal) + { + free(wsocket); + free(internal); + return NULL; + } + + internal->server = server; + internal->transport_send = transport_send; + internal->transport_context = transport_context; + wsocket->socket = socket; + wsocket->request = request; + wsocket->_internal = internal; + return wsocket; +} + +/** + * Mark a WebSocket connection as ready for application traffic. + * + * @param wsocket WebSocket handle created by the HTTP/2 bridge. + */ +void _chttpx_websocket_mark_connected(chttpx_wsocket_t* wsocket) +{ + if (wsocket) + wsocket->connected = 1; +} + +/** + * Feed inbound WebSocket bytes into the frame parser. + * + * @param wsocket Active WebSocket connection. + * @param data Newly received bytes. + * @param len Number of bytes in data. + * @return Zero on success or a negative protocol/error code. + */ +int _chttpx_websocket_feed(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len) +{ + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (!internal || (!data && len)) + return cHTTPX_ERR_INVALID_ARGUMENT; + if (!wsocket->connected) + return cHTTPX_ERR_STATE; + + size_t limit = internal->server && internal->server->max_body_size ? internal->server->max_body_size : MAX_BUFFER_BODY; + if (limit > SIZE_MAX - 14) + limit = SIZE_MAX - 14; + size_t input_limit = limit + 14; + + if (len > input_limit - internal->input_size) + return websocket_protocol_close(wsocket, 1009); -// if (recv(client_fd, hdr, 2, MSG_WAITALL) <= 0) return -1; + size_t required = internal->input_size + len; + int result = websocket_ensure(&internal->input, &internal->input_capacity, required ? required : 1, input_limit ? input_limit : 1); + if (result != cHTTPX_OK) + return websocket_protocol_close(wsocket, result == cHTTPX_ERR_LIMIT ? 1009 : 1011); -// int opcode = hdr[0] & 0x0F; -// int masked = hdr[1] & 0x80; -// int len = hdr[1] & 0x7F; -// } + if (len) + memcpy(internal->input + internal->input_size, data, len); + internal->input_size += len; + + while (internal->input_size >= 2 && wsocket->connected) + { + unsigned char* frame = internal->input; + bool fin = (frame[0] & 0x80) != 0; + unsigned int rsv = frame[0] & 0x70; + int opcode = frame[0] & 0x0F; + bool masked = (frame[1] & 0x80) != 0; + uint64_t payload_len = frame[1] & 0x7F; + size_t header_size = 2; + + if (rsv) + return websocket_protocol_close(wsocket, 1002); + + if (payload_len == 126) + { + if (internal->input_size < 4) + break; + payload_len = ((uint64_t)frame[2] << 8) | frame[3]; + if (payload_len < 126) + return websocket_protocol_close(wsocket, 1002); + header_size = 4; + } + else if (payload_len == 127) + { + if (internal->input_size < 10) + break; + if (frame[2] & 0x80) + return websocket_protocol_close(wsocket, 1002); + payload_len = 0; + for (int i = 0; i < 8; i++) + payload_len = (payload_len << 8) | frame[2 + i]; + if (payload_len <= 65535) + return websocket_protocol_close(wsocket, 1002); + header_size = 10; + } + + if (!masked) + return websocket_protocol_close(wsocket, 1002); + if ((opcode & 0x08) && (!fin || payload_len > 125)) + return websocket_protocol_close(wsocket, 1002); + if (payload_len > limit || payload_len > SIZE_MAX) + return websocket_protocol_close(wsocket, 1009); + + if (header_size > SIZE_MAX - 4 || (size_t)payload_len > SIZE_MAX - header_size - 4) + return websocket_protocol_close(wsocket, 1009); + size_t frame_size = header_size + 4 + (size_t)payload_len; + if (internal->input_size < frame_size) + break; + + unsigned char mask[4]; + memcpy(mask, frame + header_size, sizeof(mask)); + unsigned char* payload = frame + header_size + 4; + for (size_t i = 0; i < (size_t)payload_len; i++) + payload[i] ^= mask[i & 3]; + + result = websocket_process_frame(wsocket, fin, opcode, payload, (size_t)payload_len); + if (result != cHTTPX_OK && result != cHTTPX_ERR_PROTOCOL) + return result; + + size_t remaining = internal->input_size - frame_size; + if (remaining) + memmove(internal->input, internal->input + frame_size, remaining); + internal->input_size = remaining; + + if (result == cHTTPX_ERR_PROTOCOL) + return result; + } + + return cHTTPX_OK; +} + +/** + * Destroy a WebSocket handle and release internal buffers. + * + * @param wsocket WebSocket handle to free. + */ +void _chttpx_websocket_destroy(chttpx_wsocket_t* wsocket) +{ + if (!wsocket) + return; + + chttpx_wsocket_internal_t* internal = websocket_internal(wsocket); + if (internal) + { + bool was_connected = wsocket->connected != 0; + wsocket->connected = 0; + if (was_connected && !internal->close_received && internal->close_handler) + internal->close_handler(wsocket, 1006, NULL, 0); + free(internal->input); + free(internal->message); + free(internal); + } + + wsocket->_internal = NULL; + wsocket->connected = 0; + free(wsocket); +} + +/** + * Free all WebSocket routes registered on a server. + * + * @param server Server whose websocket_state should be released. + */ +void _chttpx_websocket_server_cleanup(chttpx_serv_t* server) +{ + chttpx_wsocket_server_state_t* state = server ? server->websocket_state : NULL; + if (!state) + return; + + chttpx_wsocket_route_entry_t* current = state->routes; + while (current) + { + chttpx_wsocket_route_entry_t* next = current->next; + free(current->path); + free(current); + current = next; + } + + free(state); + server->websocket_state = NULL; +} diff --git a/src/cHTTPX_websocket.h b/src/cHTTPX_websocket.h index a101994..8bb6536 100644 --- a/src/cHTTPX_websocket.h +++ b/src/cHTTPX_websocket.h @@ -2,86 +2,261 @@ * Copyright (c) 2026 netcorelink * * This library is free software; you can redistribute it and/or modify it - * under the terms of the MIT license. See `libchttpx.c` for details. + * under the terms of the MIT license. */ +#ifndef CHTTPX_WEBSOCKET_H +#define CHTTPX_WEBSOCKET_H + #include "cHTTPX_serv.h" -#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif +/** WebSocket continuation frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_CONTINUATION 0x0 +/** WebSocket text frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_TEXT 0x1 +/** WebSocket binary frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_BINARY 0x2 +/** WebSocket close frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_CLOSE 0x8 +/** WebSocket ping frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_PING 0x9 +/** WebSocket pong frame opcode. */ #define CHTTPX_WSOCKET_OPCODE_PONG 0xA -/** Parsed WebSocket frame header and payload view. */ +/** + * Parsed WebSocket frame metadata and payload. + * + * Used by legacy or diagnostic paths that expose a decoded frame. + */ typedef struct { - /* FIN - final fragment - * 1 eq. this is the last frame of the message - * 0 eq. the message is divided into several parts - */ int fin; - /* Check define CHTTPX_WSOCKET_OPCODE */ int opcode; int masked; - /* Length data in payload */ uint64_t payload_len; - /* Mask for XOR payload */ unsigned char mask[4]; - /* Data in socket */ unsigned char* payload; } wsocket_frame_t; -/** Connected WebSocket client handle. */ -typedef struct +/** + * Active WebSocket connection handle. + * + * Created by the HTTP/2 Extended CONNECT bridge. Application code registers + * message and close callbacks, then sends frames through the public helpers. + */ +typedef struct chttpx_wsocket { - int socket; + chttpx_socket_t socket; int connected; + int opcode; + chttpx_request_t* request; + void* user_data; + void* _internal; } chttpx_wsocket_t; -/** Callback invoked when a WebSocket message payload is received. */ +/** + * Callback invoked for a complete text or binary message. + * + * @param wsocket Active WebSocket connection. + * @param data Message payload bytes. + * @param len Payload length in bytes. + */ typedef void (*chttpx_wsocket_handler_t)(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); -/** Route handler invoked after a successful WebSocket upgrade. */ +/** + * Callback invoked when the peer closes or the connection is aborted. + * + * @param wsocket Active WebSocket connection. + * @param code Close status code. + * @param reason Optional UTF-8 close reason. + * @param len Reason length in bytes. + */ +typedef void (*chttpx_wsocket_close_handler_t)(chttpx_wsocket_t* wsocket, uint16_t code, const unsigned char* reason, size_t len); + +/** + * Route entry callback invoked after a successful WebSocket handshake. + * + * @param wsocket Newly accepted WebSocket connection. + */ typedef void (*chttpx_wsocket_route_t)(chttpx_wsocket_t* wsocket); /** - * Register a WebSocket route on a router. + * Register a WebSocket route on a router path prefix. * - * @param r Router that owns the route. - * @param path URL path template. - * @param handler Handler invoked after upgrade. + * @param router Target router. + * @param path Path relative to the router prefix. + * @param handler Callback invoked after handshake success. */ -void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, chttpx_wsocket_route_t handler); +void cHTTPX_WSocketRegisterRoute(chttpx_router_t* router, const char* path, chttpx_wsocket_route_t handler); /** - * Perform the HTTP Upgrade handshake for WebSocket (legacy API). + * Set the message handler for an active WebSocket. * - * @param client_socket Connected client socket. - * @param sec_wsocket_key Sec-WebSocket-Key header value. - * @return cHTTPX_OK on success or a negative error code. + * @param wsocket Active WebSocket connection. + * @param handler Callback for complete text or binary messages. */ -int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); +void cHTTPX_WSocketOnMessage(chttpx_wsocket_t* wsocket, chttpx_wsocket_handler_t handler); + +/** + * Set the close handler for an active WebSocket. + * + * @param wsocket Active WebSocket connection. + * @param handler Callback for peer close or abrupt abort. + */ +void cHTTPX_WSocketOnClose(chttpx_wsocket_t* wsocket, chttpx_wsocket_close_handler_t handler); /** - * Send one WebSocket frame payload to the client. + * Attach opaque application data to a WebSocket. * - * @param wsocket Connected WebSocket handle. - * @param data Payload bytes. + * @param wsocket Active WebSocket connection. + * @param user_data Pointer stored on the connection. + */ +void cHTTPX_WSocketSetData(chttpx_wsocket_t* wsocket, void* user_data); + +/** + * Return opaque application data previously attached to a WebSocket. + * + * @param wsocket Active WebSocket connection. + * @return Stored user data pointer, or NULL. + */ +void* cHTTPX_WSocketData(chttpx_wsocket_t* wsocket); + +/** + * Send a text WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data UTF-8 payload bytes. * @param len Payload length in bytes. - * @return cHTTPX_OK on success or a negative error code. + * @return Zero on success or a negative error code. */ int cHTTPX_WSocketSend(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); /** - * Receive WebSocket payload bytes into a caller buffer. + * Send a binary WebSocket frame. + * + * @param wsocket Active WebSocket connection. + * @param data Binary payload bytes. + * @param len Payload length in bytes. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketSendBinary(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + +/** + * Send a WebSocket ping frame. + * + * @param wsocket Active WebSocket connection. + * @param data Optional ping payload. + * @param len Payload length in bytes (at most 125). + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketPing(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + +/** + * Send a WebSocket close frame and mark the connection closing. * - * @param wsocket Connected WebSocket handle. - * @param buffer Output buffer. - * @param len Maximum bytes to read. - * @return Number of bytes read or a negative error code. + * @param wsocket Active WebSocket connection. + * @param code Close status code, or 0 for 1000. + * @param reason Optional UTF-8 close reason. + * @return Zero on success or a negative error code. + */ +int cHTTPX_WSocketClose(chttpx_wsocket_t* wsocket, uint16_t code, const char* reason); + +/** + * Legacy HTTP/1.1 upgrade helper retained for source compatibility. + * + * Always returns unavailable: WebSockets are served over HTTP/2 Extended CONNECT. + * + * @param client_socket Unused client socket. + * @param sec_wsocket_key Unused Sec-WebSocket-Key value. + * @return Always cHTTPX_ERR_UNAVAILABLE. + */ +int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); + +/** + * Legacy synchronous receive helper retained for source compatibility. + * + * @param wsocket Unused connection handle. + * @param buffer Unused output buffer. + * @param len Unused buffer capacity. + * @return Always cHTTPX_ERR_UNAVAILABLE. */ int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len); + +/** + * Transport callback used by the HTTP/2 bridge to emit framed bytes. + * + * @param context Opaque transport context. + * @param data Encoded WebSocket frame bytes. + * @param len Frame length in bytes. + * @param end_stream Whether the DATA frame should end the stream. + * @return Zero on success or a negative error code. + */ +typedef int (*chttpx_wsocket_transport_send_fn)(void* context, const unsigned char* data, size_t len, bool end_stream); + +/** + * Look up a registered WebSocket route handler for a request path. + * + * @param server Owning server instance. + * @param path Request path, optionally with a query string. + * @return Matching route handler, or NULL when none is registered. + */ +chttpx_wsocket_route_t _chttpx_websocket_find_route(chttpx_serv_t* server, const char* path); + +/** + * Allocate a WebSocket handle bound to an HTTP/2 transport send callback. + * + * @param server Owning server instance. + * @param socket Client socket associated with the connection. + * @param request Handshake request metadata. + * @param transport_send Callback that writes encoded frames. + * @param transport_context Opaque context for transport_send. + * @return New WebSocket handle, or NULL on allocation failure. + */ +chttpx_wsocket_t* _chttpx_websocket_create(chttpx_serv_t* server, chttpx_socket_t socket, chttpx_request_t* request, + chttpx_wsocket_transport_send_fn transport_send, void* transport_context); + +/** + * Mark a WebSocket connection as ready for application traffic. + * + * @param wsocket WebSocket handle created by the HTTP/2 bridge. + */ +void _chttpx_websocket_mark_connected(chttpx_wsocket_t* wsocket); + +/** + * Feed inbound WebSocket bytes into the frame parser. + * + * @param wsocket Active WebSocket connection. + * @param data Newly received bytes. + * @param len Number of bytes in data. + * @return Zero on success or a negative protocol/error code. + */ +int _chttpx_websocket_feed(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + +/** + * Destroy a WebSocket handle and release internal buffers. + * + * @param wsocket WebSocket handle to free. + */ +void _chttpx_websocket_destroy(chttpx_wsocket_t* wsocket); + +/** + * Free all WebSocket routes registered on a server. + * + * @param server Server whose websocket_state should be released. + */ +void _chttpx_websocket_server_cleanup(chttpx_serv_t* server); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/cHTTPX_worker.c b/src/cHTTPX_worker.c index 2fa2065..06fb15f 100644 --- a/src/cHTTPX_worker.c +++ b/src/cHTTPX_worker.c @@ -36,13 +36,8 @@ struct chttpx_worker_pool chttpx_worker_execute_fn execute; void* context; -#ifdef CHTTPX_PLATFORM_WINDOWS - CRITICAL_SECTION mutex; - CONDITION_VARIABLE ready; -#else pthread_mutex_t mutex; pthread_cond_t ready; -#endif }; /** @@ -52,16 +47,12 @@ struct chttpx_worker_pool */ static uint64_t monotonic_ns(void) { -#ifdef CHTTPX_PLATFORM_WINDOWS - return GetTickCount64() * 1000000ULL; -#else struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0; return (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; -#endif } /** @@ -71,11 +62,7 @@ static uint64_t monotonic_ns(void) */ static void pool_lock(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - EnterCriticalSection(&pool->mutex); -#else pthread_mutex_lock(&pool->mutex); -#endif } /** @@ -85,11 +72,7 @@ static void pool_lock(chttpx_worker_pool_t* pool) */ static void pool_unlock(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - LeaveCriticalSection(&pool->mutex); -#else pthread_mutex_unlock(&pool->mutex); -#endif } /** @@ -99,11 +82,7 @@ static void pool_unlock(chttpx_worker_pool_t* pool) */ static void pool_wait(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - SleepConditionVariableCS(&pool->ready, &pool->mutex, INFINITE); -#else pthread_cond_wait(&pool->ready, &pool->mutex); -#endif } /** @@ -113,11 +92,7 @@ static void pool_wait(chttpx_worker_pool_t* pool) */ static void pool_signal(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - WakeConditionVariable(&pool->ready); -#else pthread_cond_signal(&pool->ready); -#endif } /** @@ -127,11 +102,7 @@ static void pool_signal(chttpx_worker_pool_t* pool) */ static void pool_broadcast(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - WakeAllConditionVariable(&pool->ready); -#else pthread_cond_broadcast(&pool->ready); -#endif } /** @@ -142,11 +113,6 @@ static void pool_broadcast(chttpx_worker_pool_t* pool) */ static int pool_sync_init(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - InitializeCriticalSection(&pool->mutex); - InitializeConditionVariable(&pool->ready); - return 0; -#else if (pthread_mutex_init(&pool->mutex, NULL) != 0) return -1; @@ -157,7 +123,6 @@ static int pool_sync_init(chttpx_worker_pool_t* pool) } return 0; -#endif } /** @@ -167,12 +132,8 @@ static int pool_sync_init(chttpx_worker_pool_t* pool) */ static void pool_sync_destroy(chttpx_worker_pool_t* pool) { -#ifdef CHTTPX_PLATFORM_WINDOWS - DeleteCriticalSection(&pool->mutex); -#else pthread_cond_destroy(&pool->ready); pthread_mutex_destroy(&pool->mutex); -#endif } /** diff --git a/tests/test_compression.c b/tests/test_compression.c index b780bf6..06df875 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -82,11 +82,7 @@ static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) { -#ifdef CHTTPX_PLATFORM_WINDOWS - Sleep(1); -#else usleep(1000); -#endif } assert(__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)); diff --git a/tests/test_core.c b/tests/test_core.c index 207885b..5242d5e 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -143,7 +143,7 @@ static void test_bind_and_json(void) char* username = NULL; chttpx_validation_t fields[] = {cHTTPX_StringField("username", &username, true, 3, 32, cHTTPX_TRIM | cHTTPX_LOWERCASE, username_validator)}; chttpx_response_t response = {0}; - assert(cHTTPX_BindJSON(&req, &response, fields, CHTTPX_ARRAY_LEN(fields))); + assert(cHTTPX_BindJSON(&req, fields, CHTTPX_ARRAY_LEN(fields)) == cHTTPX_BIND_OK); assert(strcmp(username, "valid_user") == 0); chttpx_json_t* object = cHTTPX_JsonObject(&req); diff --git a/tests/test_server.c b/tests/test_server.c index 52574b3..cfd69a8 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -214,11 +214,7 @@ static void exchange_ipv6(uint16_t port, const char* request, char* response, si static chttpx_socket_t open_idle_connection(uint16_t port) { chttpx_socket_t socket_fd = socket(AF_INET, SOCK_STREAM, 0); -#ifdef CHTTPX_PLATFORM_WINDOWS - assert(socket_fd != INVALID_SOCKET); -#else assert(socket_fd >= 0); -#endif struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_port = htons(port); @@ -227,17 +223,12 @@ static chttpx_socket_t open_idle_connection(uint16_t port) return socket_fd; } - /** Polls until server->listening is true. */ static void wait_until_listening(chttpx_serv_t* server) { for (int i = 0; i < 5000 && !__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE); i++) { -#ifdef CHTTPX_PLATFORM_WINDOWS - Sleep(1); -#else usleep(1000); -#endif } assert(__atomic_load_n(&server->listening, __ATOMIC_ACQUIRE)); diff --git a/tests/test_websocket.c b/tests/test_websocket.c new file mode 100644 index 0000000..c6c462d --- /dev/null +++ b/tests/test_websocket.c @@ -0,0 +1,101 @@ +#include "cHTTPX_websocket.h" + +#include +#include +#include + +typedef struct +{ + unsigned char data[512]; + size_t len; + int calls; + bool end_stream; +} capture_t; + +static int capture_send(void* context, const unsigned char* data, size_t len, bool end_stream) +{ + capture_t* capture = context; + assert(capture); + assert(len <= sizeof(capture->data)); + memcpy(capture->data, data, len); + capture->len = len; + capture->end_stream = end_stream; + capture->calls++; + return cHTTPX_OK; +} + +static void echo_message(chttpx_wsocket_t* websocket, const unsigned char* data, size_t len) +{ + assert(websocket->opcode == CHTTPX_WSOCKET_OPCODE_TEXT); + assert(cHTTPX_WSocketSend(websocket, data, len) == cHTTPX_OK); +} + +static void mask_frame(unsigned char* output, size_t* output_len, bool fin, int opcode, const unsigned char* payload, size_t payload_len) +{ + static const unsigned char mask[4] = {0x11, 0x22, 0x33, 0x44}; + assert(payload_len <= 125); + + output[0] = (unsigned char)((fin ? 0x80 : 0) | opcode); + output[1] = (unsigned char)(0x80 | payload_len); + memcpy(output + 2, mask, sizeof(mask)); + for (size_t i = 0; i < payload_len; i++) + output[6 + i] = payload[i] ^ mask[i & 3]; + *output_len = 6 + payload_len; +} + +int main(void) +{ + chttpx_serv_t server; + memset(&server, 0, sizeof(server)); + server.max_body_size = 1024; + + chttpx_request_t request; + memset(&request, 0, sizeof(request)); + + capture_t capture; + memset(&capture, 0, sizeof(capture)); + + chttpx_wsocket_t* websocket = _chttpx_websocket_create(&server, 1, &request, capture_send, &capture); + assert(websocket); + _chttpx_websocket_mark_connected(websocket); + cHTTPX_WSocketOnMessage(websocket, echo_message); + + unsigned char frame[256]; + size_t frame_len = 0; + + const unsigned char first[] = {'h', 'e'}; + mask_frame(frame, &frame_len, false, CHTTPX_WSOCKET_OPCODE_TEXT, first, sizeof(first)); + assert(_chttpx_websocket_feed(websocket, frame, frame_len) == cHTTPX_OK); + assert(capture.calls == 0); + + const unsigned char second[] = {'l', 'l', 'o'}; + mask_frame(frame, &frame_len, true, CHTTPX_WSOCKET_OPCODE_CONTINUATION, second, sizeof(second)); + assert(_chttpx_websocket_feed(websocket, frame, frame_len) == cHTTPX_OK); + assert(capture.calls == 1); + assert(capture.len == 7); + assert(capture.data[0] == 0x81); + assert(capture.data[1] == 5); + assert(memcmp(capture.data + 2, "hello", 5) == 0); + assert(!capture.end_stream); + + const unsigned char ping[] = {'o', 'k'}; + mask_frame(frame, &frame_len, true, CHTTPX_WSOCKET_OPCODE_PING, ping, sizeof(ping)); + assert(_chttpx_websocket_feed(websocket, frame, frame_len) == cHTTPX_OK); + assert(capture.calls == 2); + assert(capture.len == 4); + assert(capture.data[0] == 0x8A); + assert(capture.data[1] == 2); + assert(memcmp(capture.data + 2, "ok", 2) == 0); + + const unsigned char close_payload[] = {0x03, 0xE8}; + mask_frame(frame, &frame_len, true, CHTTPX_WSOCKET_OPCODE_CLOSE, close_payload, sizeof(close_payload)); + assert(_chttpx_websocket_feed(websocket, frame, frame_len) == cHTTPX_OK); + assert(capture.calls == 3); + assert(capture.data[0] == 0x88); + assert(capture.end_stream); + assert(!websocket->connected); + + _chttpx_websocket_destroy(websocket); + puts("websocket tests passed"); + return 0; +} From 1143fcd69d91c9f8180b0904fbf2a7ca58127ec7 Mon Sep 17 00:00:00 2001 From: nenishyi Date: Sun, 27 Sep 2026 21:04:13 +0500 Subject: [PATCH 47/48] feat: add first-class Server-Sent Events API (#63) * feat(sse): add event-stream content type * feat(sse): add streaming transport hooks * feat(sse): mark streaming responses * feat(sse): add public SSE helper declarations * feat(sse): implement SSE framing helpers * feat(sse): expose first-class SSE API * feat(sse): hand streaming responses to transport * feat(sse): stream events over HTTP/2 DATA frames * test(sse): cover SSE framing and lifecycle * docs(sse): add server example * docs(sse): add EventSource browser client * test(sse): exercise HTTP/2 streaming transport * test(sse): wire SSE tests and example into build * docs(sse): document SSE API * docs(sse): add Russian SSE documentation * docs(sse): add SSE to documentation index * docs(sse): add SSE to Russian documentation index * docs(sse): list SSE example * docs(sse): mention SSE in project README * docs(sse): mention SSE in Russian README * fix(sse): pump HTTP/2 flow control while flushing events * refactor(sse): simplify multiline formatter * fix(sse): preserve configured CORS headers for streams * test(sse): verify CORS on EventSource streams * test(http2): preserve content type in exchange helper * fix(sse): preserve event-stream content type in HTTP/2 calls * fix: restore valid Makefile after SSE merge * fix: restore HTTP/2 stream initialization after merge * refactor: remove Windows event loop implementation * refactor: remove Windows branch from SSE example --- Makefile | 10 +- README.md | 4 +- README_RU.md | 4 +- docs/README.md | 1 + docs/README_RU.md | 1 + docs/sse/README.md | 118 +++++++++++++ docs/sse/README_RU.md | 118 +++++++++++++ example/README.md | 2 + example/sse.c | 56 +++++++ example/sse_client.html | 23 +++ include/libchttpx.h | 41 +++++ src/cHTTPX_event.c | 234 +------------------------- src/cHTTPX_http.h | 2 + src/cHTTPX_http2.c | 359 +++++++++++++++++++++++++++++++++++++++- src/cHTTPX_request.h | 13 ++ src/cHTTPX_response.c | 16 +- src/cHTTPX_response.h | 5 +- src/cHTTPX_sse.c | 307 ++++++++++++++++++++++++++++++++++ src/cHTTPX_sse.h | 45 +++++ tests/test_server.c | 33 ++++ tests/test_sse.c | 121 ++++++++++++++ 21 files changed, 1265 insertions(+), 248 deletions(-) create mode 100644 docs/sse/README.md create mode 100644 docs/sse/README_RU.md create mode 100644 example/sse.c create mode 100644 example/sse_client.html create mode 100644 src/cHTTPX_sse.c create mode 100644 src/cHTTPX_sse.h create mode 100644 tests/test_sse.c diff --git a/Makefile b/Makefile index eeb47e8..4f2d921 100644 --- a/Makefile +++ b/Makefile @@ -38,10 +38,11 @@ TEST_TLS_TARGET = $(BINDIR)/test_tls TEST_COMPRESSION_TARGET = $(BINDIR)/test_compression TEST_METRICS_TARGET = $(BINDIR)/test_metrics TEST_WEBSOCKET_TARGET = $(BINDIR)/test_websocket +TEST_SSE_TARGET = $(BINDIR)/test_sse EXAMPLE_SRC = example/basic.c EXAMPLE_OBJ = $(OBJDIR)/example/basic.o -EXAMPLE_NAMES = basic multiple_servers local_call remote_call middleware json upload metrics websocket +EXAMPLE_NAMES = basic multiple_servers local_call remote_call middleware json upload metrics websocket sse EXAMPLE_TARGETS = $(addprefix $(BINDIR)/example-,$(EXAMPLE_NAMES)) LIN_SRCS = $(wildcard src/*.c) @@ -101,11 +102,12 @@ lib-install: libchttpx.so # Linux tests # - -test: $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) $(TEST_WEBSOCKET_TARGET) +test: $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) $(TEST_WEBSOCKET_TARGET) $(TEST_SSE_TARGET) $(TEST_TARGET) $(TEST_SERVER_TARGET) $(TEST_METRICS_TARGET) $(TEST_WEBSOCKET_TARGET) + $(TEST_SSE_TARGET) $(TEST_TARGET): tests/test_core.c $(LIN_SRCS) @mkdir -p $(BINDIR) @@ -162,6 +164,10 @@ $(TEST_WEBSOCKET_TARGET): tests/test_websocket.c $(LIN_SRCS) @mkdir -p $(BINDIR) $(CC) $(CFLAGS) -std=gnu11 -O2 -g tests/test_websocket.c $(LIN_SRCS) -o $@ $(LIN_LDFLAGS) -pthread +$(TEST_SSE_TARGET): tests/test_sse.c $(LIN_SRCS) + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -std=gnu11 -g tests/test_sse.c $(LIN_SRCS) -o $@ $(LIN_LDFLAGS) -pthread + # Linux lib compile # - diff --git a/README.md b/README.md index 03b184c..aa62588 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ The library is designed so handlers contain application logic instead of repetit - CORS, cookies, logging callbacks, and rate limiting - configurable gzip response compression with `Accept-Encoding` negotiation - optional built-in metrics with thread-safe snapshots and a Prometheus exporter +- first-class HTTP/2 Server-Sent Events with retry, heartbeat, and disconnect handling - configurable server limits and graceful shutdown -> `cHTTPX_ResFile()` currently reads the complete file into memory. Streaming responses, `sendfile()`, and zero-copy output are not implemented in the current API. +> `cHTTPX_ResFile()` currently reads the complete file into memory. SSE has its own streaming HTTP/2 path; a generic streaming response API, `sendfile()`, and zero-copy file output are not implemented yet. ## Installation @@ -176,6 +177,7 @@ Detailed documentation is split by functionality: - [Request IDs and i18n](docs/i18n/README.md) - [Logging](docs/logging/README.md) - [Rate limiting](docs/rate-limiting/README.md) +- [Server-Sent Events](docs/sse/README.md) - [WebSocket API — experimental](docs/websocket/README.md) ## License diff --git a/README_RU.md b/README_RU.md index ffe3b60..e782ca0 100644 --- a/README_RU.md +++ b/README_RU.md @@ -19,9 +19,10 @@ - CORS, cookies, callback-based logging и rate limiting - настраиваемое gzip-сжатие ответов с `Accept-Encoding` negotiation - опциональные встроенные metrics, thread-safe snapshot и Prometheus exporter +- first-class Server-Sent Events по HTTP/2 с retry, heartbeat и disconnect handling - лимиты сервера и graceful shutdown -> `cHTTPX_ResFile()` пока полностью читает файл в память. Streaming response, `sendfile()` и zero-copy output в текущем API не реализованы. +> `cHTTPX_ResFile()` пока полностью читает файл в память. Для SSE используется отдельный streaming path поверх HTTP/2; generic streaming response API, `sendfile()` и zero-copy для файлов пока не реализованы. ## Установка @@ -172,6 +173,7 @@ TLS остаётся отдельной опцией сборки: `make TLS=1 l - [Request ID и i18n](docs/i18n/README.md) - [Logging](docs/logging/README_RU.md) - [Rate limiting](docs/rate-limiting/README_RU.md) +- [Server-Sent Events](docs/sse/README_RU.md) - [WebSocket API — experimental](docs/websocket/README_RU.md) ## Лицензия diff --git a/docs/README.md b/docs/README.md index ace7ea3..5a08970 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ The root README contains only the project overview, installation methods, and na | i18n | [i18n](i18n/README.md) | request IDs, `Accept-Language`, translations | | Logging | [logging](logging/README.md) | logger callbacks and request logging middleware | | Rate limiting | [rate-limiting](rate-limiting/README.md) | per-server fixed-window limiter | +| Server-Sent Events | [sse](sse/README.md) | HTTP/2 event streams, retry, heartbeat, disconnect handling | | WebSocket | [websocket](websocket/README.md) | current experimental API and limitations | ## Recommended reading order diff --git a/docs/README_RU.md b/docs/README_RU.md index fc584e6..6666725 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -21,6 +21,7 @@ | i18n | [i18n](i18n/README_RU.md) | request ID, `Accept-Language`, translations | | Logging | [logging](logging/README_RU.md) | logger callback и logging middleware | | Rate limiting | [rate-limiting](rate-limiting/README_RU.md) | встроенный fixed-window limiter | +| Server-Sent Events | [sse](sse/README_RU.md) | HTTP/2 event streams, retry, heartbeat и disconnect handling | | WebSocket | [websocket](websocket/README_RU.md) | текущий experimental API и ограничения | ## Рекомендуемый порядок diff --git a/docs/sse/README.md b/docs/sse/README.md new file mode 100644 index 0000000..3bb9a9b --- /dev/null +++ b/docs/sse/README.md @@ -0,0 +1,118 @@ +# Server-Sent Events (SSE) + +libchttpx provides first-class Server-Sent Events for one-way real-time updates over HTTP/2. SSE is useful for notifications, progress updates, telemetry, dashboards, event feeds, and token-style streaming when the client does not need a bidirectional WebSocket. + +## Server + +Open the stream from a normal route handler: + +```c +static void events(chttpx_request_t* req, chttpx_response_t* res) +{ + chttpx_sse_t* sse = cHTTPX_SSEOpen(req, res); + if (!sse) + { + *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "failed to open SSE stream"); + return; + } + + cHTTPX_SSERetry(sse, 3000); + + for (unsigned int i = 1; i <= 10 && cHTTPX_SSEConnected(sse); i++) + { + char id[32]; + char data[128]; + snprintf(id, sizeof(id), "%u", i); + snprintf(data, sizeof(data), "{\"progress\":%u}", i * 10); + + if (cHTTPX_SSESend(sse, "progress", id, data) != cHTTPX_OK) + break; + } + + cHTTPX_SSEClose(sse); +} +``` + +`cHTTPX_SSEOpen()` immediately starts a streaming response. It automatically uses: + +- `Content-Type: text/event-stream` +- `Cache-Control: no-cache` +- `X-Accel-Buffering: no` +- the current `X-Request-ID`, when request IDs are enabled + +HTTP/2 forbids connection-specific headers such as `Connection: keep-alive`, so libchttpx does not emit them. + +## Events + +```c +cHTTPX_SSESend(sse, "message", "42", "{\"status\":\"ready\"}"); +``` + +The arguments map to the standard SSE fields: + +- `event` — optional event type +- `id` — optional event id +- `data` — event payload; embedded CR/LF line breaks are encoded as multiple `data:` lines + +The event and id values must be single-line strings. Data may be multiline. + +To change the browser reconnect delay: + +```c +cHTTPX_SSERetry(sse, 5000); +``` + +## Heartbeats and disconnects + +A comment can carry diagnostic text without dispatching an application event: + +```c +cHTTPX_SSEComment(sse, "still alive"); +``` + +For a minimal keep-alive heartbeat: + +```c +cHTTPX_SSEHeartbeat(sse); +``` + +Long-running handlers should check `cHTTPX_SSEConnected()` and stop when it becomes false. A failed event or heartbeat write also reports the disconnect through its return code. During graceful server shutdown the connected state becomes false so the handler can exit and the stream can finish cleanly. + +## Browser client + +```js +const source = new EventSource("http://localhost:8080/events"); + +source.addEventListener("progress", (event) => { + console.log(event.lastEventId, event.data); +}); + +source.onerror = () => { + console.log("connection interrupted; EventSource will retry"); +}; +``` + +The repository contains a complete pair: + +- `example/sse.c` — libchttpx SSE server +- `example/sse_client.html` — browser EventSource client + +Build the server with `make examples`, run `.build/example-sse`, then serve/open the HTML client from an origin allowed by your CORS configuration if it is not same-origin. + +## Lifecycle + +`cHTTPX_SSEClose()` is idempotent. Returning from a handler with an open SSE response also causes libchttpx to finish the HTTP/2 stream. SSE response compression is disabled automatically. + +Each `cHTTPX_SSESend()`, retry, comment, or heartbeat is submitted as streaming HTTP/2 DATA rather than accumulating the complete response in memory. + +## API + +| Function | Purpose | +| --- | --- | +| `cHTTPX_SSEOpen(req, res)` | start an SSE response | +| `cHTTPX_SSESend(sse, event, id, data)` | send an event | +| `cHTTPX_SSERetry(sse, milliseconds)` | set reconnect delay | +| `cHTTPX_SSEComment(sse, comment)` | send a comment | +| `cHTTPX_SSEHeartbeat(sse)` | send a keep-alive comment | +| `cHTTPX_SSEConnected(sse)` | test whether the stream is still writable | +| `cHTTPX_SSEClose(sse)` | finish the stream | diff --git a/docs/sse/README_RU.md b/docs/sse/README_RU.md new file mode 100644 index 0000000..c236763 --- /dev/null +++ b/docs/sse/README_RU.md @@ -0,0 +1,118 @@ +# Server-Sent Events (SSE) + +В libchttpx есть first-class API для Server-Sent Events — односторонней передачи событий сервером клиенту поверх HTTP/2. SSE подходит для уведомлений, прогресса задач, телеметрии, dashboard/event feed и потоковой выдачи данных, когда двусторонний WebSocket не нужен. + +## Сервер + +SSE открывается прямо из обычного route handler: + +```c +static void events(chttpx_request_t* req, chttpx_response_t* res) +{ + chttpx_sse_t* sse = cHTTPX_SSEOpen(req, res); + if (!sse) + { + *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "failed to open SSE stream"); + return; + } + + cHTTPX_SSERetry(sse, 3000); + + for (unsigned int i = 1; i <= 10 && cHTTPX_SSEConnected(sse); i++) + { + char id[32]; + char data[128]; + snprintf(id, sizeof(id), "%u", i); + snprintf(data, sizeof(data), "{\"progress\":%u}", i * 10); + + if (cHTTPX_SSESend(sse, "progress", id, data) != cHTTPX_OK) + break; + } + + cHTTPX_SSEClose(sse); +} +``` + +`cHTTPX_SSEOpen()` сразу начинает streaming response и автоматически задаёт: + +- `Content-Type: text/event-stream` +- `Cache-Control: no-cache` +- `X-Accel-Buffering: no` +- текущий `X-Request-ID`, если request ID включён + +В HTTP/2 connection-specific headers вроде `Connection: keep-alive` запрещены, поэтому libchttpx их не отправляет. + +## События + +```c +cHTTPX_SSESend(sse, "message", "42", "{\"status\":\"ready\"}"); +``` + +Аргументы соответствуют полям SSE: + +- `event` — необязательный тип события +- `id` — необязательный id события +- `data` — данные; переносы CR/LF автоматически превращаются в несколько строк `data:` + +`event` и `id` должны быть однострочными. `data` может быть многострочным. + +Задать задержку повторного подключения браузера: + +```c +cHTTPX_SSERetry(sse, 5000); +``` + +## Heartbeat и отключение клиента + +Комментарий без создания application event: + +```c +cHTTPX_SSEComment(sse, "still alive"); +``` + +Минимальный keep-alive heartbeat: + +```c +cHTTPX_SSEHeartbeat(sse); +``` + +Долгоживущий handler должен проверять `cHTTPX_SSEConnected()` и завершать цикл, когда функция возвращает `false`. Ошибка `cHTTPX_SSESend()` или heartbeat также показывает, что запись в клиент больше невозможна. При graceful shutdown сервера connected-state становится `false`, поэтому handler может корректно выйти и закрыть поток. + +## Browser EventSource + +```js +const source = new EventSource("http://localhost:8080/events"); + +source.addEventListener("progress", (event) => { + console.log(event.lastEventId, event.data); +}); + +source.onerror = () => { + console.log("connection interrupted; EventSource will retry"); +}; +``` + +В репозитории есть готовая пара: + +- `example/sse.c` — SSE-сервер на libchttpx +- `example/sse_client.html` — браузерный EventSource-клиент + +Сервер собирается через `make examples` и запускается как `.build/example-sse`. Если HTML открыт не с того же origin, настройте CORS. + +## Жизненный цикл + +`cHTTPX_SSEClose()` идемпотентен. Если handler вернулся с открытым SSE response, libchttpx также завершит HTTP/2 stream. Сжатие SSE отключается автоматически. + +Каждый вызов `cHTTPX_SSESend()`, retry, comment или heartbeat отправляется как streaming HTTP/2 DATA, а не накапливается целиком в памяти до завершения handler. + +## API + +| Функция | Назначение | +| --- | --- | +| `cHTTPX_SSEOpen(req, res)` | открыть SSE response | +| `cHTTPX_SSESend(sse, event, id, data)` | отправить событие | +| `cHTTPX_SSERetry(sse, milliseconds)` | задать reconnect delay | +| `cHTTPX_SSEComment(sse, comment)` | отправить комментарий | +| `cHTTPX_SSEHeartbeat(sse)` | отправить keep-alive | +| `cHTTPX_SSEConnected(sse)` | проверить доступность потока | +| `cHTTPX_SSEClose(sse)` | завершить поток | diff --git a/example/README.md b/example/README.md index 7395b63..f28ebae 100644 --- a/example/README.md +++ b/example/README.md @@ -21,5 +21,7 @@ Generated binaries are placed in `.build/`: - `example-middleware` - `example-json` - `example-upload` +- `example-metrics` +- `example-sse` The normal `make lin` demo target uses `example/basic.c`. diff --git a/example/sse.c b/example/sse.c new file mode 100644 index 0000000..ef3b454 --- /dev/null +++ b/example/sse.c @@ -0,0 +1,56 @@ +#include + +#include +#include + +static void events(chttpx_request_t* req, chttpx_response_t* res) +{ + chttpx_sse_t* sse = cHTTPX_SSEOpen(req, res); + if (!sse) + { + *res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "failed to open SSE stream"); + return; + } + + if (cHTTPX_SSERetry(sse, 3000) != cHTTPX_OK) + return; + + for (unsigned int i = 1; i <= 10 && cHTTPX_SSEConnected(sse); i++) + { + char id[32]; + char data[128]; + snprintf(id, sizeof(id), "%u", i); + snprintf(data, sizeof(data), "{\"progress\":%u}", i * 10); + + if (cHTTPX_SSESend(sse, "progress", id, data) != cHTTPX_OK) + break; + + sleep(1); + } + + cHTTPX_SSEClose(sse); +} + +int main(void) +{ + chttpx_app_t app; + if (cHTTPX_AppInit(&app) != cHTTPX_OK) + return 1; + + chttpx_config_t config = cHTTPX_DefaultConfig(); + config.port = 8080; + + chttpx_serv_t* server = cHTTPX_AppServer(&app, "sse", &config); + if (!server) + { + cHTTPX_AppShutdown(&app); + return 1; + } + + chttpx_router_t router = cHTTPX_RoutePathPrefix(server, ""); + cHTTPX_Get(&router, "/events", events); + + int result = cHTTPX_AppRun(&app); + cHTTPX_AppShutdown(&app); + return result == cHTTPX_OK ? 0 : 1; +} diff --git a/example/sse_client.html b/example/sse_client.html new file mode 100644 index 0000000..b464cf3 --- /dev/null +++ b/example/sse_client.html @@ -0,0 +1,23 @@ + + + + + libchttpx SSE example + + +

Server-Sent Events

+

+    
+
+
diff --git a/include/libchttpx.h b/include/libchttpx.h
index 14a9033..09b4a91 100644
--- a/include/libchttpx.h
+++ b/include/libchttpx.h
@@ -102,6 +102,8 @@ extern "C"
 #define cHTTPX_CTYPE_CSV "text/csv"
 /* JSON data. Use for REST API responses and requests. */
 #define cHTTPX_CTYPE_JSON "application/json"
+/* Server-Sent Events stream. */
+#define cHTTPX_CTYPE_SSE "text/event-stream"
 /* URL-encoded form data. Typical for HTML form submissions. */
 #define cHTTPX_CTYPE_FORM "application/x-www-form-urlencoded"
 /* Multipart form data. Used for file uploads via forms. */
@@ -445,6 +447,18 @@ extern "C"
 
     typedef int (*chttpx_body_chunk_fn)(const unsigned char* data, size_t size, void* user_data);
 
+    struct chttpx_response;
+
+    /** Internal streaming transport used by long-lived response helpers such as SSE. */
+    typedef struct
+    {
+        void* context;
+        int (*open)(void* context, const struct chttpx_response* response);
+        int (*write)(void* context, const void* data, size_t size);
+        int (*close)(void* context);
+        bool (*connected)(void* context);
+    } chttpx_stream_transport_t;
+
     // REQuest
     typedef struct
     {
@@ -525,6 +539,7 @@ extern "C"
 
         /* Internal transport state. NULL for plain HTTP. */
         void* _tls_session;
+        chttpx_stream_transport_t _stream_transport;
 
         /* Internal request lifecycle state. */
         void* _cleanup_entries;
@@ -790,6 +805,9 @@ extern "C"
         /* Set when response compression must be bypassed. */
         bool compression_disabled;
 
+        /* Internal marker for a response already owned by a streaming transport. */
+        bool _streaming_response;
+
         /* Times for logging */
         struct timespec start_ts;
         struct timespec end_ts;
@@ -886,6 +904,29 @@ extern "C"
      */
     int cHTTPX_SendAll(chttpx_socket_t fd, const void* data, size_t size);
 
+    typedef struct chttpx_sse chttpx_sse_t;
+
+    /** Open a Server-Sent Events stream and send its response headers immediately. */
+    chttpx_sse_t* cHTTPX_SSEOpen(chttpx_request_t* req, chttpx_response_t* res);
+
+    /** Send one SSE event. event and id are optional; data may contain multiple lines. */
+    int cHTTPX_SSESend(chttpx_sse_t* sse, const char* event, const char* id, const char* data);
+
+    /** Send a retry directive in milliseconds. */
+    int cHTTPX_SSERetry(chttpx_sse_t* sse, uint64_t milliseconds);
+
+    /** Send an SSE comment, splitting multiline comments correctly. */
+    int cHTTPX_SSEComment(chttpx_sse_t* sse, const char* comment);
+
+    /** Send an empty SSE comment suitable for keep-alive heartbeats. */
+    int cHTTPX_SSEHeartbeat(chttpx_sse_t* sse);
+
+    /** Return whether the underlying client stream is still writable. */
+    bool cHTTPX_SSEConnected(const chttpx_sse_t* sse);
+
+    /** Gracefully finish the SSE stream. Safe to call more than once. */
+    int cHTTPX_SSEClose(chttpx_sse_t* sse);
+
 #ifdef __cplusplus
     extern
 }
diff --git a/src/cHTTPX_event.c b/src/cHTTPX_event.c
index 437663a..d67cb18 100644
--- a/src/cHTTPX_event.c
+++ b/src/cHTTPX_event.c
@@ -4,239 +4,7 @@
 #include 
 #include 
 
-#ifdef CHTTPX_PLATFORM_WINDOWS
-
-struct chttpx_event_loop
-{
-    WSAPOLLFD* pollfds;
-    void** data;
-    size_t count;
-    size_t capacity;
-    volatile LONG wake_pending;
-};
-
-/**
- * Translate portable event flags to poll/epoll/kqueue flags.
- *
- * @param events Poll interest mask or output event array.
- * @return Zero on success or a negative error code.
- */
-static short native_events(uint32_t events)
-{
-    short value = 0;
-    if (events & CHTTPX_EVENT_READ)
-        value |= POLLRDNORM;
-    if (events & CHTTPX_EVENT_WRITE)
-        value |= POLLWRNORM;
-    return value;
-}
-
-/**
- * Grow pollfd tables for the fallback event loop.
- *
- * @param loop Event loop instance.
- * @param capacity Initial capacity when growing an internal table.
- * @return Zero on success or a negative error code.
- */
-static int ensure_capacity(chttpx_event_loop_t* loop, size_t capacity)
-{
-    if (capacity <= loop->capacity)
-        return 1;
-    size_t next = loop->capacity ? loop->capacity * 2 : 64;
-    while (next < capacity)
-        next *= 2;
-    WSAPOLLFD* pollfds = realloc(loop->pollfds, next * sizeof(*pollfds));
-    if (!pollfds)
-        return 0;
-    void** data = realloc(loop->data, next * sizeof(*data));
-    if (!data)
-    {
-        loop->pollfds = pollfds;
-        return 0;
-    }
-    loop->pollfds = pollfds;
-    loop->data = data;
-    loop->capacity = next;
-    return 1;
-}
-
-/**
- * Find the index of a registered socket descriptor.
- *
- * @param loop Event loop instance.
- * @param fd Socket descriptor.
- * @return Zero on success or a negative error code.
- */
-static ptrdiff_t find_fd(chttpx_event_loop_t* loop, chttpx_socket_t fd)
-{
-    for (size_t i = 0; i < loop->count; i++)
-        if (loop->pollfds[i].fd == fd)
-            return (ptrdiff_t)i;
-    return -1;
-}
-
-/**
- * Allocate a platform-specific event loop.
- * @return New event loop, or NULL on allocation failure.
- */
-chttpx_event_loop_t* _chttpx_event_create(void)
-{
-    return calloc(1, sizeof(chttpx_event_loop_t));
-}
-
-/**
- * Free an event loop and registered state.
- *
- * @param loop Event loop instance.
- */
-void _chttpx_event_destroy(chttpx_event_loop_t* loop)
-{
-    if (!loop)
-        return;
-    free(loop->pollfds);
-    free(loop->data);
-    free(loop);
-}
-
-/**
- * Register a socket for read/write monitoring.
- *
- * @param loop Event loop instance.
- * @param fd Socket descriptor.
- * @param events Poll interest mask or output event array.
- * @param data Payload bytes for the current chunk.
- * @return Zero on success or a negative error code.
- */
-int _chttpx_event_add(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data)
-{
-    if (!loop || find_fd(loop, fd) >= 0 || !ensure_capacity(loop, loop->count + 1))
-        return -1;
-    loop->pollfds[loop->count].fd = fd;
-    loop->pollfds[loop->count].events = native_events(events);
-    loop->pollfds[loop->count].revents = 0;
-    loop->data[loop->count] = data;
-    loop->count++;
-    return 0;
-}
-
-/**
- * Update interest flags or user data for a socket.
- *
- * @param loop Event loop instance.
- * @param fd Socket descriptor.
- * @param events Poll interest mask or output event array.
- * @param data Payload bytes for the current chunk.
- * @return Zero on success or a negative error code.
- */
-int _chttpx_event_mod(chttpx_event_loop_t* loop, chttpx_socket_t fd, uint32_t events, void* data)
-{
-    if (!loop)
-        return -1;
-    ptrdiff_t index = find_fd(loop, fd);
-    if (index < 0)
-        return -1;
-    loop->pollfds[index].events = native_events(events);
-    loop->data[index] = data;
-    return 0;
-}
-
-/**
- * Remove a socket from the event loop.
- *
- * @param loop Event loop instance.
- * @param fd Socket descriptor.
- */
-void _chttpx_event_del(chttpx_event_loop_t* loop, chttpx_socket_t fd)
-{
-    if (!loop)
-        return;
-    ptrdiff_t index = find_fd(loop, fd);
-    if (index < 0)
-        return;
-    size_t i = (size_t)index;
-    size_t last = loop->count - 1;
-    if (i != last)
-    {
-        loop->pollfds[i] = loop->pollfds[last];
-        loop->data[i] = loop->data[last];
-    }
-    loop->count--;
-}
-
-/**
- * Wait for ready sockets or a wake signal.
- *
- * @param loop Event loop instance.
- * @param events Poll interest mask or output event array.
- * @param max_events Capacity of the output event array.
- * @param timeout_ms Maximum wait in milliseconds, or -1 to block indefinitely.
- * @return Zero on success or a negative error code.
- */
-int _chttpx_event_wait(chttpx_event_loop_t* loop, chttpx_event_t* events, size_t max_events, int timeout_ms)
-{
-    if (!loop || !events || max_events == 0)
-        return -1;
-
-    size_t output = 0;
-    if (InterlockedExchange(&loop->wake_pending, 0) != 0)
-        events[output++] = (chttpx_event_t){.fd = INVALID_SOCKET, .events = CHTTPX_EVENT_WAKE, .data = NULL};
-
-    if (output == max_events)
-        return (int)output;
-
-    int wait_ms = timeout_ms;
-    if (wait_ms < 0 || wait_ms > 10)
-        wait_ms = 10;
-
-    int ready = WSAPoll(loop->pollfds, (ULONG)loop->count, wait_ms);
-    if (ready == SOCKET_ERROR)
-        return -1;
-
-    for (size_t i = 0; i < loop->count && output < max_events; i++)
-    {
-        short revents = loop->pollfds[i].revents;
-        if (!revents)
-            continue;
-        uint32_t value = 0;
-        if (revents & (POLLRDNORM | POLLRDBAND | POLLIN))
-            value |= CHTTPX_EVENT_READ;
-        if (revents & (POLLWRNORM | POLLOUT))
-            value |= CHTTPX_EVENT_WRITE;
-        if (revents & (POLLERR | POLLHUP | POLLNVAL))
-            value |= CHTTPX_EVENT_ERROR;
-        events[output++] = (chttpx_event_t){.fd = loop->pollfds[i].fd, .events = value, .data = loop->data[i]};
-    }
-
-    if (output < max_events && InterlockedExchange(&loop->wake_pending, 0) != 0)
-        events[output++] = (chttpx_event_t){.fd = INVALID_SOCKET, .events = CHTTPX_EVENT_WAKE, .data = NULL};
-
-    return (int)output;
-}
-
-/**
- * Interrupt a blocking event wait.
- *
- * @param loop Event loop instance.
- */
-void _chttpx_event_wake(chttpx_event_loop_t* loop)
-{
-    if (loop)
-        InterlockedExchange(&loop->wake_pending, 1);
-}
-
-/**
- * Set a socket to non-blocking mode.
- *
- * @param fd Socket descriptor.
- * @return Zero on success or a negative error code.
- */
-int _chttpx_socket_set_nonblocking(chttpx_socket_t fd)
-{
-    u_long mode = 1;
-    return ioctlsocket(fd, FIONBIO, &mode) == 0 ? 0 : -1;
-}
-
-#elif defined(__linux__)
+#if defined(__linux__)
 
 #include 
 #include 
diff --git a/src/cHTTPX_http.h b/src/cHTTPX_http.h
index 3cd1c1f..771a2e5 100644
--- a/src/cHTTPX_http.h
+++ b/src/cHTTPX_http.h
@@ -26,6 +26,8 @@ extern "C"
 #define cHTTPX_CTYPE_CSV "text/csv"
 /* JSON data. Use for REST API responses and requests. */
 #define cHTTPX_CTYPE_JSON "application/json"
+/* Server-Sent Events stream. */
+#define cHTTPX_CTYPE_SSE "text/event-stream"
 /* URL-encoded form data. Typical for HTML form submissions. */
 #define cHTTPX_CTYPE_FORM "application/x-www-form-urlencoded"
 /* Multipart form data. Used for file uploads via forms. */
diff --git a/src/cHTTPX_http2.c b/src/cHTTPX_http2.c
index 5f7a4a0..536ae7f 100644
--- a/src/cHTTPX_http2.c
+++ b/src/cHTTPX_http2.c
@@ -21,6 +21,17 @@
 #define CHTTPX_H2_CALL_TIMEOUT_SEC 30
 #define CHTTPX_H2_MAX_AUTHORITY 512
 
+typedef struct chttpx_h2_server chttpx_h2_server_t;
+
+typedef struct chttpx_h2_sse_chunk
+{
+    unsigned char* data;
+    size_t size;
+    size_t offset;
+    bool completed;
+    struct chttpx_h2_sse_chunk* next;
+} chttpx_h2_sse_chunk_t;
+
 /**
  * Per-stream HTTP/2 request/response state on the server.
  */
@@ -41,27 +52,39 @@ typedef struct chttpx_h2_stream
     size_t body_capacity;
     size_t body_limit;
     bool responded;
+    bool request_ready;
+    bool ready_queued;
+    bool handler_active;
+    bool closed;
+    bool sse_open;
+    bool sse_closing;
+    bool sse_connected;
     int error_status;
     char* response;
     size_t response_size;
     size_t response_body_offset;
     size_t response_body_sent;
-    struct chttpx_h2_server* connection;
+    chttpx_h2_server_t* connection;
     chttpx_wsocket_t* websocket;
     chttpx_request_t* websocket_request;
+    struct chttpx_h2_stream* ready_next;
+    chttpx_h2_sse_chunk_t* sse_chunks;
+    chttpx_h2_sse_chunk_t* sse_chunks_tail;
 } chttpx_h2_stream_t;
 
 /**
  * Server-side HTTP/2 session bound to one connection.
  */
-typedef struct chttpx_h2_server
+struct chttpx_h2_server
 {
     chttpx_serv_t* server;
     chttpx_socket_t fd;
     void* tls_session;
     nghttp2_session* session;
     chttpx_h2_ws_send_t* ws_outgoing;
-} chttpx_h2_server_t;
+    chttpx_h2_stream_t* ready_head;
+    chttpx_h2_stream_t* ready_tail;
+};
 
 struct chttpx_h2_ws_send
 {
@@ -180,9 +203,56 @@ static void h2_stream_free(chttpx_h2_stream_t* stream)
         free(stream->websocket_request->path);
         free(stream->websocket_request);
     }
+    chttpx_h2_sse_chunk_t* chunk = stream->sse_chunks;
+    while (chunk)
+    {
+        chttpx_h2_sse_chunk_t* next = chunk->next;
+        free(chunk->data);
+        free(chunk);
+        chunk = next;
+    }
     free(stream);
 }
 
+static void h2_ready_push(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream)
+{
+    if (!connection || !stream || stream->ready_queued)
+        return;
+    stream->ready_queued = true;
+    stream->ready_next = NULL;
+    if (connection->ready_tail)
+        connection->ready_tail->ready_next = stream;
+    else
+        connection->ready_head = stream;
+    connection->ready_tail = stream;
+}
+
+static void h2_ready_remove(chttpx_h2_server_t* connection, chttpx_h2_stream_t* stream)
+{
+    if (!connection || !stream || !stream->ready_queued)
+        return;
+
+    chttpx_h2_stream_t* previous = NULL;
+    chttpx_h2_stream_t* current = connection->ready_head;
+    while (current)
+    {
+        if (current == stream)
+        {
+            if (previous)
+                previous->ready_next = current->ready_next;
+            else
+                connection->ready_head = current->ready_next;
+            if (connection->ready_tail == current)
+                connection->ready_tail = previous;
+            current->ready_next = NULL;
+            current->ready_queued = false;
+            return;
+        }
+        previous = current;
+        current = current->ready_next;
+    }
+}
+
 /**
  * Decode one HPACK header into stream request state.
  *
@@ -462,6 +532,217 @@ static ssize_t h2_server_send(nghttp2_session* session, const uint8_t* data, siz
                : NGHTTP2_ERR_CALLBACK_FAILURE;
 }
 
+static void h2_sse_reap_chunks(chttpx_h2_stream_t* stream)
+{
+    while (stream && stream->sse_chunks && stream->sse_chunks->completed)
+    {
+        chttpx_h2_sse_chunk_t* chunk = stream->sse_chunks;
+        stream->sse_chunks = chunk->next;
+        if (!stream->sse_chunks)
+            stream->sse_chunks_tail = NULL;
+        free(chunk->data);
+        free(chunk);
+    }
+}
+
+static ssize_t h2_sse_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data)
+{
+    (void)session;
+    (void)stream_id;
+    (void)user_data;
+    chttpx_h2_sse_chunk_t* chunk = source ? source->ptr : NULL;
+    if (!chunk || !data_flags)
+        return NGHTTP2_ERR_CALLBACK_FAILURE;
+
+    size_t remaining = chunk->size - chunk->offset;
+    size_t take = remaining < length ? remaining : length;
+    if (take)
+        memcpy(buf, chunk->data + chunk->offset, take);
+    chunk->offset += take;
+
+    if (chunk->offset >= chunk->size)
+    {
+        chunk->completed = true;
+        *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+    }
+    return (ssize_t)take;
+}
+
+static int h2_sse_queue(chttpx_h2_stream_t* stream, const void* data, size_t size, uint8_t flags)
+{
+    if (!stream || !stream->connection || !stream->connection->session || stream->closed)
+        return cHTTPX_ERR_STATE;
+
+    chttpx_h2_sse_chunk_t* chunk = calloc(1, sizeof(*chunk));
+    if (!chunk)
+        return cHTTPX_ERR_MEMORY;
+
+    if (size)
+    {
+        chunk->data = malloc(size);
+        if (!chunk->data)
+        {
+            free(chunk);
+            return cHTTPX_ERR_MEMORY;
+        }
+        memcpy(chunk->data, data, size);
+    }
+    chunk->size = size;
+
+    if (stream->sse_chunks_tail)
+        stream->sse_chunks_tail->next = chunk;
+    else
+        stream->sse_chunks = chunk;
+    stream->sse_chunks_tail = chunk;
+
+    nghttp2_data_provider provider = {
+        .source = {.ptr = chunk},
+        .read_callback = h2_sse_read,
+    };
+
+    int rv = nghttp2_submit_data(stream->connection->session, flags, stream->id, &provider);
+    if (rv != 0)
+    {
+        if (stream->sse_chunks == chunk)
+            stream->sse_chunks = NULL;
+        else
+        {
+            chttpx_h2_sse_chunk_t* previous = stream->sse_chunks;
+            while (previous && previous->next != chunk)
+                previous = previous->next;
+            if (previous)
+                previous->next = NULL;
+        }
+        stream->sse_chunks_tail = stream->sse_chunks;
+        while (stream->sse_chunks_tail && stream->sse_chunks_tail->next)
+            stream->sse_chunks_tail = stream->sse_chunks_tail->next;
+        free(chunk->data);
+        free(chunk);
+        stream->sse_connected = false;
+        return cHTTPX_ERR_PROTOCOL;
+    }
+
+    if (nghttp2_session_send(stream->connection->session) != 0)
+    {
+        stream->sse_connected = false;
+        return cHTTPX_ERR_IO;
+    }
+
+    while (!chunk->completed && !stream->closed &&
+           !__atomic_load_n(&stream->connection->server->shutdown_requested, __ATOMIC_ACQUIRE))
+    {
+        unsigned char input[BUFFER_SIZE];
+        int received = _chttpx_io_recv(stream->connection->fd, stream->connection->tls_session, input, sizeof(input));
+        if (received <= 0)
+        {
+            stream->sse_connected = false;
+            return received == cHTTPX_ERR_TIMEOUT ? cHTTPX_ERR_TIMEOUT : cHTTPX_ERR_IO;
+        }
+
+        size_t offset = 0;
+        while (offset < (size_t)received)
+        {
+            ssize_t consumed = nghttp2_session_mem_recv(stream->connection->session, input + offset, (size_t)received - offset);
+            if (consumed <= 0)
+            {
+                stream->sse_connected = false;
+                return cHTTPX_ERR_PROTOCOL;
+            }
+            offset += (size_t)consumed;
+        }
+
+        if (nghttp2_session_send(stream->connection->session) != 0)
+        {
+            stream->sse_connected = false;
+            return cHTTPX_ERR_IO;
+        }
+    }
+
+    bool closed = stream->closed;
+    h2_sse_reap_chunks(stream);
+    if (closed && !(flags & NGHTTP2_FLAG_END_STREAM))
+        return cHTTPX_ERR_STATE;
+    if (__atomic_load_n(&stream->connection->server->shutdown_requested, __ATOMIC_ACQUIRE) && !(flags & NGHTTP2_FLAG_END_STREAM))
+        return cHTTPX_ERR_STATE;
+    return cHTTPX_OK;
+}
+
+static int h2_sse_open(void* context, const struct chttpx_response* response)
+{
+    chttpx_h2_stream_t* stream = context;
+    if (!stream || !stream->connection || !response || stream->closed || stream->sse_open)
+        return cHTTPX_ERR_STATE;
+
+    char status_text[4];
+    snprintf(status_text, sizeof(status_text), "%03d", response->status);
+
+    nghttp2_nv headers[MAX_HEADERS + 2];
+    char names[MAX_HEADERS][MAX_HEADER_NAME];
+    size_t count = 0;
+    headers[count++] = h2_nv(":status", status_text);
+    headers[count++] = h2_nv("content-type", response->content_type ? response->content_type : cHTTPX_CTYPE_SSE);
+
+    for (size_t i = 0; i < response->headers_count && count < CHTTPX_ARRAY_LEN(headers); i++)
+    {
+        if (h2_forbidden_header(response->headers[i].name) ||
+            strcasecmp(response->headers[i].name, "content-length") == 0 ||
+            strcasecmp(response->headers[i].name, "content-type") == 0)
+            continue;
+
+        size_t name_size = strlen(response->headers[i].name);
+        if (name_size >= sizeof(names[0]))
+            return cHTTPX_ERR_LIMIT;
+        for (size_t j = 0; j <= name_size; j++)
+            names[i][j] = (char)tolower((unsigned char)response->headers[i].name[j]);
+        headers[count++] = h2_nv(names[i], response->headers[i].value);
+    }
+
+    int rv = nghttp2_submit_headers(stream->connection->session, NGHTTP2_FLAG_NONE, stream->id, NULL, headers, count, NULL);
+    if (rv != 0)
+        return cHTTPX_ERR_PROTOCOL;
+
+    stream->responded = true;
+    stream->sse_open = true;
+    stream->sse_connected = true;
+
+    if (nghttp2_session_send(stream->connection->session) != 0)
+    {
+        stream->sse_connected = false;
+        return cHTTPX_ERR_IO;
+    }
+    return cHTTPX_OK;
+}
+
+static int h2_sse_write(void* context, const void* data, size_t size)
+{
+    chttpx_h2_stream_t* stream = context;
+    if (!stream || !data || !size || !stream->sse_open || stream->sse_closing || stream->closed ||
+        !stream->connection || __atomic_load_n(&stream->connection->server->shutdown_requested, __ATOMIC_ACQUIRE))
+        return cHTTPX_ERR_STATE;
+    return h2_sse_queue(stream, data, size, NGHTTP2_FLAG_NONE);
+}
+
+static int h2_sse_close(void* context)
+{
+    chttpx_h2_stream_t* stream = context;
+    if (!stream || !stream->sse_open || stream->closed)
+        return cHTTPX_OK;
+    if (stream->sse_closing)
+        return cHTTPX_OK;
+
+    stream->sse_closing = true;
+    stream->sse_connected = false;
+    return h2_sse_queue(stream, NULL, 0, NGHTTP2_FLAG_END_STREAM);
+}
+
+static bool h2_sse_connected(void* context)
+{
+    chttpx_h2_stream_t* stream = context;
+    if (!stream || !stream->connection || !stream->sse_open || stream->sse_closing || stream->closed || !stream->sse_connected)
+        return false;
+    return !__atomic_load_n(&stream->connection->server->shutdown_requested, __ATOMIC_ACQUIRE);
+}
+
 /**
  * nghttp2 data provider for serialized response bodies.
  *
@@ -812,10 +1093,35 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t
 
     char* response = NULL;
     size_t response_size = 0;
+    chttpx_stream_transport_t stream_transport = {
+        .context = stream,
+        .open = h2_sse_open,
+        .write = h2_sse_write,
+        .close = h2_sse_close,
+        .connected = h2_sse_connected,
+    };
+
+    stream->handler_active = true;
     int execute_result = _chttpx_execute_prefetched(connection->server, connection->fd, connection->tls_session,
-        headers, header_size, body, body_size, NULL, body_size, &response, &response_size);
+        headers, header_size, body, body_size, NULL, body_size, &stream_transport, &response, &response_size);
     free(headers);
 
+    if (execute_result == cHTTPX_OK && !response && response_size == SIZE_MAX)
+    {
+        stream->handler_active = false;
+        if (stream->closed)
+            h2_stream_free(stream);
+        return 0;
+    }
+
+    stream->handler_active = false;
+    if (stream->closed)
+    {
+        free(response);
+        h2_stream_free(stream);
+        return 0;
+    }
+
     if (execute_result != cHTTPX_OK || !response)
     {
         free(response);
@@ -827,6 +1133,24 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t
     return h2_submit_serialized_response(connection, stream);
 }
 
+static int h2_process_ready(chttpx_h2_server_t* connection)
+{
+    while (connection && connection->ready_head)
+    {
+        chttpx_h2_stream_t* stream = connection->ready_head;
+        connection->ready_head = stream->ready_next;
+        if (!connection->ready_head)
+            connection->ready_tail = NULL;
+        stream->ready_next = NULL;
+        stream->ready_queued = false;
+
+        int rv = h2_process_request(connection, stream);
+        if (rv != 0)
+            return rv;
+    }
+    return 0;
+}
+
 /**
  * Allocate stream state when request headers begin.
  *
@@ -837,14 +1161,15 @@ static int h2_process_request(chttpx_h2_server_t* connection, chttpx_h2_stream_t
  */
 static int h2_server_begin_headers(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
 {
-    if (!frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST)
+    chttpx_h2_server_t* connection = user_data;
+    if (!connection || !frame || frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST)
         return 0;
 
     chttpx_h2_stream_t* stream = calloc(1, sizeof(*stream));
     if (!stream)
         return NGHTTP2_ERR_CALLBACK_FAILURE;
     stream->id = frame->hd.stream_id;
-    stream->connection = user_data;
+    stream->connection = connection;
 
     int rv = nghttp2_session_set_stream_user_data(session, stream->id, stream);
     if (rv != 0)
@@ -951,8 +1276,12 @@ static int h2_server_frame_recv(nghttp2_session* session, const nghttp2_frame* f
     if (stream->websocket)
         return 0;
 
-    int rv = h2_process_request(connection, stream);
-    return rv == 0 ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
+    if (!stream->request_ready)
+    {
+        stream->request_ready = true;
+        h2_ready_push(connection, stream);
+    }
+    return 0;
 }
 
 /**
@@ -976,8 +1305,13 @@ static int h2_server_stream_close(nghttp2_session* session, int32_t stream_id, u
     chttpx_h2_stream_t* stream = h2_stream(session, stream_id);
     if (stream)
     {
+        if (connection)
+            h2_ready_remove(connection, stream);
+        stream->closed = true;
+        stream->sse_connected = false;
         nghttp2_session_set_stream_user_data(session, stream_id, NULL);
-        h2_stream_free(stream);
+        if (!stream->handler_active)
+            h2_stream_free(stream);
     }
     return 0;
 }
@@ -1059,6 +1393,12 @@ int _chttpx_http2_serve(chttpx_serv_t* server, chttpx_socket_t client_fd, void*
             offset += (size_t)consumed;
         }
 
+        if (h2_process_ready(&connection) != 0)
+        {
+            result = cHTTPX_ERR_PROTOCOL;
+            break;
+        }
+
         if (nghttp2_session_send(connection.session) != 0)
         {
             result = cHTTPX_ERR_IO;
@@ -1216,6 +1556,7 @@ static const char* h2_stable_content_type(const char* content_type)
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_CSS);
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_CSV);
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_JSON);
+    CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_SSE);
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_FORM);
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_MULTI);
     CHTTPX_H2_MATCH_CTYPE(cHTTPX_CTYPE_OCTET);
diff --git a/src/cHTTPX_request.h b/src/cHTTPX_request.h
index 66dfa4f..68e76f7 100644
--- a/src/cHTTPX_request.h
+++ b/src/cHTTPX_request.h
@@ -196,6 +196,18 @@ extern "C"
     /** Chunk callback used by cHTTPX_OnBodyChunk. */
     typedef int (*chttpx_body_chunk_fn)(const unsigned char* data, size_t size, void* user_data);
 
+    struct chttpx_response;
+
+    /** Internal streaming transport used by long-lived response helpers such as SSE. */
+    typedef struct
+    {
+        void* context;
+        int (*open)(void* context, const struct chttpx_response* response);
+        int (*write)(void* context, const void* data, size_t size);
+        int (*close)(void* context);
+        bool (*connected)(void* context);
+    } chttpx_stream_transport_t;
+
     /** Per-request HTTP state populated by the server parser. */
     typedef struct
     {
@@ -276,6 +288,7 @@ extern "C"
 
         /* Internal transport state. NULL for plain HTTP. */
         void* _tls_session;
+        chttpx_stream_transport_t _stream_transport;
 
         /* Internal request lifecycle state. */
         void* _cleanup_entries;
diff --git a/src/cHTTPX_response.c b/src/cHTTPX_response.c
index 658a7fb..bad6856 100644
--- a/src/cHTTPX_response.c
+++ b/src/cHTTPX_response.c
@@ -1036,7 +1036,7 @@ int _chttpx_dispatch(chttpx_serv_t* server, chttpx_request_t* req, chttpx_respon
     return cHTTPX_OK;
 }
 
-int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size)
+int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, const chttpx_stream_transport_t* stream_transport, char** output, size_t* output_size)
 {
     if (!server || !headers || !output || !output_size)
     {
@@ -1051,6 +1051,8 @@ int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd,
     chttpx_request_t* req = parse_prefetched_request(server, client_fd, tls_session, headers, header_size, body, body_size, body_stream, content_length);
     if (!req)
         return cHTTPX_ERR_MEMORY;
+    if (stream_transport)
+        req->_stream_transport = *stream_transport;
 
     chttpx_response_t res = {0};
     if (req->_parse_status)
@@ -1066,6 +1068,17 @@ int _chttpx_execute_prefetched(chttpx_serv_t* server, chttpx_socket_t client_fd,
             res = cHTTPX_ResError(cHTTPX_StatusInternalServerError, "request dispatch failed");
     }
 
+    if (res._streaming_response)
+    {
+        if (req->_stream_transport.close)
+            req->_stream_transport.close(req->_stream_transport.context);
+        *output = NULL;
+        *output_size = SIZE_MAX;
+        cHTTPX_ResponseCleanup(&res);
+        free_request_object(req);
+        return cHTTPX_OK;
+    }
+
     int result = build_response_buffer(req, res, output, output_size);
     cHTTPX_ResponseCleanup(&res);
     free_request_object(req);
@@ -1310,6 +1323,7 @@ void cHTTPX_ResponseCleanup(chttpx_response_t* res)
     res->body = NULL;
     res->body_size = 0;
     res->body_ownership = cHTTPX_BODY_BORROWED;
+    res->_streaming_response = false;
 }
 
 chttpx_response_t cHTTPX_ResNoContent(void)
diff --git a/src/cHTTPX_response.h b/src/cHTTPX_response.h
index f67e59b..d5424fe 100644
--- a/src/cHTTPX_response.h
+++ b/src/cHTTPX_response.h
@@ -55,6 +55,9 @@ extern "C"
         /* Set when response compression must be bypassed. */
         bool compression_disabled;
 
+        /* Internal marker for a response already owned by a streaming transport. */
+        bool _streaming_response;
+
         /* Times for logging */
         struct timespec start_ts;
         struct timespec end_ts;
@@ -75,7 +78,7 @@ extern "C"
     int _chttpx_dispatch(struct chttpx_serv* server, chttpx_request_t* req, chttpx_response_t* res);
 
     /* Internal adapter used by the HTTP/2 transport to reuse the public request/handler API. */
-    int _chttpx_execute_prefetched(struct chttpx_serv* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, char** output, size_t* output_size);
+    int _chttpx_execute_prefetched(struct chttpx_serv* server, chttpx_socket_t client_fd, void* tls_session, char* headers, size_t header_size, unsigned char* body, size_t body_size, FILE* body_stream, size_t content_length, const chttpx_stream_transport_t* stream_transport, char** output, size_t* output_size);
 
     /**
      * Create a JSON HTTP response with formatted content.
diff --git a/src/cHTTPX_sse.c b/src/cHTTPX_sse.c
new file mode 100644
index 0000000..1750f80
--- /dev/null
+++ b/src/cHTTPX_sse.c
@@ -0,0 +1,307 @@
+/**
+ * Copyright (c) 2026 netcorelink
+ *
+ * Server-Sent Events helpers.
+ */
+
+#include "cHTTPX_sse.h"
+
+#include "cHTTPX_headers.h"
+#include "cHTTPX_http.h"
+#include "cHTTPX_serv.h"
+
+#include 
+#include 
+#include 
+
+struct chttpx_sse
+{
+    chttpx_request_t* request;
+    chttpx_response_t* response;
+    bool closed;
+};
+
+typedef struct
+{
+    char* data;
+    size_t size;
+    size_t capacity;
+} chttpx_sse_buffer_t;
+
+static int sse_buffer_reserve(chttpx_sse_buffer_t* buffer, size_t extra)
+{
+    if (!buffer || extra > SIZE_MAX - buffer->size)
+        return cHTTPX_ERR_LIMIT;
+
+    size_t required = buffer->size + extra;
+    if (required <= buffer->capacity)
+        return cHTTPX_OK;
+
+    size_t capacity = buffer->capacity ? buffer->capacity : 128;
+    while (capacity < required)
+    {
+        if (capacity > SIZE_MAX / 2)
+        {
+            capacity = required;
+            break;
+        }
+        capacity *= 2;
+    }
+
+    char* resized = realloc(buffer->data, capacity);
+    if (!resized)
+        return cHTTPX_ERR_MEMORY;
+    buffer->data = resized;
+    buffer->capacity = capacity;
+    return cHTTPX_OK;
+}
+
+static int sse_buffer_append(chttpx_sse_buffer_t* buffer, const void* data, size_t size)
+{
+    if (!size)
+        return cHTTPX_OK;
+    int result = sse_buffer_reserve(buffer, size);
+    if (result != cHTTPX_OK)
+        return result;
+    memcpy(buffer->data + buffer->size, data, size);
+    buffer->size += size;
+    return cHTTPX_OK;
+}
+
+static int sse_buffer_text(chttpx_sse_buffer_t* buffer, const char* text)
+{
+    return sse_buffer_append(buffer, text, strlen(text));
+}
+
+static bool sse_single_line(const char* value)
+{
+    if (!value)
+        return true;
+    return strchr(value, '\r') == NULL && strchr(value, '\n') == NULL;
+}
+
+static int sse_append_multiline(chttpx_sse_buffer_t* buffer, const char* prefix, const char* value)
+{
+    const char* cursor = value ? value : "";
+    for (;;)
+    {
+        const char* end = cursor;
+        while (*end && *end != '\r' && *end != '\n')
+            end++;
+
+        int result = sse_buffer_text(buffer, prefix);
+        if (result == cHTTPX_OK)
+            result = sse_buffer_append(buffer, cursor, (size_t)(end - cursor));
+        if (result == cHTTPX_OK)
+            result = sse_buffer_text(buffer, "\n");
+        if (result != cHTTPX_OK)
+            return result;
+
+        if (!*end)
+            break;
+
+        if (*end == '\r' && end[1] == '\n')
+            end++;
+        cursor = end + 1;
+        if (!*cursor)
+        {
+            result = sse_buffer_text(buffer, prefix);
+            if (result == cHTTPX_OK)
+                result = sse_buffer_text(buffer, "\n");
+            return result;
+        }
+    }
+
+    return cHTTPX_OK;
+}
+
+static int sse_write(chttpx_sse_t* sse, const void* data, size_t size)
+{
+    if (!sse || sse->closed || !data || !size)
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+    chttpx_stream_transport_t* transport = &sse->request->_stream_transport;
+    if (!transport->write || !transport->connected || !transport->connected(transport->context))
+        return cHTTPX_ERR_STATE;
+    return transport->write(transport->context, data, size);
+}
+
+static const char* sse_allowed_origin(chttpx_serv_t* server, const char* request_origin)
+{
+    if (!server || !server->cors.enabled || !request_origin)
+        return NULL;
+
+    size_t left = 0;
+    size_t right = server->cors.origins_count;
+    while (left < right)
+    {
+        size_t middle = left + (right - left) / 2;
+        int order = strcmp(server->cors.origins[middle], request_origin);
+        if (order == 0)
+            return server->cors.origins[middle];
+        if (order < 0)
+            left = middle + 1;
+        else
+            right = middle;
+    }
+    return NULL;
+}
+
+static int sse_response_header(chttpx_response_t* response, const char* name, const char* value)
+{
+    for (size_t i = 0; i < response->headers_count; i++)
+    {
+        if (strcasecmp(response->headers[i].name, name) == 0)
+        {
+            snprintf(response->headers[i].value, sizeof(response->headers[i].value), "%s", value);
+            return cHTTPX_OK;
+        }
+    }
+    return cHTTPX_HeaderAdd(response, name, value) == 0 ? cHTTPX_OK : cHTTPX_ERR_LIMIT;
+}
+
+chttpx_sse_t* cHTTPX_SSEOpen(chttpx_request_t* req, chttpx_response_t* res)
+{
+    if (!req || !res)
+        return NULL;
+
+    chttpx_stream_transport_t* transport = &req->_stream_transport;
+    if (!transport->context || !transport->open || !transport->write || !transport->close || !transport->connected)
+        return NULL;
+
+    cHTTPX_ResponseCleanup(res);
+    res->status = cHTTPX_StatusOK;
+    res->content_type = cHTTPX_CTYPE_SSE;
+    res->compression_disabled = true;
+    res->_streaming_response = true;
+
+    if (sse_response_header(res, "Cache-Control", "no-cache") != cHTTPX_OK ||
+        sse_response_header(res, "X-Accel-Buffering", "no") != cHTTPX_OK)
+    {
+        res->_streaming_response = false;
+        return NULL;
+    }
+
+    if (req->request_id[0] && sse_response_header(res, "X-Request-ID", req->request_id) != cHTTPX_OK)
+    {
+        res->_streaming_response = false;
+        return NULL;
+    }
+
+    chttpx_serv_t* server = req->_server;
+    const char* allowed_origin = sse_allowed_origin(server, cHTTPX_HeaderGet(req, "Origin"));
+    if (allowed_origin &&
+        (sse_response_header(res, "Access-Control-Allow-Origin", allowed_origin) != cHTTPX_OK ||
+         sse_response_header(res, "Access-Control-Allow-Methods", server->cors.methods) != cHTTPX_OK ||
+         sse_response_header(res, "Access-Control-Allow-Headers", server->cors.headers) != cHTTPX_OK ||
+         sse_response_header(res, "Access-Control-Allow-Credentials", "true") != cHTTPX_OK))
+    {
+        res->_streaming_response = false;
+        return NULL;
+    }
+
+    chttpx_sse_t* sse = cHTTPX_Alloc(req, sizeof(*sse));
+    if (!sse)
+    {
+        res->_streaming_response = false;
+        return NULL;
+    }
+
+    sse->request = req;
+    sse->response = res;
+    if (transport->open(transport->context, res) != cHTTPX_OK)
+    {
+        res->_streaming_response = false;
+        sse->closed = true;
+        return NULL;
+    }
+    return sse;
+}
+
+int cHTTPX_SSESend(chttpx_sse_t* sse, const char* event, const char* id, const char* data)
+{
+    if (!sse || sse->closed || !sse_single_line(event) || !sse_single_line(id))
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+
+    chttpx_sse_buffer_t buffer = {0};
+    int result = cHTTPX_OK;
+
+    if (event && *event)
+    {
+        result = sse_buffer_text(&buffer, "event: ");
+        if (result == cHTTPX_OK)
+            result = sse_buffer_text(&buffer, event);
+        if (result == cHTTPX_OK)
+            result = sse_buffer_text(&buffer, "\n");
+    }
+
+    if (result == cHTTPX_OK && id)
+    {
+        result = sse_buffer_text(&buffer, "id: ");
+        if (result == cHTTPX_OK)
+            result = sse_buffer_text(&buffer, id);
+        if (result == cHTTPX_OK)
+            result = sse_buffer_text(&buffer, "\n");
+    }
+
+    if (result == cHTTPX_OK)
+        result = sse_append_multiline(&buffer, "data: ", data ? data : "");
+    if (result == cHTTPX_OK)
+        result = sse_buffer_text(&buffer, "\n");
+    if (result == cHTTPX_OK)
+        result = sse_write(sse, buffer.data, buffer.size);
+
+    free(buffer.data);
+    return result;
+}
+
+int cHTTPX_SSERetry(chttpx_sse_t* sse, uint64_t milliseconds)
+{
+    if (!sse || sse->closed)
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+    char buffer[64];
+    int written = snprintf(buffer, sizeof(buffer), "retry: %llu\n\n", (unsigned long long)milliseconds);
+    if (written <= 0 || (size_t)written >= sizeof(buffer))
+        return cHTTPX_ERR_LIMIT;
+    return sse_write(sse, buffer, (size_t)written);
+}
+
+int cHTTPX_SSEComment(chttpx_sse_t* sse, const char* comment)
+{
+    if (!sse || sse->closed)
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+
+    chttpx_sse_buffer_t buffer = {0};
+    int result = sse_append_multiline(&buffer, ": ", comment ? comment : "");
+    if (result == cHTTPX_OK)
+        result = sse_buffer_text(&buffer, "\n");
+    if (result == cHTTPX_OK)
+        result = sse_write(sse, buffer.data, buffer.size);
+    free(buffer.data);
+    return result;
+}
+
+int cHTTPX_SSEHeartbeat(chttpx_sse_t* sse)
+{
+    static const char heartbeat[] = ":\n\n";
+    return sse_write(sse, heartbeat, sizeof(heartbeat) - 1);
+}
+
+bool cHTTPX_SSEConnected(const chttpx_sse_t* sse)
+{
+    if (!sse || sse->closed || !sse->request)
+        return false;
+    const chttpx_stream_transport_t* transport = &sse->request->_stream_transport;
+    return transport->connected && transport->connected(transport->context);
+}
+
+int cHTTPX_SSEClose(chttpx_sse_t* sse)
+{
+    if (!sse)
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+    if (sse->closed)
+        return cHTTPX_OK;
+
+    sse->closed = true;
+    chttpx_stream_transport_t* transport = &sse->request->_stream_transport;
+    return transport->close ? transport->close(transport->context) : cHTTPX_ERR_STATE;
+}
diff --git a/src/cHTTPX_sse.h b/src/cHTTPX_sse.h
new file mode 100644
index 0000000..66126df
--- /dev/null
+++ b/src/cHTTPX_sse.h
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2026 netcorelink
+ *
+ * Server-Sent Events helpers.
+ */
+
+#ifndef CHTTPX_SSE_H
+#define CHTTPX_SSE_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "cHTTPX_request.h"
+#include "cHTTPX_response.h"
+
+    typedef struct chttpx_sse chttpx_sse_t;
+
+    /** Open an HTTP/2 Server-Sent Events stream and send its response headers. */
+    chttpx_sse_t* cHTTPX_SSEOpen(chttpx_request_t* req, chttpx_response_t* res);
+
+    /** Send one SSE event. event and id are optional; data may contain multiple lines. */
+    int cHTTPX_SSESend(chttpx_sse_t* sse, const char* event, const char* id, const char* data);
+
+    /** Send a retry directive in milliseconds. */
+    int cHTTPX_SSERetry(chttpx_sse_t* sse, uint64_t milliseconds);
+
+    /** Send an SSE comment, splitting multiline comments correctly. */
+    int cHTTPX_SSEComment(chttpx_sse_t* sse, const char* comment);
+
+    /** Send an empty SSE comment suitable for keep-alive heartbeats. */
+    int cHTTPX_SSEHeartbeat(chttpx_sse_t* sse);
+
+    /** Return whether the underlying client stream is still writable. */
+    bool cHTTPX_SSEConnected(const chttpx_sse_t* sse);
+
+    /** Gracefully finish the SSE stream. Safe to call more than once. */
+    int cHTTPX_SSEClose(chttpx_sse_t* sse);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/tests/test_server.c b/tests/test_server.c
index cfd69a8..e42357f 100644
--- a/tests/test_server.c
+++ b/tests/test_server.c
@@ -26,6 +26,19 @@ static void request_handler(chttpx_request_t* req, chttpx_response_t* res)
     *res = cHTTPX_ResMessage(cHTTPX_StatusOK, "done");
 }
 
+/** Emits a short SSE stream over the real HTTP/2 transport. */
+static void sse_handler(chttpx_request_t* req, chttpx_response_t* res)
+{
+    chttpx_sse_t* sse = cHTTPX_SSEOpen(req, res);
+    assert(sse);
+    assert(cHTTPX_SSEConnected(sse));
+    assert(cHTTPX_SSERetry(sse, 1500) == cHTTPX_OK);
+    assert(cHTTPX_SSESend(sse, "progress", "1", "first\nline") == cHTTPX_OK);
+    assert(cHTTPX_SSEHeartbeat(sse) == cHTTPX_OK);
+    assert(cHTTPX_SSESend(sse, "progress", "2", "done") == cHTTPX_OK);
+    assert(cHTTPX_SSEClose(sse) == cHTTPX_OK);
+}
+
 /** Captures client_ip and returns 204. */
 static void ip_handler(chttpx_request_t* req, chttpx_response_t* res)
 {
@@ -181,6 +194,13 @@ static void exchange_family(uint16_t port, int family, const char* request, char
     assert(written > 0 && (size_t)written < response_size);
     used = (size_t)written;
 
+    if (result.content_type && *result.content_type)
+    {
+        written = snprintf(response + used, response_size - used, "content-type: %s\r\n", result.content_type);
+        assert(written >= 0 && (size_t)written < response_size - used);
+        used += (size_t)written;
+    }
+
     for (size_t i = 0; i < result.headers_count; i++)
     {
         written = snprintf(response + used, response_size - used, "%s: %s\r\n", result.headers[i].name, result.headers[i].value);
@@ -286,6 +306,7 @@ int main(void)
     chttpx_router_t public_router = cHTTPX_RoutePathPrefix(public_api, "");
     assert(cHTTPX_Post(&public_router, "/body", request_handler));
     assert(cHTTPX_Get(&public_router, "/ip", ip_handler));
+    assert(cHTTPX_Get(&public_router, "/events", sse_handler));
     assert(cHTTPX_Options(&public_router, "/body", options_handler));
     assert(cHTTPX_Get(&public_router, "/empty", empty_handler));
     assert(cHTTPX_Post(&public_router, "/proxy", proxy_handler));
@@ -339,6 +360,18 @@ int main(void)
     assert(strstr(response, "HTTP/2 204 No Content") != NULL);
     assert(strcmp(observed_client_ip, "127.0.0.1") == 0);
 
+    exchange(public_port, "GET /events HTTP/2\r\nHost: localhost\r\nOrigin: https://example.com\r\nX-Request-ID: sse-integration\r\n\r\n", response, sizeof(response));
+    assert(strstr(response, "HTTP/2 200 OK") != NULL);
+    assert(strstr(response, "content-type: text/event-stream") != NULL);
+    assert(strstr(response, "cache-control: no-cache") != NULL);
+    assert(strstr(response, "x-accel-buffering: no") != NULL);
+    assert(strstr(response, "access-control-allow-origin: https://example.com") != NULL);
+    assert(strstr(response, "content-length:") == NULL);
+    assert(strstr(response, "retry: 1500\n\n") != NULL);
+    assert(strstr(response, "event: progress\nid: 1\ndata: first\ndata: line\n\n") != NULL);
+    assert(strstr(response, ":\n\n") != NULL);
+    assert(strstr(response, "event: progress\nid: 2\ndata: done\n\n") != NULL);
+
     exchange(public_port,
              "OPTIONS /body HTTP/2\r\nHost: localhost\r\nOrigin: https://example.com\r\nAccess-Control-Request-Method: POST\r\n\r\n",
              response, sizeof(response));
diff --git a/tests/test_sse.c b/tests/test_sse.c
new file mode 100644
index 0000000..014d7d1
--- /dev/null
+++ b/tests/test_sse.c
@@ -0,0 +1,121 @@
+#include "libchttpx.h"
+
+#include 
+#include 
+#include 
+
+typedef struct
+{
+    char output[4096];
+    size_t size;
+    bool opened;
+    bool closed;
+    bool connected;
+} sse_mock_t;
+
+static const char* response_header(const chttpx_response_t* res, const char* name)
+{
+    for (size_t i = 0; i < res->headers_count; i++)
+        if (strcasecmp(res->headers[i].name, name) == 0)
+            return res->headers[i].value;
+    return NULL;
+}
+
+static int mock_open(void* context, const struct chttpx_response* response)
+{
+    sse_mock_t* mock = context;
+    assert(mock);
+    assert(response);
+    assert(response->status == cHTTPX_StatusOK);
+    assert(strcmp(response->content_type, cHTTPX_CTYPE_SSE) == 0);
+    assert(strcmp(response_header(response, "Cache-Control"), "no-cache") == 0);
+    assert(strcmp(response_header(response, "X-Accel-Buffering"), "no") == 0);
+    assert(response->compression_disabled);
+    mock->opened = true;
+    mock->connected = true;
+    return cHTTPX_OK;
+}
+
+static int mock_write(void* context, const void* data, size_t size)
+{
+    sse_mock_t* mock = context;
+    if (!mock || !mock->connected || !data || size > sizeof(mock->output) - mock->size)
+        return cHTTPX_ERR_IO;
+    memcpy(mock->output + mock->size, data, size);
+    mock->size += size;
+    return cHTTPX_OK;
+}
+
+static int mock_close(void* context)
+{
+    sse_mock_t* mock = context;
+    if (!mock)
+        return cHTTPX_ERR_INVALID_ARGUMENT;
+    mock->closed = true;
+    mock->connected = false;
+    return cHTTPX_OK;
+}
+
+static bool mock_connected(void* context)
+{
+    const sse_mock_t* mock = context;
+    return mock && mock->connected;
+}
+
+int main(void)
+{
+    sse_mock_t mock = {0};
+    chttpx_request_t req = {0};
+    chttpx_response_t res = {0};
+
+    snprintf(req.request_id, sizeof(req.request_id), "%s", "sse-unit-test");
+    req._stream_transport = (chttpx_stream_transport_t){
+        .context = &mock,
+        .open = mock_open,
+        .write = mock_write,
+        .close = mock_close,
+        .connected = mock_connected,
+    };
+
+    chttpx_sse_t* sse = cHTTPX_SSEOpen(&req, &res);
+    assert(sse);
+    assert(mock.opened);
+    assert(cHTTPX_SSEConnected(sse));
+    assert(strcmp(response_header(&res, "X-Request-ID"), "sse-unit-test") == 0);
+
+    assert(cHTTPX_SSESend(sse, "progress", "42", "first\nsecond\r\nthird\r") == cHTTPX_OK);
+    assert(cHTTPX_SSERetry(sse, 5000) == cHTTPX_OK);
+    assert(cHTTPX_SSEComment(sse, "keep\nalive") == cHTTPX_OK);
+    assert(cHTTPX_SSEHeartbeat(sse) == cHTTPX_OK);
+
+    static const char expected[] =
+        "event: progress\n"
+        "id: 42\n"
+        "data: first\n"
+        "data: second\n"
+        "data: third\n"
+        "data: \n"
+        "\n"
+        "retry: 5000\n\n"
+        ": keep\n"
+        ": alive\n"
+        "\n"
+        ":\n\n";
+
+    assert(mock.size == sizeof(expected) - 1);
+    assert(memcmp(mock.output, expected, sizeof(expected) - 1) == 0);
+
+    assert(cHTTPX_SSESend(sse, "bad\nevent", NULL, "data") == cHTTPX_ERR_INVALID_ARGUMENT);
+    assert(cHTTPX_SSESend(sse, NULL, "bad\nid", "data") == cHTTPX_ERR_INVALID_ARGUMENT);
+
+    assert(cHTTPX_SSEClose(sse) == cHTTPX_OK);
+    assert(cHTTPX_SSEClose(sse) == cHTTPX_OK);
+    assert(mock.closed);
+    assert(!cHTTPX_SSEConnected(sse));
+
+    cHTTPX_ResponseCleanup(&res);
+    cHTTPX_RequestCleanup(&req);
+
+    puts("SSE formatting tests passed");
+    return 0;
+}

From 178f2d11006ec4daa122d397f17b0b1ca621d70a Mon Sep 17 00:00:00 2001
From: nenishyi 
Date: Sun, 27 Sep 2026 22:31:13 +0500
Subject: [PATCH 48/48] ci: install nghttp2 for CodeQL build

---
 .github/workflows/codeql.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 168e1e0..00990fe 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -33,7 +33,7 @@ jobs:
       - name: Install dependencies
         run: |
           sudo apt update
-          sudo apt install -y gcc make libcjson-dev zlib1g-dev
+          sudo apt install -y gcc make libcjson-dev zlib1g-dev libnghttp2-dev
 
       - name: Build
         run: make libchttpx.so