From a86ded503513c850c9970e0261f088b33113326d Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sun, 20 Apr 2025 18:01:41 +0200 Subject: [PATCH] Add tests for connection reuse --- Makefile | 2 +- tests/{main.c => test.c} | 101 +++++++++---------------- tests/test.h | 41 ++++++++++ tests/test_reuse.c | 158 +++++++++++++++++++++++++++++++++++++++ tinyhttp.c | 30 +++++--- 5 files changed, 254 insertions(+), 78 deletions(-) rename tests/{main.c => test.c} (84%) create mode 100644 tests/test.h create mode 100644 tests/test_reuse.c diff --git a/Makefile b/Makefile index 97c31f7..3f1d26b 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ SUFFIX_DEBUG = _debug # ------------------------------------------------------ # -TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/main.c +TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/test.c tests/test_reuse.c TEST_HFILES = tinyhttp.h tests/picohttpparser.h TEST_FLAGS = -Wall -Wextra diff --git a/tests/main.c b/tests/test.c similarity index 84% rename from tests/main.c rename to tests/test.c index 7ceef15..e0deb8a 100644 --- a/tests/main.c +++ b/tests/test.c @@ -2,47 +2,8 @@ #include #include +#include "test.h" #include "picohttpparser.h" -#include "../tinyhttp.h" - -////////////////////////////////////////////////////////////////////////////////////// -// TYPES, MACROS, PROTOTYPES -////////////////////////////////////////////////////////////////////////////////////// - -typedef struct { - - int minor; - - int status_code; - TinyHTTPString status_text; - - int num_headers; - TinyHTTPHeader headers[TINYHTTP_HEADER_LIMIT]; - - char *body; - int body_len; -} Response; - -// Memory function used to initialize TinyHTTPStream -static void *memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data); - -// Moves the request "str" into the stream, checks that the stream -// became ready and that it parsed the request correctly. When this -// functions returns the stream is ready for a response. -static void send_request(TinyHTTPStream *stream, const char *str); - -// Copies into the "dst" buffer the output bytes from the stream -// (up to "cap" bytes) and parses them as an HTTP response into -// "res". -static void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap); - -static int header_exists(Response *res, TinyHTTPString name); -static int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value); - -#define TEST(X) {if (!(X)) { printf("Test failed at %s:%d\n", __FILE__, __LINE__); fflush(stdout); __builtin_trap(); }} - -#define TEST_START printf("Test %s:%d\n", __FILE__, __LINE__); -#define TEST_END ////////////////////////////////////////////////////////////////////////////////////// // TEST CASES @@ -104,6 +65,23 @@ static void test_setreuse(void) TEST_END } +static void test_kill(void) +{ + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED)); + + tinyhttp_stream_kill(&stream); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED); + + tinyhttp_stream_free(&stream); + TEST_END +} + static void test_recv_started_flag(void) { @@ -127,23 +105,6 @@ test_recv_started_flag(void) TEST_END } -static void test_kill(void) -{ - TinyHTTPStream stream; - - TEST_START - tinyhttp_stream_init(&stream, memfunc, NULL); - - TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED)); - - tinyhttp_stream_kill(&stream); - - TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED); - - tinyhttp_stream_free(&stream); - TEST_END -} - static void test_send_started_flag(void) { @@ -211,6 +172,16 @@ static void test_exchange(int reuse) 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 ////////////////////////////////////////////////////////////////////////////////////// @@ -224,6 +195,7 @@ int main(void) test_send_started_flag(); test_exchange(0); test_exchange(1); + test_reuse(); printf("OK\n"); return 0; } @@ -232,8 +204,7 @@ int main(void) // Helper Functions ////////////////////////////////////////////////////////////////////////////////////// -static void* -memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data) +void *memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data) { (void) data; switch (tag) { @@ -426,8 +397,7 @@ parse_response(TinyHTTPString txt, Response *res) res->body_len = 0; // TODO } -static void -send_request(TinyHTTPStream *stream, const char *str) +void send_request(TinyHTTPStream *stream, const char *str) { int received = buffer_into_stream(stream, str, strlen(str)); TEST(received == (int) strlen(str)); @@ -438,8 +408,7 @@ send_request(TinyHTTPStream *stream, const char *str) expect_request(stream, req); } -static void -recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap) +void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap) { int len = stream_into_buffer(stream, dst, cap); @@ -449,8 +418,7 @@ recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap) parse_response((TinyHTTPString) { dst, len }, res); } -static int -header_exists(Response *res, TinyHTTPString name) +int header_exists(Response *res, TinyHTTPString name) { for (int i = 0; i < res->num_headers; i++) if (tinyhttp_streqcase(res->headers[i].name, name)) @@ -458,8 +426,7 @@ header_exists(Response *res, TinyHTTPString name) return 0; } -static int -header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value) +int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value) { for (int i = 0; i < res->num_headers; i++) if (tinyhttp_streqcase(res->headers[i].name, name)) diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..2792883 --- /dev/null +++ b/tests/test.h @@ -0,0 +1,41 @@ +#include +#include "../tinyhttp.h" + +typedef struct { + + int minor; + + int status_code; + TinyHTTPString status_text; + + int num_headers; + TinyHTTPHeader headers[TINYHTTP_HEADER_LIMIT]; + + char *body; + int body_len; +} Response; + +// Memory function used to initialize TinyHTTPStream +// TODO: Maybe choose a better name for this +void *memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data); + +// Moves the request "str" into the stream, checks that the stream +// became ready and that it parsed the request correctly. When this +// functions returns the stream is ready for a response. +void send_request(TinyHTTPStream *stream, const char *str); + +// Copies into the "dst" buffer the output bytes from the stream +// (up to "cap" bytes) and parses them as an HTTP response into +// "res". +void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap); + +int header_exists(Response *res, TinyHTTPString name); +int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value); + +#define TEST(X) {if (!(X)) { printf("Test failed at %s:%d\n", __FILE__, __LINE__); fflush(stdout); __builtin_trap(); }} + +#define TEST_START printf("Test %s:%d\n", __FILE__, __LINE__); +#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 diff --git a/tests/test_reuse.c b/tests/test_reuse.c new file mode 100644 index 0000000..ac09c47 --- /dev/null +++ b/tests/test_reuse.c @@ -0,0 +1,158 @@ +#include "test.h" + +// This file tests the behavior of the "Connection" header + +// HTTP/1.1, No "Connection" header +#define REQUEST_HTTP1_1_NO_CONNECTION_HEADER \ + "GET / HTTP/1.1\r\n" \ + "Host: 127.0.0.1:8080\r\n" \ + "\r\n" + +// HTTP/1.1, "Connection" header with invalid value +#define REQUEST_HTTP1_1_CONNECTION_INVALID \ + "GET / HTTP/1.1\r\n" \ + "Host: 127.0.0.1:8080\r\n" \ + "Connection: zzz\r\n" \ + "\r\n" + +// HTTP/1.1, "Connection: Keep-Alive" header +#define REQUEST_HTTP1_1_CONNECTION_KEEPALIVE \ + "GET / HTTP/1.1\r\n" \ + "Host: 127.0.0.1:8080\r\n" \ + "Connection: Keep-Alive\r\n" \ + "\r\n" + +// HTTP/1.1, "Connection: Close" header +#define REQUEST_HTTP1_1_CONNECTION_CLOSE \ + "GET / HTTP/1.1\r\n" \ + "Host: 127.0.0.1:8080\r\n" \ + "Connection: Close\r\n" \ + "\r\n" + +// HTTP/1.0, No "Connection" header +#define REQUEST_HTTP1_0_NO_CONNECTION_HEADER \ + "GET / HTTP/1.0\r\n" \ + "\r\n" + +// HTTP/1.0, "Connection" header with invalid value +#define REQUEST_HTTP1_0_CONNECTION_INVALID \ + "GET / HTTP/1.0\r\n" \ + "Connection: zzz\r\n" \ + "\r\n" + +// HTTP/1.0, "Connection: Keep-Alive" header +#define REQUEST_HTTP1_0_CONNECTION_KEEPALIVE \ + "GET / HTTP/1.0\r\n" \ + "Connection: Keep-Alive\r\n" \ + "\r\n" + +// HTTP/1.0, "Connection: Close" header +#define REQUEST_HTTP1_0_CONNECTION_CLOSE \ + "GET / HTTP/1.0\r\n" \ + "Connection: Close\r\n" \ + "\r\n" + +typedef enum { + DONT_REUSE, + ALLOW_REUSE, +} ServerReuse; + +typedef enum { + INCONNHDR_NONE, + INCONNHDR_KEEPALIVE, + INCONNHDR_CLOSE, + INCONNHDR_INVALID, +} InputConnectionHeader; + +typedef enum { + OUTCONNHDR_MISSING_OR_KEEPALIVE, + OUTCONNHDR_CLOSE, +} OutputConnectionHeader; + +static void test_reuse_helper( + ServerReuse server_reuse, + InputConnectionHeader input_conn_header, + int input_http_minor_version, + OutputConnectionHeader expect_output_conn_header, + int expect_output_http_minor_version, + const char *file, int line) +{ + TinyHTTPStream stream; + + TEST_START2(file, line) + tinyhttp_stream_init(&stream, memfunc, NULL); + tinyhttp_stream_setreuse(&stream, server_reuse); + + if (input_http_minor_version == 1) { + switch (input_conn_header) { + case INCONNHDR_NONE : send_request(&stream, REQUEST_HTTP1_1_NO_CONNECTION_HEADER); break; + case INCONNHDR_KEEPALIVE: send_request(&stream, REQUEST_HTTP1_1_CONNECTION_KEEPALIVE); break; + case INCONNHDR_CLOSE : send_request(&stream, REQUEST_HTTP1_1_CONNECTION_CLOSE); break; + case INCONNHDR_INVALID : send_request(&stream, REQUEST_HTTP1_1_CONNECTION_INVALID); break; + } + } else { + switch (input_conn_header) { + case INCONNHDR_NONE : send_request(&stream, REQUEST_HTTP1_0_NO_CONNECTION_HEADER); break; + case INCONNHDR_KEEPALIVE: send_request(&stream, REQUEST_HTTP1_0_CONNECTION_KEEPALIVE); break; + case INCONNHDR_CLOSE : send_request(&stream, REQUEST_HTTP1_0_CONNECTION_CLOSE); break; + case INCONNHDR_INVALID : send_request(&stream, REQUEST_HTTP1_0_CONNECTION_INVALID); break; + } + } + + // Build a dummy response + tinyhttp_stream_response_status(&stream, 200); + tinyhttp_stream_response_send(&stream); + + Response res; + char buf[1<<10]; + recv_response(&stream, &res, buf, sizeof(buf)); + + TEST(res.minor == expect_output_http_minor_version); + + int state = tinyhttp_stream_state(&stream); + switch (expect_output_conn_header) { + + case OUTCONNHDR_MISSING_OR_KEEPALIVE: + { + TEST(!header_exists(&res, TINYHTTP_STRING("Connection")) + || header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Keep-Alive"))); + TEST((state & TINYHTTP_STREAM_DIED) == 0); + } + break; + + case OUTCONNHDR_CLOSE: + { + TEST(header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Close"))); + TEST(state & TINYHTTP_STREAM_DIED); + } + break; + } + + tinyhttp_stream_free(&stream); + TEST_END +} + +void test_reuse(void) +{ + // Relevant specs: + // RFC 9112, Section 9.3. (Persistence) + // RFC 9112, Section 9.6. (Tear-down) + // RFC 9110, Section 7.6.1. (Connection) + + 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__); + test_reuse_helper(DONT_REUSE, INCONNHDR_INVALID, 1, OUTCONNHDR_CLOSE, 1, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_NONE, 1, OUTCONNHDR_MISSING_OR_KEEPALIVE, 1, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_KEEPALIVE, 1, OUTCONNHDR_MISSING_OR_KEEPALIVE, 1, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_CLOSE, 1, OUTCONNHDR_CLOSE, 1, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_INVALID, 1, OUTCONNHDR_MISSING_OR_KEEPALIVE, 1, __FILE__, __LINE__); + test_reuse_helper(DONT_REUSE, INCONNHDR_NONE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(DONT_REUSE, INCONNHDR_KEEPALIVE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(DONT_REUSE, INCONNHDR_CLOSE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(DONT_REUSE, INCONNHDR_INVALID, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_NONE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_KEEPALIVE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_CLOSE, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); + test_reuse_helper(ALLOW_REUSE, INCONNHDR_INVALID, 0, OUTCONNHDR_CLOSE, 0, __FILE__, __LINE__); +} diff --git a/tinyhttp.c b/tinyhttp.c index 5be1f32..8b12bea 100644 --- a/tinyhttp.c +++ b/tinyhttp.c @@ -1108,12 +1108,20 @@ should_keep_alive(TinyHTTPStream *stream) if ((stream->state & TINYHTTP_STREAM_REUSE) == 0) return 0; - // If the client is using HTTP/1.0, we can't - // keep alive. - if (stream->req.minor == 0) + if (stream->numexch >= 100) // TODO: Make this a parameter return 0; - if (stream->numexch >= 100) // TODO: Make this a parameter + TinyHTTPRequest *req = &stream->req; + + // If the client is using HTTP/1.0, we can't + // keep alive. + if (req->minor == 0) + return 0; + + // TODO: This assumes "Connection" can only hold a single token, + // but this is not true. + int i = find_header(req, TINYHTTP_STRING("Connection")); + if (i >= 0 && tinyhttp_streqcase(req->headers[i].value, TINYHTTP_STRING("Close"))) return 0; return 1; @@ -1271,7 +1279,8 @@ void tinyhttp_stream_response_status(TinyHTTPStream *stream, int status) return; } - byte_queue_write_fmt(&stream->out, "HTTP/1.1 %d %s\r\n", status, get_status_text(status)); + byte_queue_write_fmt(&stream->out, "HTTP/1.%d %d %s\r\n", + stream->req.minor, status, get_status_text(status)); stream->output_state = TINYHTTP_OUTPUT_STATE_HEADER; } @@ -1306,10 +1315,8 @@ append_special_headers(TinyHTTPStream *stream) { if (stream->keepalive) byte_queue_write(&stream->out, "Connection: Keep-Alive\r\n"); - else { - if (stream->req.minor > 0) - byte_queue_write(&stream->out, "Connection: Close\r\n"); - } + else + byte_queue_write(&stream->out, "Connection: Close\r\n"); if (stream->chunked) byte_queue_write(&stream->out, "Transfer-Encoding: Chunked\r\n"); @@ -1502,7 +1509,10 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) byte_queue_read_unlock(&stream->out); stream->reqsize = 0; - process_next_request(stream); + if (stream->keepalive) + process_next_request(stream); + else + stream->state |= TINYHTTP_STREAM_CLOSE; } // See tinyhttp.h