Add tests for connection reuse

This commit is contained in:
2025-04-20 18:01:41 +02:00
parent 3a4f4b5351
commit a86ded5035
5 changed files with 254 additions and 78 deletions
+1 -1
View File
@@ -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
+34 -67
View File
@@ -2,47 +2,8 @@
#include <string.h>
#include <stdlib.h>
#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))
+41
View File
@@ -0,0 +1,41 @@
#include <stdio.h>
#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);
+158
View File
@@ -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__);
}
+20 -10
View File
@@ -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