From 1573d831dbdfb1dff4226079d0895d044172d190 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sun, 20 Apr 2025 19:07:16 +0200 Subject: [PATCH] First version of chunk encoding --- Makefile | 2 +- examples/server_api.c | 2 +- tests/test.c | 58 +------------- tests/test.h | 3 +- tests/test_chunking.c | 6 ++ tests/test_reuse.c | 27 +++++++ tinyhttp.c | 176 +++++++++++++++++++++++++++++++----------- tinyhttp.h | 1 + 8 files changed, 172 insertions(+), 103 deletions(-) create mode 100644 tests/test_chunking.c diff --git a/Makefile b/Makefile index 3f1d26b..1e4a1a8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ SUFFIX_DEBUG = _debug # ------------------------------------------------------ # -TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/test.c tests/test_reuse.c +TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/test.c tests/test_reuse.c tests/test_chunking.c TEST_HFILES = tinyhttp.h tests/picohttpparser.h TEST_FLAGS = -Wall -Wextra diff --git a/examples/server_api.c b/examples/server_api.c index 3df8dba..9eef09c 100644 --- a/examples/server_api.c +++ b/examples/server_api.c @@ -18,7 +18,7 @@ int main(void) TinyHTTPServerConfig config = { .reuse = 1, - .plain_addr = NULL, + .plain_addr = "127.0.0.1", .plain_port = 8080, .plain_backlog = 32, }; diff --git a/tests/test.c b/tests/test.c index e0deb8a..ad27d06 100644 --- a/tests/test.c +++ b/tests/test.c @@ -9,7 +9,6 @@ // TEST CASES ////////////////////////////////////////////////////////////////////////////////////// -// Plain HTTP 1.1 request string with no Connection header #define BASIC_REQUEST_STRING \ "GET / HTTP/1.1\r\n" \ "Host: 127.0.0.1:8080\r\n" \ @@ -40,31 +39,6 @@ static void test_init(void) TEST_END } -static void test_setreuse(void) -{ - TinyHTTPStream stream; - - TEST_START - tinyhttp_stream_init(&stream, memfunc, NULL); - - TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); - - tinyhttp_stream_setreuse(&stream, 1); - - TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); - - tinyhttp_stream_setreuse(&stream, 0); - - TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); - - tinyhttp_stream_setreuse(&stream, 5847295); - - TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); - - tinyhttp_stream_free(&stream); - TEST_END -} - static void test_kill(void) { TinyHTTPStream stream; @@ -128,15 +102,13 @@ test_send_started_flag(void) TEST_END } -static void test_exchange(int reuse) +static void test_basic_exchange() { TinyHTTPStream stream; TEST_START tinyhttp_stream_init(&stream, memfunc, NULL); - tinyhttp_stream_setreuse(&stream, reuse); - // Send request send_request(&stream, BASIC_REQUEST_STRING); @@ -155,33 +127,10 @@ static void test_exchange(int reuse) TEST(res.status_code == 200); TEST(tinyhttp_streq(res.status_text, TINYHTTP_STRING("OK"))); - if (reuse) { - // If we allowed connection reuse on this stream, we expect - // the connection to be kept alive. The response must therefore - // contain the "Connection: Keep-Alive" header or no "Connection" - // header at all since "Keep-Alive" is the default. - TEST(!header_exists(&res, TINYHTTP_STRING("Connection")) || - header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Keep-Alive"))); - } else { - // If we didn't allow connection reuse, then the response must - // contain the "Connection: Close" header - TEST(header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Close"))); - } - tinyhttp_stream_free(&stream); TEST_END } -/* -KEEP ALIVE TESTS - no connection header in request - invalid connection header in request - keep-alive value in request - close value in request - - -*/ - ////////////////////////////////////////////////////////////////////////////////////// // ENTRY POINT ////////////////////////////////////////////////////////////////////////////////////// @@ -189,13 +138,12 @@ KEEP ALIVE TESTS int main(void) { test_init(); - test_setreuse(); test_kill(); test_recv_started_flag(); test_send_started_flag(); - test_exchange(0); - test_exchange(1); + test_basic_exchange(); test_reuse(); + test_chunking(); printf("OK\n"); return 0; } diff --git a/tests/test.h b/tests/test.h index 2792883..0a9c787 100644 --- a/tests/test.h +++ b/tests/test.h @@ -38,4 +38,5 @@ int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString #define TEST_START2(file, line) printf("Test %s:%d\n", (file), (line)); #define TEST_END -void test_reuse(void); \ No newline at end of file +void test_reuse(void); +void test_chunking(void); \ No newline at end of file diff --git a/tests/test_chunking.c b/tests/test_chunking.c new file mode 100644 index 0000000..a60261e --- /dev/null +++ b/tests/test_chunking.c @@ -0,0 +1,6 @@ +#include "test.h" + +void test_chunking(void) +{ + // TODO +} \ No newline at end of file diff --git a/tests/test_reuse.c b/tests/test_reuse.c index ac09c47..00bf40a 100644 --- a/tests/test_reuse.c +++ b/tests/test_reuse.c @@ -52,6 +52,32 @@ "Connection: Close\r\n" \ "\r\n" +static void test_setreuse(void) +{ + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); + + tinyhttp_stream_setreuse(&stream, 1); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); + + tinyhttp_stream_setreuse(&stream, 0); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); + + tinyhttp_stream_setreuse(&stream, 5847295); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); + + tinyhttp_stream_free(&stream); + TEST_END +} + + typedef enum { DONT_REUSE, ALLOW_REUSE, @@ -139,6 +165,7 @@ void test_reuse(void) // RFC 9112, Section 9.6. (Tear-down) // RFC 9110, Section 7.6.1. (Connection) + test_setreuse(); test_reuse_helper(DONT_REUSE, INCONNHDR_NONE, 1, OUTCONNHDR_CLOSE, 1, __FILE__, __LINE__); test_reuse_helper(DONT_REUSE, INCONNHDR_KEEPALIVE, 1, OUTCONNHDR_CLOSE, 1, __FILE__, __LINE__); test_reuse_helper(DONT_REUSE, INCONNHDR_CLOSE, 1, OUTCONNHDR_CLOSE, 1, __FILE__, __LINE__); diff --git a/tinyhttp.c b/tinyhttp.c index 8b12bea..8d92a91 100644 --- a/tinyhttp.c +++ b/tinyhttp.c @@ -59,7 +59,7 @@ #define ASSERT(X) {if (!(X)) __builtin_trap();} #define COUNTOF(X) (sizeof(X)/sizeof((X)[0])) -#define DUMP_IO 0 +#define DUMP_IO 1 #if TINYHTTP_ROUTER_ENABLE #error "The router interface isn't ready yet" @@ -172,8 +172,26 @@ void tinyhttp_printstate_(int state, const char *file, const char *line) print(")\n", -1); } -static char -to_lower(char c) +static int is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int is_hex_digit(char c) +{ + return (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F'); +} + +static int hex_digit_to_int(char c) +{ + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return c - '0'; +} + +static char to_lower(char c) { if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; @@ -369,47 +387,47 @@ parse_transfer_encoding(const char *src, ptrdiff_t len, int *items, int max) cur++; if (6 < len - cur - && src[cur+0] == 'c' - && src[cur+1] == 'h' - && src[cur+2] == 'u' - && src[cur+3] == 'n' - && src[cur+4] == 'k' - && src[cur+5] == 'e' - && src[cur+6] == 'd') { + && to_lower(src[cur+0]) == 'c' + && to_lower(src[cur+1]) == 'h' + && to_lower(src[cur+2]) == 'u' + && to_lower(src[cur+3]) == 'n' + && to_lower(src[cur+4]) == 'k' + && to_lower(src[cur+5]) == 'e' + && to_lower(src[cur+6]) == 'd') { if (num == max) return -1; items[num++] = TRANSFER_ENCODING_CHUNKED; cur += 7; } else if (7 < len - cur - && src[cur+0] == 'c' - && src[cur+1] == 'o' - && src[cur+2] == 'm' - && src[cur+3] == 'p' - && src[cur+4] == 'r' - && src[cur+5] == 'e' - && src[cur+6] == 's' - && src[cur+7] == 's') { + && to_lower(src[cur+0]) == 'c' + && to_lower(src[cur+1]) == 'o' + && to_lower(src[cur+2]) == 'm' + && to_lower(src[cur+3]) == 'p' + && to_lower(src[cur+4]) == 'r' + && to_lower(src[cur+5]) == 'e' + && to_lower(src[cur+6]) == 's' + && to_lower(src[cur+7]) == 's') { if (num == max) return -1; items[num++] = TRANSFER_ENCODING_COMPRESS; cur += 8; } else if (6 < len - cur - && src[cur+0] == 'd' - && src[cur+1] == 'e' - && src[cur+2] == 'f' - && src[cur+3] == 'l' - && src[cur+4] == 'a' - && src[cur+5] == 't' - && src[cur+6] == 'e') { + && to_lower(src[cur+0]) == 'd' + && to_lower(src[cur+1]) == 'e' + && to_lower(src[cur+2]) == 'f' + && to_lower(src[cur+3]) == 'l' + && to_lower(src[cur+4]) == 'a' + && to_lower(src[cur+5]) == 't' + && to_lower(src[cur+6]) == 'e') { if (num == max) return -1; items[num++] = TRANSFER_ENCODING_DEFLATE; cur += 7; } else if (3 < len - cur - && src[cur+0] == 'g' - && src[cur+1] == 'z' - && src[cur+2] == 'i' - && src[cur+3] == 'p') { + && to_lower(src[cur+0]) == 'g' + && to_lower(src[cur+1]) == 'z' + && to_lower(src[cur+2]) == 'i' + && to_lower(src[cur+3]) == 'p') { if (num == max) return -1; items[num++] = TRANSFER_ENCODING_GZIP; @@ -432,11 +450,6 @@ parse_transfer_encoding(const char *src, ptrdiff_t len, int *items, int max) return num; } -static int is_digit(char c) -{ - return c >= '0' && c <= '9'; -} - static int parse_content_length(const char *src, ptrdiff_t len, unsigned long long *out) { @@ -460,7 +473,8 @@ parse_content_length(const char *src, ptrdiff_t len, unsigned long long *out) } static int -parse_request(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPRequest *req) +parse_request(char *src, ptrdiff_t len, + unsigned long long body_limit, TinyHTTPRequest *req) { ptrdiff_t ret = parse_request_head(src, len, req); if (ret <= 0) @@ -477,12 +491,82 @@ parse_request(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPR if (num < 0) return -400; + typedef struct { + ptrdiff_t offset; + ptrdiff_t length; + } Chunk; + + ptrdiff_t body_length = 0; + Chunk chunks[TINYHTTP_CHUNK_LIMIT]; + int num_chunks = 0; + + ptrdiff_t cur = head_len; for (;;) { - return -501; // TODO: Parse chunks + if (cur == len) + return 0; + + if (!is_hex_digit(src[cur])) + return -400; + + unsigned long long chunk_length = 0; + do { + int d = hex_digit_to_int(src[cur++]); + if (chunk_length > (MAX_U64 - d) / 16) + return -400; + chunk_length = chunk_length * 16 + d; + } while (cur < len && is_hex_digit(src[cur])); + + if (chunk_length > body_limit - body_length) { + // TODO + } + body_length += chunk_length; + + if (cur == len) + return 0; + if (src[cur] != '\r') + return -400; + cur++; + + if (cur == len) + return 0; + if (src[cur] != '\n') + return -400; + cur++; + + ptrdiff_t chunk_offset = cur; + cur += chunk_length; // TODO: Check overflow + + if (cur >= len) + return 0; + if (src[cur] != '\r') + return -400; + cur++; + + if (cur == len) + return 0; + if (src[cur] != '\n') + return -400; + cur++; + + if (chunk_length == 0) + break; + + if (num_chunks == TINYHTTP_CHUNK_LIMIT) + return -500; + chunks[num_chunks++] = (Chunk) { chunk_offset, chunk_length }; } - return 1; + // Pack all of the chunks tightly after the head + ptrdiff_t next = head_len; + for (int i = 0; i < num_chunks; i++) { + memmove(src + next, src + chunks[i].offset, chunks[i].length); + next += chunks[i].length; + } + + req->body = src + head_len; + req->body_len = body_length; + return cur; } int content_length_index = find_header(req, TINYHTTP_STRING("Content-Length")); @@ -1502,7 +1586,7 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) #if DUMP_IO ptrdiff_t ressize = (byte_queue_offset(&stream->out) - stream->out.lock); - print_bytes("R << ", stream->out.data + stream->out.head, ressize); + tinyhttp_printbytes("R << ", stream->out.data + stream->out.head, ressize); #endif byte_queue_read_ack(&stream->in, stream->reqsize); @@ -1837,7 +1921,9 @@ process_network_events(TinyHTTPServer *server, int timeout) if (flags & (EPOLLERR | EPOLLHUP)) tinyhttp_stream_kill(stream); - int state = tinyhttp_stream_state(stream); + int old_state = tinyhttp_stream_state(stream); + int state = old_state; + if (flags & EPOLLIN) { while (state & TINYHTTP_STREAM_RECV) { ptrdiff_t cap; @@ -1856,7 +1942,7 @@ process_network_events(TinyHTTPServer *server, int timeout) break; } #if DUMP_IO - print_bytes("N >> ", dst, ret); + tinyhttp_printbytes("N >> ", dst, ret); #endif tinyhttp_stream_recv_ack(stream, ret); state = tinyhttp_stream_state(stream); @@ -1882,7 +1968,7 @@ process_network_events(TinyHTTPServer *server, int timeout) break; } #if DUMP_IO - print_bytes("N << ", src, ret); + tinyhttp_printbytes("N << ", src, ret); #endif tinyhttp_stream_send_ack(stream, ret); state = tinyhttp_stream_state(stream); @@ -1891,7 +1977,7 @@ process_network_events(TinyHTTPServer *server, int timeout) } int new_state = tinyhttp_stream_state(&server->stream_state[idx]); - if ((state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND)) != (new_state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND))) { + if ((old_state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND)) != (new_state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND))) { struct epoll_event tmp; tmp.data.fd = idx; tmp.events = 0; @@ -1903,7 +1989,7 @@ process_network_events(TinyHTTPServer *server, int timeout) } } - if ((new_state & TINYHTTP_STREAM_READY) && !(state & TINYHTTP_STREAM_READY)) { + if ((new_state & TINYHTTP_STREAM_READY) && !(old_state & TINYHTTP_STREAM_READY)) { int ready_idx = (server->ready_head + server->ready_count) % TINYHTTP_SERVER_CONN_LIMIT; server->ready_queue[ready_idx] = idx; server->ready_count++; @@ -2228,7 +2314,7 @@ process_network_events(TinyHTTPServer *server, int timeout) if (recv_overlapped == overlapped) { #if DUMP_IO printf("RECV COMPLETED (num=%ld)\n", transferred); - print_bytes("N >> ", stream->in.data + stream->in.head, transferred); + tinyhttp_printbytes("N >> ", stream->in.data + stream->in.head, transferred); #endif tinyhttp_stream_recv_ack(stream, transferred); if (transferred == 0) @@ -2237,7 +2323,7 @@ process_network_events(TinyHTTPServer *server, int timeout) ASSERT(send_overlapped == overlapped); #if DUMP_IO printf("SEND COMPLETED (num=%ld)\n", transferred); - print_bytes("N << ", stream->out.data + stream->out.head, transferred); + tinyhttp_printbytes("N << ", stream->out.data + stream->out.head, transferred); #endif tinyhttp_stream_send_ack(stream, transferred); } diff --git a/tinyhttp.h b/tinyhttp.h index 42e8641..5a52893 100644 --- a/tinyhttp.h +++ b/tinyhttp.h @@ -37,6 +37,7 @@ #define TINYHTTP_HEADER_LIMIT 32 #define TINYHTTP_SERVER_CONN_LIMIT (1<<10) #define TINYHTTP_SERVER_EPOLL_BATCH_SIZE (1<<10) +#define TINYHTTP_CHUNK_LIMIT (1<<10) #endif #define TINYHTTP_LINESTR_HELPER1(X) #X