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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ if (NOT TH_DISABLE_TESTS)
--filter ${CMAKE_CURRENT_SOURCE_DIR}/src/
--exclude .*_test\\.c$$
--exclude .*_bench\\.c$$
--exclude .*/th_bench\\.h$$
${TH_COVERAGE_GPERF_EXCLUDES}
--object-directory ${CMAKE_CURRENT_BINARY_DIR}
--merge-mode-functions separate
Expand Down
20 changes: 13 additions & 7 deletions src/th_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ th_http_complete(th_http* http)
TH_LOCAL(void)
th_http_write_response(th_http* http)
{
th_response_async_write(&http->response, http->conn, th_http_handle_write_response, http);
th_response_write_plan plan;
th_err err = th_response_prepare_write(&http->response, &plan);
if (err != TH_ERR_OK) {
th_http_handle_write_response(http, 0, err);
return;
}
th_conn_send(http->conn, plan.iov, plan.iovcnt, plan.file, plan.offset, plan.len, th_http_handle_write_response, http);
}

TH_LOCAL(void)
Expand Down Expand Up @@ -222,11 +228,6 @@ th_http_handle_read_request(void* user_data, size_t len, th_err err)
th_conn_recv(http->conn, th_buf_vec_at(&http->buf, http->read_bytes),
th_buf_vec_size(&http->buf) - http->read_bytes, false, th_http_handle_read_request, http);
} else {
if (th_conn_tracker_count(http->tracker) > TH_CONFIG_MAX_CONNECTIONS) {
TH_LOG_WARN("Too many connections, rejecting new connection");
th_http_write_error_response(http, TH_ERR_HTTP(TH_CODE_SERVICE_UNAVAILABLE));
return;
}
size_t content_received = http->read_bytes - http->parsed_bytes;
size_t content_len = th_request_parser_content_len(&http->parser);
if (content_len > TH_MAX_BODY_LEN) {
Expand All @@ -236,7 +237,7 @@ th_http_handle_read_request(void* user_data, size_t len, th_err err)
}
size_t remaining = content_len - content_received;
if (http->read_bytes + remaining > th_buf_vec_size(&http->buf)) {
memcpy(th_buf_vec_at(&http->buf, 0), th_buf_vec_at(&http->buf, http->parsed_bytes), content_received);
memmove(th_buf_vec_at(&http->buf, 0), th_buf_vec_at(&http->buf, http->parsed_bytes), content_received);
http->read_bytes = content_received;
http->parsed_bytes = 0;
if (content_len > th_buf_vec_size(&http->buf)) {
Expand Down Expand Up @@ -314,6 +315,11 @@ th_http_upgrader_upgrade(void* self, th_conn* conn)
th_conn_destroy(conn);
return;
}
if (th_conn_tracker_count(upgrader->tracker) > TH_CONFIG_MAX_CONNECTIONS) {
TH_LOG_WARN("Too many connections, rejecting new connection");
th_http_handle_error(http, TH_ERR_HTTP(TH_CODE_SERVICE_UNAVAILABLE));
return;
}
th_http_start(http);
}

Expand Down
92 changes: 92 additions & 0 deletions src/th_http_test.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "th_conn_tracker.h"
#include "th_fmt.h"
#include "th_http.h"
#include "th_test.h"
#include "th_utility.h"
Expand Down Expand Up @@ -289,6 +290,97 @@ TH_TEST_BEGIN(http)
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_rejects_header_too_large)
{
// Header never terminates and keeps growing past
// TH_CONFIG_LARGE_HEADER_LEN, so it's rejected outright rather
// than resized indefinitely.
char request[TH_CONFIG_LARGE_HEADER_LEN + 256];
size_t pos = 0;
pos += th_fmt_str_append(request, pos, sizeof(request), "GET /test HTTP/1.1\r\n");
while (pos + 32 < sizeof(request)) {
pos += th_fmt_str_append(request, pos, sizeof(request), "X-Pad: aaaaaaaaaaaaaaaaaaaaaaaa\r\n");
}
th_fake_conn_set_request(&conn, th_str_make(request, pos));

th_conn_upgrader_upgrade(&upgrader.base, &conn.base);
while (!conn.destroyed && conn.callback != NULL)
th_fake_conn_run(&conn);

TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 431 Request Header Fields Too Large\r\n"));
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_rejects_body_too_large)
{
char request[256];
size_t pos = 0;
pos += th_fmt_str_append(request, pos, sizeof(request), "POST /test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nContent-Length: ");
char content_len[32];
pos += th_fmt_str_append(request, pos, sizeof(request), th_fmt_uint_to_str(content_len, sizeof(content_len), TH_MAX_BODY_LEN + 1));
pos += th_fmt_str_append(request, pos, sizeof(request), "\r\n\r\n");
th_fake_conn_set_request(&conn, th_str_make(request, pos));

th_conn_upgrader_upgrade(&upgrader.base, &conn.base);
while (!conn.destroyed && conn.callback != NULL)
th_fake_conn_run(&conn);

TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 413 Payload Too Large\r\n"));
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_accepts_large_body_growing_internal_buffer)
{
// Body alone is bigger than TH_CONFIG_SMALL_HEADER_LEN (the
// initial buffer size), but well within TH_MAX_BODY_LEN, so the
// request is accepted and th_buf_vec_resize's growth path runs.
size_t body_len = TH_CONFIG_SMALL_HEADER_LEN + 1000;
char request[TH_CONFIG_SMALL_HEADER_LEN + 1200];
size_t pos = 0;
pos += th_fmt_str_append(request, pos, sizeof(request), "POST /test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nContent-Length: ");
char content_len[32];
pos += th_fmt_str_append(request, pos, sizeof(request), th_fmt_uint_to_str(content_len, sizeof(content_len), (unsigned int)body_len));
pos += th_fmt_str_append(request, pos, sizeof(request), "\r\n\r\n");
for (size_t i = 0; i < body_len; ++i)
request[pos++] = 'a';
th_fake_conn_set_request(&conn, th_str_make(request, pos));

th_conn_upgrader_upgrade(&upgrader.base, &conn.base);
while (!conn.destroyed && conn.callback != NULL)
th_fake_conn_run(&conn);

TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 200 OK\r\n"));
TH_EXPECT(th_buf_ends_with(conn.written, conn.written_len, "Hello, World!"));
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_rejects_too_many_connections)
{
// Rejected outright at upgrade time, before any request is read.
tracker.count = TH_CONFIG_MAX_CONNECTIONS + 1;

th_conn_upgrader_upgrade(&upgrader.base, &conn.base);
while (!conn.destroyed && conn.callback != NULL)
th_fake_conn_run(&conn);

TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 503 Service Unavailable\r\n"));
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_head_request_writes_headers_without_body)
{
th_fake_conn_set_request(&conn, TH_STR("HEAD /test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"));

th_conn_upgrader_upgrade(&upgrader.base, &conn.base);
while (!conn.destroyed && conn.callback != NULL)
th_fake_conn_run(&conn);

TH_EXPECT(th_buf_starts_with(conn.written, conn.written_len, "HTTP/1.1 200 OK\r\n"));
TH_EXPECT(th_buf_has_header(conn.written, conn.written_len, "Content-Length", "13")); // matches GET's body length
TH_EXPECT(!th_buf_ends_with(conn.written, conn.written_len, "Hello, World!")); // but body itself is omitted
TH_EXPECT(conn.destroyed);
}
TH_TEST_CASE_END
TH_TEST_CASE_BEGIN(http_handles_options_for_known_route)
{
th_fake_conn_set_request(&conn, TH_STR("OPTIONS /test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"));
Expand Down
26 changes: 14 additions & 12 deletions src/th_response.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ th_response_set_body_va(th_response* response, const char* fmt, va_list args)
}
} else {
th_string_resize(&response->body, (size_t)len, ' ');
vsnprintf(th_string_at(&response->body, 0), (size_t)len, fmt, args);
vsnprintf(th_string_at(&response->body, 0), (size_t)len + 1, fmt, args);
}
response->is_file = 0;
return TH_ERR_OK;
Expand Down Expand Up @@ -239,33 +239,35 @@ th_response_set_default_headers(th_response* response)
return TH_ERR_OK;
}

