252 LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
253 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
256 // Malformed: version is out of range
258 LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
259 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
262 // Malformed: version is not exactly two integers and a dot
264 LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
265 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
268 // Malformed: contains NUL
270 LineReader reader{std::string_view{"GET /safe\0/etc/passwd HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", 50}, MAX_HEADERS_SIZE};
271 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
274 // Malformed: differing Content-Length values, case insensitive
275 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
277 "Content-Length: 8\n"
278 "content-length: 9\n\n"
281 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
282 BOOST_CHECK(req.LoadControlData(reader));
283 BOOST_CHECK(req.LoadHeaders(reader));
284 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
287 // Ok: multiple same Content-Length values
288 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
290 "Content-Length: 8\n"
291 "content-length: 8\n\n"
294 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
295 BOOST_CHECK(req.LoadControlData(reader));
296 BOOST_CHECK(req.LoadHeaders(reader));
297 BOOST_CHECK(req.LoadBody(reader));
302 LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
303 BOOST_CHECK(req.LoadControlData(reader));
304 BOOST_CHECK(req.LoadHeaders(reader));
305 BOOST_CHECK(req.LoadBody(reader));
306 BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::GET);
307 BOOST_CHECK_EQUAL(req.GetURI(), "/");
308 BOOST_CHECK_EQUAL(req.GetVersion().major, 1);
309 BOOST_CHECK_EQUAL(req.GetVersion().minor, 0);
310 BOOST_CHECK_EQUAL(req.GetHeader("Host"), "127.0.0.1");
312 BOOST_CHECK_EQUAL(req.ReadBody(), "");
315 // Malformed: missing colon
317 LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
318 BOOST_CHECK(req.LoadControlData(reader));
319 BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
359 std::string
huge_body(excessive_size,
'x');
389 std::string_view
ok_chunked =
"GET / HTTP/1.0\n"
390 "Transfer-Encoding: chunked\n"
393 R
"({"method":"getbl)""\n"
398 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
399 BOOST_CHECK(req.LoadControlData(reader));
400 BOOST_CHECK(req.LoadHeaders(reader));
401 BOOST_CHECK(req.LoadBody(reader));
402 BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
408 "Transfer-Encoding: chunked\n"
411 R
"({"method":"getbl)""\n"
416 LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
417 BOOST_CHECK(req.LoadControlData(reader));
418 BOOST_CHECK(req.LoadHeaders(reader));
419 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
422 // Allow (but ignore) Chunk Extensions
424 std::string_view ok_chunked = "GET / HTTP/1.0\n"
425 "Transfer-Encoding: chunked\n"
427 "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
428 R"({"method":"getbl)""\n"
431 "0;why;would;anyone;do;this;\n"
432 "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
434 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
435 BOOST_CHECK(req.LoadControlData(reader));
436 BOOST_CHECK(req.LoadHeaders(reader));
437 BOOST_CHECK(req.LoadBody(reader));
438 BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
447 "Transfer-Encoding: chunked\n"
450 R
"({"method":"getbl)""\n"
455 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
456 BOOST_CHECK(req.LoadControlData(reader));
457 BOOST_CHECK(req.LoadHeaders(reader));
458 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
461 // Invalid "chunked" transfer, missing chunk termination \n
463 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
464 "Transfer-Encoding: chunked\n"
467 R"({"method":"getbl)"
485 DummyClient() : HTTPRemoteClient{0, CService(),
CreateSock(0, 0, 0)} {}
487 void receive(std::string_view s)
489 MutateRecvBuffer().append(s);
495 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
498 client->receive(
"POST / HTTP/1.0\n");
500 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
502 client->receive(
"Host: 127.0.0.1\n"
503 "Content-Length: 10\n\n");
505 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
507 client->receive(
"I miss you\n");
514 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
517 client->receive(
"POST / HTTP/1.0\n"
519 "Content-Length: 10\n\n"
523 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
526 client->receive(
" you"
527 "GET /endpoint HTTP/1.0\n\n");
552 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
555 client->receive(
"POST / HTTP/1.0\n"
556 "Content-Length: 30000\n\n");
558 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
562 for (
int i = 1; i <= 3; ++i) {
563 client->receive(std::string(10000,
'x'));
580 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
583 client->receive(
"POST / HTTP/1.0\n"
584 "Content-Length: 4\n\n"
586 "GET /next HTTP/1.0\n\n");
595 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
599 client->receive(
"GET / HTTP/1.0\n"
600 "Transfer-Encoding: chunked\n"
605 BOOST_REQUIRE(client->GetRequest()->GetChunkSize());
609 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
612 client->receive(R
"(":"getbl)""\n");
613 BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
615 BOOST_CHECK(!client->GetRequest()->GetChunkSize());
616 BOOST_CHECK_EQUAL(client->GetRequest()->GetChunkProgress(), 0);
617 // New data is added to body but body is still incomplete
618 BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 16);
619 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
621 // Next chunk arrives without terminal CRLF
622 client->receive("a\n"
629 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
632 client->receive(
"\n0\n\n");
642 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
645 client->receive(
"POST / HTTP/1.0\n"
646 "Host: 127.0.0.1\n");
649 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
650 client->receive(
"Invalid header with no colon\n"
663 client->receive(
"Content-Length: 2\n\nok");
670 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
675 client->receive(
"POST /huge HTTP/1.0\n");
677 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
679 for (
int i = 0; i < 410; ++i) {
680 client->receive(
"key:value\n");
683 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
685 for (
int i = 0; i < 409; ++i) {
686 client->receive(
"key:value\n");
689 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
693 client->receive(
"k:\n");
699 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
703 client->receive(
"POST /huge HTTP/1.0\n");
705 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
707 client->receive(
"Transfer-Encoding: chunked\n\n");
709 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
712 client->receive(
"10\nno auto updates!\n");
714 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
719 client->receive(
"1fffff1\n");
725 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
729 client->receive(
"GET / HTTP/1.0\n"
730 "Transfer-Encoding: chunked\n"
735 "Digest: sha-4=deadbeef\n");
737 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
740 client->receive(
"Expires:");
742 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
745 client->receive(
"never\n");
747 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
750 client->receive(
"\n");
757 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
760 client->receive(
"POST /huge HTTP/1.0\n"
761 "Transfer-Encoding: chunked\n");
762 for (
int i = 0; i < 816; ++i) {
763 client->receive(
"key:value\n");
770 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
774 client->receive(
"k:vv\n");
788 Mutex requests_mutex;
789 std::deque<std::unique_ptr<HTTPRequest>> requests;
790 auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&&
req) {
791 LOCK(requests_mutex);
792 requests.push_back(std::move(
req));
800 CService onion_address{
Lookup(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", 0,
false).value()};
802 BOOST_REQUIRE(!result);
803 BOOST_CHECK_EQUAL(result.error(),
"Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
825 std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(
full_request)))};
830 std::this_thread::sleep_for(10ms);
831 BOOST_REQUIRE(--attempts > 0);
835 std::shared_ptr<HTTPRemoteClient> client;
843 LOCK(requests_mutex);
845 if (requests.size() == 1) {
847 BOOST_CHECK_EQUAL(requests.front()->ReadBody(), R
"({"method":"getblockcount","params":[],"id":1})""\n");
848 BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
850 // Inspect the connection pointed to from the request
851 client = requests.front()->GetClient();
852 BOOST_REQUIRE(client);
853 BOOST_CHECK_EQUAL(client->GetOrigin(), "5.5.5.5:6789");
855 // Respond to request
856 requests.front()->WriteReply(HTTP_OK, "874140\n");
861 std::this_thread::sleep_for(10ms);
862 BOOST_REQUIRE(--attempts > 0);
865 // Check the sent response from the mock client at the other end of the mock socket
867 // Wait up to one minute for all the bytes to appear in the "send" pipe.
868 char buf[0x10000] = {};
872 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
873 if (bytes_read > 0) {
874 actual.append(buf, bytes_read);
875 if (actual.length() == 146) {
879 std::this_thread::sleep_for(10ms);
882 BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
883 BOOST_CHECK(actual.ends_with("\r\n874140\n"));
884 // Headers can be sorted in any order, and will be, since we use unordered_map
885 BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
886 BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
887 BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
888 BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);
890 // Wait up to one minute for connection to be automatically closed, because
891 // keep-alive was not set by the client and we are done responding to their request.
893 while (server.GetConnectionsCount() != 0) {
894 std::this_thread::sleep_for(10ms);
895 BOOST_REQUIRE(--attempts > 0);
898 // Stop the I/O loop and shutdown
899 server.InterruptNet();
900 // Wait for I/O loop to finish, after all connected sockets are closed
901 server.JoinSocketsThreads();
902 // Close all listening sockets
903 server.StopListening();
906BOOST_AUTO_TEST_CASE(http_socket_error_tests)
908 // Create a tiny threadpool for the HTTPRequest handler
909 ThreadPool workers("http");
912 // Hard-code the server's request handler to respond to each request with
913 // an incremented block count. Handle the replies in the worker thread.
914 std::atomic<int> height{0};
915 HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
916 auto item = [req, &height]() {
917 const int h = height.fetch_add(1);
918 req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
920 // Can't call BOOST_REQUIRE from worker thread
921 Assert(workers.Submit(std::move(item)));
923 server.InitHTTPAllowList();
925 // All replies will be the same size
926 static constexpr std::size_t reply_length = std::string_view{
927 "HTTP/1.1 200 OK\r\n"
928 "Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n" // All RFC1123 dates are 29 characters
929 "Content-Length: 10\r\n"
930 "Content-Type: text/html; charset=ISO-8859-1\r\n"
944 class ErrorSock : public DynSock
947 explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
948 DynSock& operator=(Sock&&) override { assert(false); return *this; }
950 ssize_t Send(const void* buf, size_t len, int flags) const override
952 if (len <= reply_length && !m_have_sent) {
954 WSASetLastError(WSAEWOULDBLOCK);
961 return DynSock::Send(buf, len, flags);
965 mutable bool m_have_sent{false};
968 // Simpler server startup than the last test
969 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
970 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
971 server.StartSocketsThreads();
973 // Prepare initial requests
974 int num_requests = 2;
975 // Use keep-alive so the server holds the connection open for all requests.
976 std::string keepalive_request{full_request};
977 keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
978 // Combine all requests so they are read from the socket on a single iteration of the I/O loop
979 std::string all_requests;
980 for (int i = 0; i < num_requests; i++) {
981 all_requests += keepalive_request;
984 // Watch the log messages to ensure that the first two replies were sent
985 // together. This indicates the non-optimistic send path was used
986 // because a reply was already sitting in the send buffer when a second reply
988 DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
989 [&](const std::string* s) {
992 // Last reply should be sent on its own by optimistic send path, because
993 // the send buffer was empty when the reply was written.
994 DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
995 [&](const std::string* s) {
999 // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
1000 std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
1001 ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
1004 // Wait up to one minute for the last reply from the server
1006 char buf[0x10000] = {};
1007 int attempts = 6000;
1008 while (attempts > 0)
1010 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1011 if (bytes_read > 0) {
1012 actual.append(buf, bytes_read);
1013 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1017 std::this_thread::sleep_for(10ms);
1021 // Send the third request.
1022 // If there was a race between WriteReply() in the worker thread setting m_send_ready=true
1023 // and SocketHandlerConnected() in the I/O thread flushing the send buffer,
1024 // then the socket would be stuck in write mode with nothing to write,
1025 // the server would never read from the socket, and this request would time out.
1026 // Wait a second to ensure both the worker thread and I/O thread are idle.
1027 // If we send the next request too soon it might get accepted by the server before
1028 // it gets wedged shut.
1029 std::this_thread::sleep_for(1000ms);
1030 mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
1033 // Wait up to one minute for reply
1035 while (attempts > 0)
1037 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1038 if (bytes_read > 0) {
1039 actual.append(buf, bytes_read);
1040 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1044 std::this_thread::sleep_for(10ms);
1048 // All replies were received
1049 for (int i = 0; i < num_requests; i++) {
1050 BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
1053 // Close the keep-alive connection
1054 server.DisconnectAllClients();
1058 server.InterruptNet();
1059 server.JoinSocketsThreads();
1060 server.StopListening();
1063BOOST_AUTO_TEST_CASE(http_server_rejects_disallowed_client_before_read)
1065 // DynSock reports accepted connections as coming from 5.5.5.5.
1066 gArgs.ForceSetArg("-rpcallowip", "4.4.4.4");
1068 std::atomic_bool request_dispatched{false};
1069 HTTPServer server{[&request_dispatched](std::unique_ptr<HTTPRequest>&&) {
1070 request_dispatched = true;
1072 BOOST_REQUIRE(server.InitHTTPAllowList());
1074 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
1075 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
1076 server.StartSocketsThreads();
1078 // Queue a complete request; the server should never read from it.
1079 std::shared_ptr<DynSock::Pipes> client_pipes{
1080 ConnectClient(std::as_bytes(std::span(full_request)))};
1082 // Wait for the socket to close with an EOF (bytes_read == 0)
1083 // 'bytes_read > 0' means the server replied to the prohibited client
1084 // 'bytes_read < 0' is an error, expected until the connection is fully processed by the I/O loop
1085 ssize_t bytes_read{};
1086 char buf[0x10000]{};
1087 for (int attempts{0}; attempts != 1'000; ++attempts) {
1088 bytes_read = client_pipes->send.GetBytes(&buf, sizeof(buf), MSG_PEEK);
1089 if (bytes_read >= 0) break;
1090 std::this_thread::sleep_for(10ms);
1093 BOOST_CHECK_EQUAL(bytes_read, 0);
1094 BOOST_CHECK(!request_dispatched);
1095 BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
1097 server.InterruptNet();
1098 server.JoinSocketsThreads();
1099 server.StopListening();
1101 // 'recv' buffer still holds the client's request untouched which
1102 // proves the server never called Recv()
1103 const ssize_t recv_bytes{client_pipes->recv.GetBytes(&buf, sizeof(buf))};
1104 BOOST_REQUIRE_EQUAL(recv_bytes, static_cast<ssize_t>(full_request.size()));
1105 BOOST_CHECK_EQUAL(std::string_view(buf, recv_bytes), full_request);
1108BOOST_AUTO_TEST_SUITE_END()