15#include <boost/test/unit_test.hpp>
24 "Connection: close\r\n"
25 "Content-Type: application/json\r\n"
26 "Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
27 "Content-Length: 46\r\n"
29 R
"({"method":"getblockcount","params":[],"id":1})""\n";
31BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
33BOOST_AUTO_TEST_CASE(test_query_parameters)
37 // Tolerate a URI with invalid characters (% not followed by hex digits)
38 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
39 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
40 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");
43 uri = "localhost:8080/rest/headers/someresource.json";
44 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
47 uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
48 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
49 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));
51 // Multiple parameters
52 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
53 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
54 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");
56 // If the query string contains duplicate keys, the first value is returned
57 uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
58 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
60 // Invalid query string syntax is the same as not having parameters
61 uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
62 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
64 // Multiple parameters, some characters encoded
65 uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
66 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
67 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");
69 // Encoded query delimiters are part of the parameter value, not structure.
70 uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
71 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
72 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");
74 // An encoded question mark in the path does not introduce a query section.
75 uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
76 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
79BOOST_AUTO_TEST_CASE(http_headers_tests)
82 // Writing response headers
83 HTTPHeaders headers{};
84 BOOST_CHECK(!headers.FindFirst("Cache-Control"));
85 headers.Write("Cache-Control", "no-cache");
86 // Check case-insensitive key matching
87 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
88 BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
89 // Additional values are appended, compared case-insensitive
90 headers.Write("cache-control", "max-age=60");
91 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
92 BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
94 headers.Write("Pie", "apple");
95 headers.Write("Sandwich", "ham");
96 headers.Write("Coffee", "black");
97 BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
99 headers.RemoveAll("Pie");
100 BOOST_CHECK(!headers.FindFirst("Pie"));
101 // Combine for transmission
102 std::string headers_string{headers.Stringify()};
103 BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
104 "cache-control: max-age=60\r\n"
110 // Reading request headers captured from bitcoin-cli
111 constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
112 "Connection: close\r\n"
113 "Content-Type: application/json\r\n"
114 "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
115 "Content-Length: 46\r\n";
116 util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
117 HTTPHeaders headers{};
118 headers.Read(reader);
119 BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
120 BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
121 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
122 BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
123 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
124 BOOST_CHECK(!headers.FindFirst("Pizza"));
126 // Ensure invalid headers are rejected
129 util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
130 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
160 lines.reserve(820 * 10);
161 for (
int i = 0; i < 820; ++i) {
162 lines.append(
"key:value\n");
191 res.StringifyHeaders(),
192 "HTTP/1.1 200 OK\r\n"
193 "Content-Length: 41\r\n"
210 BOOST_CHECK_EQUAL(req.
GetHeader(
"Authorization"),
"Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
215 // Malformed: no spaces between data
217 LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
218 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
221 // Malformed: too many spaces
223 LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
224 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
227 // Malformed: slash missing before version
229 LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
230 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
233 // Malformed: no decimal in version
235 LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
236 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
239 // Malformed: version is not a number
241 LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
242 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
245 // Malformed: version is out of range
247 LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
248 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
251 // Malformed: version is out of range
253 LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
254 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
257 // Malformed: version is out of range
259 LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
260 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
263 // Malformed: version is not exactly two integers and a dot
265 LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
266 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
269 // Malformed: contains NUL
271 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};
272 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
275 // Malformed: differing Content-Length values, case insensitive
276 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
278 "Content-Length: 8\n"
279 "content-length: 9\n\n"
282 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
283 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
286 // Ok: multiple same Content-Length values
287 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
289 "Content-Length: 8\n"
290 "content-length: 8\n\n"
293 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
294 BOOST_CHECK_EQUAL(req.Load(reader), HTTPRequest::State::Complete);
299 LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
300 BOOST_CHECK_EQUAL(req.Load(reader), HTTPRequest::State::Complete);
301 BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::GET);
302 BOOST_CHECK_EQUAL(req.GetURI(), "/");
303 BOOST_CHECK_EQUAL(req.GetVersion().major, 1);
304 BOOST_CHECK_EQUAL(req.GetVersion().minor, 0);
305 BOOST_CHECK_EQUAL(req.GetHeader("Host"), "127.0.0.1");
307 BOOST_CHECK_EQUAL(req.ReadBody(), "");
310 // Malformed: missing colon
312 LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
313 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
370 std::string_view
ok_chunked =
"GET / HTTP/1.0\n"
371 "Transfer-Encoding: chunked\n"
374 R
"({"method":"getbl)""\n"
379 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
380 BOOST_CHECK_EQUAL(req.Load(reader), HTTPRequest::State::Complete);
381 BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
387 "Transfer-Encoding: chunked\n"
390 R
"({"method":"getbl)""\n"
395 LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
396 BOOST_CHECK_EXCEPTION(req.Load(reader), ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
399 // Allow (but ignore) Chunk Extensions
401 std::string_view ok_chunked = "GET / HTTP/1.0\n"
402 "Transfer-Encoding: chunked\n"
404 "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
405 R"({"method":"getbl)""\n"
408 "0;why;would;anyone;do;this;\n"
409 "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
411 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
412 BOOST_CHECK_EQUAL(req.Load(reader), HTTPRequest::State::Complete);
413 BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
422 "Transfer-Encoding: chunked\n"
425 R
"({"method":"getbl)""\n"
430 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
431 BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
434 // Invalid "chunked" transfer, missing chunk termination \n
436 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
437 "Transfer-Encoding: chunked\n"
440 R"({"method":"getbl)"
458 void Receive(std::string_view s)
476 "Content-Length: 10\n\n",
488 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
491 client->Receive(
"POST / HTTP/1.0\n"
493 "Content-Length: 10\n\n"
501 "GET /endpoint HTTP/1.0\n\n");
526 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
529 client->Receive(
"POST / HTTP/1.0\n"
530 "Content-Length: 30000\n\n");
536 for (
int i = 1; i <= 3; ++i) {
537 client->Receive(std::string(10000,
'x'));
554 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
557 client->Receive(
"POST / HTTP/1.0\n"
558 "Content-Length: 4\n\n"
560 "GET /next HTTP/1.0\n\n");
569 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
573 client->Receive(
"GET / HTTP/1.0\n"
574 "Transfer-Encoding: chunked\n"
586 client->Receive(R
"(":"getbl)""\n");
587 BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
589 BOOST_CHECK(!client->GetRequest()->GetChunkSize());
590 BOOST_CHECK_EQUAL(client->GetRequest()->GetChunkProgress(), 0);
591 // New data is added to body but body is still incomplete
592 BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 16);
593 BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), NeedsBody);
595 // Next chunk arrives without terminal CRLF
596 client->Receive("a\n"
606 client->Receive(
"\n0\n\n");
616 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
619 client->Receive(
"POST / HTTP/1.0\n"
620 "Host: 127.0.0.1\n");
624 client->Receive(
"Invalid header with no colon\n"
637 client->Receive(
"Content-Length: 2\n\nok");
644 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
649 client->Receive(
"POST /huge HTTP/1.0\n");
653 for (
int i = 0; i < 410; ++i) {
654 client->Receive(
"key:value\n");
659 for (
int i = 0; i < 409; ++i) {
660 client->Receive(
"key:value\n");
673 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
677 client->Receive(
"POST /huge HTTP/1.0\n");
681 client->Receive(
"Transfer-Encoding: chunked\n\n");
686 client->Receive(
"10\nno auto updates!\n");
693 client->Receive(
"1fffff1\n");
699 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
703 client->Receive(
"GET / HTTP/1.0\n"
704 "Transfer-Encoding: chunked\n"
709 "Digest: sha-4=deadbeef\n");
714 client->Receive(
"Expires:");
719 client->Receive(
"never\n");
731 std::shared_ptr<DummyClient>
client{std::make_shared<DummyClient>()};
734 client->Receive(
"POST /huge HTTP/1.0\n"
735 "Transfer-Encoding: chunked\n");
736 for (
int i = 0; i < 816; ++i) {
737 client->Receive(
"key:value\n");
748 client->Receive(
"k:vv\n");
763 constexpr auto timeout{30
s};
768 auto idle{std::make_shared<Client>()};
773 BOOST_CHECK(
idle->ShouldDisconnect(now + timeout + 1s, timeout,
false));
780 ssize_t Send(
const void*,
size_t len,
int)
const override {
785 assert(
false &&
"Move of Sock into SlowSocket not allowed.");
791 auto busy_slow{std::make_shared<Client>(std::make_unique<SlowSocket>())};
792 busy_slow->MutateRecvBuffer().append(
"GET / HTTP/1.0\n\n");
820 auto busy_fast{std::make_shared<Client>(std::make_unique<ZeroSock>())};
821 busy_fast->MutateRecvBuffer().append(
"GET / HTTP/1.0\n\n");
831 if (s)
BOOST_ERROR(
"Should not get idle message when m_disconnect is already set.");
840 auto keepalive{std::make_shared<Client>(std::make_unique<ZeroSock>())};
841 keepalive->MutateRecvBuffer().append(
"GET / HTTP/1.1\nHost: 127.0.0.1\n\n");
853 auto bad{std::make_shared<Client>()};
854 bad->MutateRecvBuffer().append(
"GET / HTTP/1.0\nInvalid header with no colon\n\n");
869 std::deque<std::unique_ptr<HTTPRequest>> requests;
870 auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
872 requests.push_back(std::move(req));
883 BOOST_CHECK_EQUAL(result.error(),
"Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
897 server.StartSocketsThreads();
912 while (server.GetConnectionsCount() < 1) {
913 std::this_thread::sleep_for(10
ms);
918 std::shared_ptr<HTTPRemoteClient>
client;
928 if (requests.size() == 1) {
930 BOOST_CHECK_EQUAL(requests.front()->ReadBody(), R
"({"method":"getblockcount","params":[],"id":1})""\n");
931 BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
933 // Inspect the connection pointed to from the request
934 client = requests.front()->GetClient();
935 BOOST_REQUIRE(client);
936 BOOST_CHECK_EQUAL(client->GetOrigin(), "5.5.5.5:6789");
938 // Respond to request
939 requests.front()->WriteReply(HTTP_OK, "874140\n");
944 std::this_thread::sleep_for(10ms);
945 BOOST_REQUIRE(--attempts > 0);
948 // Check the sent response from the mock client at the other end of the mock socket
949 constexpr std::string_view expected_response{
950 "HTTP/1.1 200 OK\r\n"
951 "Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n"
952 "Content-Length: 7\r\n"
953 "Content-Type: text/html; charset=ISO-8859-1\r\n"
954 "Connection: close\r\n"
958 actual.reserve(expected_response.length());
959 // Wait up to one minute for all the bytes to appear in the "send" pipe.
960 char buf[0x10000] = {};
964 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
965 if (bytes_read > 0) {
966 actual.append(buf, bytes_read);
967 if (actual.length() >= expected_response.length()) {
971 std::this_thread::sleep_for(10ms);
974 BOOST_CHECK_EQUAL(actual, expected_response);
976 // Wait up to one minute for connection to be automatically closed, because
977 // keep-alive was not set by the client and we are done responding to their request.
979 while (server.GetConnectionsCount() != 0) {
980 std::this_thread::sleep_for(10ms);
981 BOOST_REQUIRE(--attempts > 0);
984 // Stop the I/O loop and shutdown
985 server.InterruptNet();
986 // Wait for I/O loop to finish, after all connected sockets are closed
987 server.JoinSocketsThreads();
988 // Close all listening sockets
989 server.StopListening();
992BOOST_AUTO_TEST_CASE(http_socket_error_tests)
994 // Create a tiny threadpool for the HTTPRequest handler
995 ThreadPool workers("http");
998 // Hard-code the server's request handler to respond to each request with
999 // an incremented block count. Handle the replies in the worker thread.
1000 std::atomic<int> height{0};
1001 HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
1002 auto item = [req, &height]() {
1003 const int h = height.fetch_add(1);
1004 req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
1006 // Can't call BOOST_REQUIRE from worker thread
1007 Assert(workers.Submit(std::move(item)));
1009 server.InitHTTPAllowList();
1011 // All replies will be the same size
1012 static constexpr std::size_t reply_length = std::string_view{
1013 "HTTP/1.1 200 OK\r\n"
1014 "Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n" // All RFC1123 dates are 29 characters
1015 "Content-Length: 10\r\n"
1016 "Content-Type: text/html; charset=ISO-8859-1\r\n"
1030 class ErrorSock : public DynSock
1033 explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
1034 DynSock& operator=(Sock&&) override { assert(false); return *this; }
1036 ssize_t Send(const void* buf, size_t len, int flags) const override
1038 if (len <= reply_length && !m_have_sent) {
1040 WSASetLastError(WSAEWOULDBLOCK);
1047 return DynSock::Send(buf, len, flags);
1051 mutable bool m_have_sent{false};
1054 // Simpler server startup than the last test
1055 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
1056 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
1057 server.StartSocketsThreads();
1059 // Prepare initial requests
1060 int num_requests = 2;
1061 // Use keep-alive so the server holds the connection open for all requests.
1062 std::string keepalive_request{full_request};
1063 keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
1064 // Combine all requests so they are read from the socket on a single iteration of the I/O loop
1065 std::string all_requests;
1066 for (int i = 0; i < num_requests; i++) {
1067 all_requests += keepalive_request;
1070 // Watch the log messages to ensure that the first two replies were sent
1071 // together. This indicates the non-optimistic send path was used
1072 // because a reply was already sitting in the send buffer when a second reply
1074 DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
1075 [&](const std::string* s) {
1078 // Last reply should be sent on its own by optimistic send path, because
1079 // the send buffer was empty when the reply was written.
1080 DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
1081 [&](const std::string* s) {
1085 // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
1086 std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
1087 ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
1090 // Wait up to one minute for the last reply from the server
1092 char buf[0x10000] = {};
1093 int attempts = 6000;
1094 while (attempts > 0)
1096 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1097 if (bytes_read > 0) {
1098 actual.append(buf, bytes_read);
1099 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1103 std::this_thread::sleep_for(10ms);
1107 // Send the third request.
1108 // If there was a race between WriteReply() in the worker thread setting m_send_ready=true
1109 // and SocketHandlerConnected() in the I/O thread flushing the send buffer,
1110 // then the socket would be stuck in write mode with nothing to write,
1111 // the server would never read from the socket, and this request would time out.
1112 // Wait a second to ensure both the worker thread and I/O thread are idle.
1113 // If we send the next request too soon it might get accepted by the server before
1114 // it gets wedged shut.
1115 std::this_thread::sleep_for(1000ms);
1116 mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
1119 // Wait up to one minute for reply
1121 while (attempts > 0)
1123 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1124 if (bytes_read > 0) {
1125 actual.append(buf, bytes_read);
1126 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1130 std::this_thread::sleep_for(10ms);
1134 // All replies were received
1135 for (int i = 0; i < num_requests; i++) {
1136 BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
1139 // Close the keep-alive connection
1140 server.DisconnectAllClients();
1144 server.InterruptNet();
1145 server.JoinSocketsThreads();
1146 server.StopListening();
1149BOOST_AUTO_TEST_CASE(http_server_rejects_disallowed_client_before_read)
1151 // DynSock reports accepted connections as coming from 5.5.5.5.
1152 gArgs.ForceSetArg("-rpcallowip", "4.4.4.4");
1154 std::atomic_bool request_dispatched{false};
1155 HTTPServer server{[&request_dispatched](std::unique_ptr<HTTPRequest>&&) {
1156 request_dispatched = true;
1158 BOOST_REQUIRE(server.InitHTTPAllowList());
1160 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
1161 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
1162 server.StartSocketsThreads();
1164 // Queue a complete request; the server should never read from it.
1165 std::shared_ptr<DynSock::Pipes> client_pipes{
1166 ConnectClient(std::as_bytes(std::span(full_request)))};
1168 // Wait for the socket to close with an EOF (bytes_read == 0)
1169 // 'bytes_read > 0' means the server replied to the prohibited client
1170 // 'bytes_read < 0' is an error, expected until the connection is fully processed by the I/O loop
1171 ssize_t bytes_read{};
1172 char buf[0x10000]{};
1173 for (int attempts{0}; attempts != 1'000; ++attempts) {
1174 bytes_read = client_pipes->send.GetBytes(&buf, sizeof(buf), MSG_PEEK);
1175 if (bytes_read >= 0) break;
1176 std::this_thread::sleep_for(10ms);
1179 BOOST_CHECK_EQUAL(bytes_read, 0);
1180 BOOST_CHECK(!request_dispatched);
1181 BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
1183 server.InterruptNet();
1184 server.JoinSocketsThreads();
1185 server.StopListening();
1187 // 'recv' buffer still holds the client's request untouched which
1188 // proves the server never called Recv()
1189 const ssize_t recv_bytes{client_pipes->recv.GetBytes(&buf, sizeof(buf))};
1190 BOOST_REQUIRE_EQUAL(recv_bytes, static_cast<ssize_t>(full_request.size()));
1191 BOOST_CHECK_EQUAL(std::string_view(buf, recv_bytes), full_request);
1194BOOST_AUTO_TEST_SUITE_END()
A combination of a network address (CNetAddr) and a (TCP) port.
Helper to initialize the global NodeClock, let a duration elapse, and reset it after use in a test.
Helper to initialize the global MockableSteadyClock, let a duration elapse, and reset it after use in...
std::string & MutateRecvBuffer()
Used for tests.
static std::unique_ptr< HTTPRequest > TryReadRequest(const std::shared_ptr< HTTPRemoteClient > &client) EXCLUSIVE_LOCKS_REQUIRED(!client -> m_send_mutex)
Try to read an HTTPRequest from a client's receive buffer.
void Receive() EXCLUSIVE_LOCKS_REQUIRED(!m_sock_mutex)
std::string GetURI() const
State Load(util::LineReader &reader)
Try to read an HTTP request or part of it.
std::optional< std::string > GetHeader(std::string_view hdr) const
HTTPRequestMethod GetRequestMethod() const
std::string ReadBody() const
const HTTPVersion & GetVersion() const LIFETIMEBOUND
bool InitHTTPAllowList()
Parse the user's -rpcallowip settings and populate m_allow_subnets.
BOOST_CHECK_EXCEPTION predicates to check the specific validation error.
RAII helper class that manages a socket and closes it automatically when it goes out of scope.
A mocked Sock alternative that succeeds on all operations.
ssize_t Send(const void *, size_t len, int) const override
send(2) wrapper.
ZeroSock & operator=(Sock &&other) override
Move assignment operator, grab the socket from another object and close ours (if set).
size_t Remaining() const
Returns remaining size of bytes in buffer.
BOOST_AUTO_TEST_CASE(http_response_tests)
std::string_view excessive_headers
BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE)
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"})
constexpr std::string_view full_request
std::unique_ptr< ProxyClient< messages::FooInterface > > client
constexpr uint64_t MAX_BODY_SIZE
Maximum size of an HTTP request body received from a client.
constexpr size_t MAX_HEADERS_SIZE
Maximum size of each headers line in an HTTP request, also the maximum size of all headers total.
std::string ToString(const T &t)
Locale-independent version of std::to_string.
std::vector< CService > Lookup(const std::string &name, uint16_t portDefault, bool fAllowLookup, unsigned int nMaxSolutions, DNSLookupFn dns_lookup_function)
Resolve a service string to its corresponding service.
std::function< std::unique_ptr< Sock >(int, int, int)> CreateSock
Socket factory.
#define BOOST_CHECK_EQUAL(v1, v2)
#define BOOST_CHECK(expr)
constexpr auto CeilDiv(const Dividend dividend, const Divisor divisor)
Integer ceiling division (for unsigned values).
static time_point now() noexcept
Return current system time or mocked time, if set.
Thrown when a request body exceeds MAX_BODY_SIZE (or will exceed, in chunked transfer) so the server ...
constexpr auto Ticks(Dur2 d)
Helper to count the seconds of a duration/time_point.