TH_PRIVATE(void)
th_response_async_write(th_response* response, th_conn* conn, th_send_cb callback, void* user_data)
TH_PRIVATE(th_err)
th_response_prepare_write(th_response* response, th_response_write_plan* plan)
{
th_err err = TH_ERR_OK;
size_t iovcnt = 2; // start line + headers
if (response->is_file) {
response->file_len = response->fcache_entry->stream.size;
}
if ((err = th_response_set_default_headers(response)) != TH_ERR_OK)
goto cleanup;
return err;
if ((err = th_response_finalize_headers(response)) != TH_ERR_OK)
goto cleanup;
return err;
if (!response->only_headers && response->is_file == 0 && th_string_len(&response->body) > 0) {
response->iov[iovcnt].base = (void*)th_string_data(&response->body);
response->iov[iovcnt].len = th_string_len(&response->body);
iovcnt++;
}
plan->iov = response->iov;
plan->iovcnt = iovcnt;
if (!response->only_headers && response->is_file != 0) {
th_conn_send(conn, response->iov, iovcnt, &response->fcache_entry->stream, 0, (size_t)response->file_len, callback, user_data);
plan->file = &response->fcache_entry->stream;
plan->offset = 0;
plan->len = (size_t)response->file_len;
} else {
th_conn_send(conn, response->iov, iovcnt, NULL, 0, 0, callback, user_data);
plan->file = NULL;
plan->offset = 0;
plan->len = 0;
}
return;
cleanup:
// Header formatting failed before any I/O was attempted (out of
// memory); safe to call back synchronously since no op is pending.
callback(user_data, 0, err);
return TH_ERR_OK;
}

/* Public response API begin */
Expand Down
25 changes: 22 additions & 3 deletions src/th_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

#include "th_allocator.h"
#include "th_config.h"
#include "th_conn.h"
#include "th_dir_mgr.h"
#include "th_fcache.h"
#include "th_file.h"
#include "th_header_id.h"
#include "th_iov.h"
#include "th_string.h"
/* th_response begin */

Expand Down Expand Up @@ -53,7 +54,25 @@ th_response_deinit(th_response* response);

/* th_response end */

TH_PRIVATE(void)
th_response_async_write(th_response* response, th_conn* conn, th_send_cb callback, void* user_data);
/** th_response_write_plan
* @brief What to send for a response: iov always (start line + headers,
* plus body if any), file/offset/len additionally if a file is being
* sent (file is NULL otherwise).
*/
typedef struct th_response_write_plan {
th_iov* iov;
size_t iovcnt;
th_file* file;
size_t offset;
size_t len;
} th_response_write_plan;

/** th_response_prepare_write
* @brief Finalizes headers (default headers, start line) and fills out
* plan with what to send. Does no I/O - the caller sends plan itself
* (e.g. via th_conn_send).
*/
TH_PRIVATE(th_err)
th_response_prepare_write(th_response* response, th_response_write_plan* plan);

#endif
Loading
Loading