From c7db3ae1f907a10748e608cd69899c7586bfc210 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 21 Jul 2026 14:57:44 -0400 Subject: [PATCH] test: cover HTTPRequest state machine --- src/test/httpserver_tests.cpp | 337 ++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 9916e84ad7a..15a7872f799 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -486,6 +486,343 @@ BOOST_AUTO_TEST_CASE(http_request_tests) } } +BOOST_AUTO_TEST_CASE(http_request_state_tests) +{ + // For these tests we just need a receive buffer for the requests to read from. + class DummyClient : public HTTPRemoteClient + { + public: + DummyClient() : HTTPRemoteClient{/*id=*/0, /*addr=*/CService(), /*socket=*/CreateSock(0, 0, 0)} {} + + void receive(std::string_view s) + { + m_recv_buffer.insert( + m_recv_buffer.end(), + s.begin(), + s.end()); + } + }; + + { + // Step through state machine + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST / HTTP/1.0\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders); + + client->receive("Host: 127.0.0.1\n" + "Content-Length: 10\n\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + client->receive("I miss you\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + + // m_req holds a shared_ptr back to the client, so break the cycle + // before the client goes out of scope (as the server does on disconnect). + client->ReleaseRequest(); + } + { + // Read body over multiple data pushes, multiple requests in same push + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST / HTTP/1.0\n" + "Host: 127.0.0.1\n" + "Content-Length: 10\n\n" + "I miss"); + client->ReadRequest(*client->m_req); + // Because of the Content-Length header we know the body is not complete + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Finish sending first request and include second request in the same buffer + client->receive(" you" + "GET /endpoint HTTP/1.0\n\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + BOOST_CHECK_EQUAL(client->m_req->m_body, "I miss you"); + // Next request sitting in buffer + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24); + // Complete first request hasn't been moved yet, expect no-op + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24); + + // Reset m_req + client->m_req = std::make_unique(client); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + // Read second request + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + BOOST_CHECK_EQUAL(client->m_req->m_target, "/endpoint"); + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0); + // Buffer is cleared + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0); + + client->ReleaseRequest(); + } + { + // A Content-Length body is drained out of the receive buffer as it + // arrives, instead of accumulating there until the request is complete. + + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST / HTTP/1.0\n" + "Content-Length: 30000\n\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Body arrives in 10kB pieces. Each one is copied onto m_body and + // erased from the receive buffer, which never holds more than one piece. + for (int i = 1; i <= 3; ++i) { + client->receive(std::string(10000, 'x')); + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 10000); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 10000 * i); + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0); + } + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + + client->ReleaseRequest(); + } + { + // A body sent in the same push as the next request is split correctly + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST / HTTP/1.0\n" + "Content-Length: 4\n\n" + "body" + "GET /next HTTP/1.0\n\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + BOOST_CHECK_EQUAL(client->m_req->m_body, "body"); + // Only the second request is left over + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 20); + + client->ReleaseRequest(); + } + { + // Chunked transfer with state + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + + BOOST_CHECK(!client->m_req->m_chunk_size); + BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0); + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + // First chunk is incomplete + client->receive("GET / HTTP/1.0\n" + "Transfer-Encoding: chunked\n" + "\n" + "10\n" + R"({"method)"); + client->ReadRequest(*client->m_req); + BOOST_CHECK(client->m_req->m_chunk_size); + BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 16); + BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 8); + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 8); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // More data arrives, chunk is completed. + client->receive(R"(":"getbl)""\n"); + client->ReadRequest(*client->m_req); + // State is reset + BOOST_CHECK(!client->m_req->m_chunk_size); + BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0); + // New data is added to body but body is still incomplete + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 16); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Next chunk arrives without terminal CRLF + client->receive("a\n" + R"(ockcount"})"); + client->ReadRequest(*client->m_req); + BOOST_CHECK(client->m_req->m_chunk_size); + BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 10); + BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 10); + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Chunk terminal CRLF arrives with final (size 0) chunk + client->receive("\n0\n\n"); + client->ReadRequest(*client->m_req); + // Body size hasn't changed + BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26); + // We're done + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + BOOST_CHECK_EQUAL(client->m_req->m_body, R"({"method":"getblockcount"})"); + + client->ReleaseRequest(); + } + { + // Invalid headers: error state stops reading + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + + // Request is in the buffer + client->receive("POST / HTTP/1.0\n" + "Host: 127.0.0.1\n" + "Invalid header with no colon\n" + "\n" + "body is not read"); + BOOST_CHECK(!client->m_recv_buffer.empty()); + + // Reading throws an error, sets state + BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req), + std::runtime_error, + HasReason{"HTTP header missing colon (:)"}); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error); + + // We read up to the invalid line + BOOST_CHECK_EQUAL(*client->m_req->m_headers.FindFirst("Host"), "127.0.0.1"); + // Buffer was cleared, client should just be disconnected now + BOOST_CHECK(client->m_recv_buffer.empty()); + + // Even if more data comes in, trying to read again in error state is a no-op + client->receive("Content-Length: 2\n\nok"); + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21); + + client->ReleaseRequest(); + } + { + // Headers sent in batches that are below MAX_HEADERS_SIZE but the total is excessive + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST /huge HTTP/1.0\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders); + + for (int i = 0; i < 410; ++i) { + client->receive("key:value\n"); + } + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders); + + for (int i = 0; i < 409; ++i) { + client->receive("key:value\n"); + } + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders); + + // We're at 819 x 10-byte headers + // The limit is 8192, three more bytes should throw. + client->receive("k:\n"); + BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req), + std::runtime_error, + HasReason{"HTTP headers exceed size limit"}); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error); + + client->ReleaseRequest(); + } + { + // Client sends chunks that are below the limit but the total is excessive + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init); + + client->receive("POST /huge HTTP/1.0\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders); + + client->receive("Transfer-Encoding: chunked\n\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Send 16-byte chunk + client->receive("10\nno auto updates!\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // The next chunk will be of size 32MiB - 16 + 1, below the limit + // on its own but not if it were added to the total cumulative body so far. + // We don't need to actually send or prepare this amount of data. + client->receive("1fffff1\n"); + BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req), + http_bitcoin::ContentTooLargeError, + HasReason{"Chunk will exceed max body size"}); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error); + + client->ReleaseRequest(); + } + { + // Ensure chunk trailer is parsed over state lines + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + + // Send a 1-byte chunk then send the 0-chunk with a trailer but no terminal CRLF + client->receive("GET / HTTP/1.0\n" + "Transfer-Encoding: chunked\n" + "\n" + "1\n" + "x\n" + "0\n" + "Digest: sha-4=deadbeef\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Send first part of another trailer line + client->receive("Expires:"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Finish the trailer line + client->receive("never\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // Terminate + client->receive("\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete); + BOOST_CHECK_EQUAL(client->m_req->m_body, "x"); + + client->ReleaseRequest(); + } + { + // Ensure chunk trailer counts towards the headers size limit + std::shared_ptr client{std::make_shared()}; + client->m_req = std::make_unique(client); + + client->receive("POST /huge HTTP/1.0\n" + "Transfer-Encoding: chunked\n"); // 27 bytes + for (int i = 0; i < 816; ++i) { + client->receive("key:value\n"); // 8160 + } + client->receive("\n" // 1 + "1\n" + "x\n" + "0\n"); + client->ReadRequest(*client->m_req); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody); + + // We're in the trailer section with a total of 8188 bytes of headers. + // The limit is 8192, five more bytes should throw. + client->receive("k:vv\n"); + BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req), + std::runtime_error, + HasReason{"HTTP headers exceed size limit"}); + BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error); + + client->ReleaseRequest(); + } +} + BOOST_AUTO_TEST_CASE(http_server_socket_tests) { // Hard code the timestamp for the Date header in the HTTP response