Bitcoin Core 31.99.0
P2P Digital Currency
httpserver_tests.cpp
Go to the documentation of this file.
1// Copyright (c) 2012-present The Bitcoin Core developers
2// Distributed under the MIT software license, see the accompanying
3// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5#include <httpserver.h>
6#include <rpc/protocol.h>
7#include <test/util/common.h>
8#include <test/util/logging.h>
10#include <util/string.h>
11#include <util/threadpool.h>
12
13#include <boost/test/unit_test.hpp>
14
24
25// HTTP request captured from bitcoin-cli
26constexpr std::string_view full_request = "POST / HTTP/1.1\r\n"
27 "Host: 127.0.0.1\r\n"
28 "Connection: close\r\n"
29 "Content-Type: application/json\r\n"
30 "Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
31 "Content-Length: 46\r\n"
32 "\r\n"
33 R"({"method":"getblockcount","params":[],"id":1})""\n";
34
35BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
36
37BOOST_AUTO_TEST_CASE(test_query_parameters)
38{
39 std::string uri {};
40
41 // Tolerate a URI with invalid characters (% not followed by hex digits)
42 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
43 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
44 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");
45
46 // No parameters
47 uri = "localhost:8080/rest/headers/someresource.json";
48 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
49
50 // Single parameter
51 uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
52 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
53 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));
54
55 // Multiple parameters
56 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
57 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
58 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");
59
60 // If the query string contains duplicate keys, the first value is returned
61 uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
62 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
63
64 // Invalid query string syntax is the same as not having parameters
65 uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
66 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
67
68 // Multiple parameters, some characters encoded
69 uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
70 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
71 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");
72
73 // Encoded query delimiters are part of the parameter value, not structure.
74 uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
75 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
76 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");
77
78 // An encoded question mark in the path does not introduce a query section.
79 uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
80 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
81}
82
83BOOST_AUTO_TEST_CASE(http_headers_tests)
84{
85 {
86 // Writing response headers
87 HTTPHeaders headers{};
88 BOOST_CHECK(!headers.FindFirst("Cache-Control"));
89 headers.Write("Cache-Control", "no-cache");
90 // Check case-insensitive key matching
91 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
92 BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
93 // Additional values are appended, compared case-insensitive
94 headers.Write("cache-control", "max-age=60");
95 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
96 BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
97 // Add a few more
98 headers.Write("Pie", "apple");
99 headers.Write("Sandwich", "ham");
100 headers.Write("Coffee", "black");
101 BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
102 // Remove
103 headers.RemoveAll("Pie");
104 BOOST_CHECK(!headers.FindFirst("Pie"));
105 // Combine for transmission
106 std::string headers_string{headers.Stringify()};
107 BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
108 "cache-control: max-age=60\r\n"
109 "Sandwich: ham\r\n"
110 "Coffee: black\r\n"
111 "\r\n");
112 }
113 {
114 // Reading request headers captured from bitcoin-cli
115 constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
116 "Connection: close\r\n"
117 "Content-Type: application/json\r\n"
118 "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
119 "Content-Length: 46\r\n";
120 util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
121 HTTPHeaders headers{};
122 headers.Read(reader);
123 BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
124 BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
125 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
126 BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
127 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
128 BOOST_CHECK(!headers.FindFirst("Pizza"));
129 }
130 // Ensure invalid headers are rejected
131 {
132 // missing a colon
133 util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
134 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
135 }
136 {
137 // missing a key
138 util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
139 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
140 }
141 {
142 // contains NUL
143 util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
144 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
145 }
146 {
147 // contains bare \r (not followed by \n)
148 util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
149 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
150 }
151 {
152 // contains odd \r preceding the expected CRLF
153 util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
154 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
155 }
156 {
157 // key contains whitespace
158 util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
159 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
160 }
161 {
162 // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
163 std::string lines;
164 lines.reserve(820 * 10);
165 for (int i = 0; i < 820; ++i) {
166 lines.append("key:value\n");
167 }
168 std::string_view excessive_headers{lines};
171 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
172 }
173 {
174 // Ok
175 util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
179 }
180}
181
182BOOST_AUTO_TEST_CASE(http_response_tests)
183{
184 // Typical HTTP 1.1 response headers
186 headers.Write("Content-Length", "41");
187
188 // Response points to headers which already exist because some of them
189 // are set before we even know what the response will be.
190 HTTPResponse res;
191 res.m_version = {.major = 1, .minor = 1};
192 res.m_status = HTTP_OK;
193 res.m_headers = std::move(headers);
195 res.StringifyHeaders(),
196 "HTTP/1.1 200 OK\r\n"
197 "Content-Length: 41\r\n"
198 "\r\n");
199}
200
201BOOST_AUTO_TEST_CASE(http_request_tests)
202{
203 {
204 HTTPRequest req;
211 BOOST_CHECK_EQUAL(req.m_target, "/");
212 BOOST_CHECK_EQUAL(req.GetURI(), "/");
215 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
216 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
217 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
218 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
219 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
220 BOOST_CHECK_EQUAL(req.m_body.size(), 46);
221 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
222 }
223 {
224 // Malformed: no spaces between data
225 HTTPRequest req;
226 LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
227 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
228 }
229 {
230 // Malformed: too many spaces
231 HTTPRequest req;
232 LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
233 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
234 }
235 {
236 // Malformed: slash missing before version
237 HTTPRequest req;
238 LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
239 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
240 }
241 {
242 // Malformed: no decimal in version
243 HTTPRequest req;
244 LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
245 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
246 }
247 {
248 // Malformed: version is not a number
249 HTTPRequest req;
250 LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
251 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
252 }
253 {
254 // Malformed: version is out of range
255 HTTPRequest req;
256 LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
257 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
258 }
259 {
260 // Malformed: version is out of range
261 HTTPRequest req;
262 LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
263 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
264 }
265 {
266 // Malformed: version is out of range
267 HTTPRequest req;
268 LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
269 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
270 }
271 {
272 // Malformed: version is not exactly two integers and a dot
273 HTTPRequest req;
274 LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
275 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
276 }
277 {
278 // Malformed: contains NUL
279 HTTPRequest req;
280 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};
281 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
282 }
283 {
284 // Malformed: differing Content-Length values, case insensitive
285 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
286 "Host: 127.0.0.1\n"
287 "Content-Length: 8\n"
288 "content-length: 9\n\n"
289 "12345678";
290 HTTPRequest req;
291 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
292 BOOST_CHECK(req.LoadControlData(reader));
293 BOOST_CHECK(req.LoadHeaders(reader));
294 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
295 }
296 {
297 // Ok: multiple same Content-Length values
298 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
299 "Host: 127.0.0.1\n"
300 "Content-Length: 8\n"
301 "content-length: 8\n\n"
302 "12345678";
303 HTTPRequest req;
304 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
305 BOOST_CHECK(req.LoadControlData(reader));
306 BOOST_CHECK(req.LoadHeaders(reader));
307 BOOST_CHECK(req.LoadBody(reader));
308 }
309 {
310 // Ok
311 HTTPRequest req;
312 LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
313 BOOST_CHECK(req.LoadControlData(reader));
314 BOOST_CHECK(req.LoadHeaders(reader));
315 BOOST_CHECK(req.LoadBody(reader));
316 BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::GET);
317 BOOST_CHECK_EQUAL(req.m_target, "/");
318 BOOST_CHECK_EQUAL(req.m_version.major, 1);
319 BOOST_CHECK_EQUAL(req.m_version.minor, 0);
320 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
321 // no body is OK
322 BOOST_CHECK_EQUAL(req.m_body.size(), 0);
323 }
324 {
325 // Malformed: missing colon
326 HTTPRequest req;
327 LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
328 BOOST_CHECK(req.LoadControlData(reader));
329 BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
330 }
331 {
332 // We might not have received enough data from the client which is not
333 // an error. We return false so the caller can try again later when the
334 // buffer has more data.
335 HTTPRequest req;
336 LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
339 }
340 {
341 // No Content-Length: body is not read
342 HTTPRequest req;
343 LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
347 // Don't try to read request body if Content-Length is missing
348 BOOST_CHECK_EQUAL(req.m_body.size(), 0);
349 }
350 {
351 // Malformed: Content-Length is not a number
352 HTTPRequest req;
353 LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
356 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
357 }
358 {
359 // Malformed: Content-Length is negative
360 HTTPRequest req;
361 LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
364 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
365 }
366 {
367 // Content-Length exceeds limit
368 constexpr auto excessive_size{MAX_BODY_SIZE + 1};
369 std::string huge_body(excessive_size, 'x');
370 const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
371 HTTPRequest req;
376 }
377 {
378 // Content-Length exactly on the limit
379 std::string max_body(MAX_BODY_SIZE, 'x');
380 const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
381 HTTPRequest req;
386 }
387 {
388 // Content-Length indicates more data than we have in the buffer.
389 // Not an error; we wait for more data before completing the body.
390 HTTPRequest req;
391 LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
395 }
396 {
397 // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
398 HTTPRequest req;
399 std::string_view ok_chunked = "GET / HTTP/1.0\n"
400 "Transfer-Encoding: chunked\n"
401 "\n"
402 "10\n"
403 R"({"method":"getbl)""\n"
404 " a \n"
405 R"(ockcount"})""\n"
406 "0\n"
407 "\n";
408 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
409 BOOST_CHECK(req.LoadControlData(reader));
410 BOOST_CHECK(req.LoadHeaders(reader));
411 BOOST_CHECK(req.LoadBody(reader));
412 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
413 }
414 {
415 // Prevent "chunked" transfer from exceeding size limit
416 HTTPRequest req;
417 std::string_view excessive_chunk_size = "GET / HTTP/1.0\n"
418 "Transfer-Encoding: chunked\n"
419 "\n"
420 "10\n"
421 R"({"method":"getbl)""\n"
422 "20000000\n"
423 R"(ockcount"})""\n"
424 "0\n"
425 "\n";
426 LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
427 BOOST_CHECK(req.LoadControlData(reader));
428 BOOST_CHECK(req.LoadHeaders(reader));
429 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
430 }
431 {
432 // Allow (but ignore) Chunk Extensions
433 HTTPRequest req;
434 std::string_view ok_chunked = "GET / HTTP/1.0\n"
435 "Transfer-Encoding: chunked\n"
436 "\n"
437 "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
438 R"({"method":"getbl)""\n"
439 " a ; compressed\n"
440 R"(ockcount"})""\n"
441 "0;why;would;anyone;do;this;\n"
442 "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
443 "\n";
444 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
445 BOOST_CHECK(req.LoadControlData(reader));
446 BOOST_CHECK(req.LoadHeaders(reader));
447 BOOST_CHECK(req.LoadBody(reader));
448 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
449 // Chunk Trailer was parsed, but ignored
451 BOOST_CHECK(!req.GetHeader("Expires").first);
452 }
453 {
454 // Invalid "chunked" transfer, using roman numerals instead of hex for chunk length
455 HTTPRequest req;
456 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
457 "Transfer-Encoding: chunked\n"
458 "\n"
459 "XVI\n"
460 R"({"method":"getbl)""\n"
461 "X\n"
462 R"(ockcount"})""\n"
463 "0\n"
464 "\n";
465 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
466 BOOST_CHECK(req.LoadControlData(reader));
467 BOOST_CHECK(req.LoadHeaders(reader));
468 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
469 }
470 {
471 // Invalid "chunked" transfer, missing chunk termination \n
472 HTTPRequest req;
473 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
474 "Transfer-Encoding: chunked\n"
475 "\n"
476 "10\n"
477 R"({"method":"getbl)"
478 "a\n" // interpreted as extra data at the end of `0x10`-sized chunk
479 R"(ockcount"})"
480 "0\n"
481 "\n";
482 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
485 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
486 }
487}
488
489BOOST_AUTO_TEST_CASE(http_request_state_tests)
490{
491 // For these tests we just need a receive buffer for the requests to read from.
492 class DummyClient : public HTTPRemoteClient
493 {
494 public:
495 DummyClient() : HTTPRemoteClient{/*id=*/0, /*addr=*/CService(), /*socket=*/CreateSock(0, 0, 0)} {}
496
497 void receive(std::string_view s)
498 {
499 m_recv_buffer.insert(
500 m_recv_buffer.end(),
501 s.begin(),
502 s.end());
503 }
504 };
505
506 {
507 // Step through state machine
508 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
509 client->m_req = std::make_unique<HTTPRequest>(client);
510 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
511
512 client->receive("POST / HTTP/1.0\n");
513 client->ReadRequest(*client->m_req);
514 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
515
516 client->receive("Host: 127.0.0.1\n"
517 "Content-Length: 10\n\n");
518 client->ReadRequest(*client->m_req);
519 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
520
521 client->receive("I miss you\n");
522 client->ReadRequest(*client->m_req);
523 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
524 }
525 {
526 // Read body over multiple data pushes, multiple requests in same push
527 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
528 client->m_req = std::make_unique<HTTPRequest>(client);
529 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
530
531 client->receive("POST / HTTP/1.0\n"
532 "Host: 127.0.0.1\n"
533 "Content-Length: 10\n\n"
534 "I miss");
535 client->ReadRequest(*client->m_req);
536 // Because of the Content-Length header we know the body is not complete
537 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
538
539 // Finish sending first request and include second request in the same buffer
540 client->receive(" you"
541 "GET /endpoint HTTP/1.0\n\n");
542 client->ReadRequest(*client->m_req);
543 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
544 BOOST_CHECK_EQUAL(client->m_req->m_body, "I miss you");
545 // Next request sitting in buffer
546 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24);
547 // Complete first request hasn't been moved yet, expect no-op
548 client->ReadRequest(*client->m_req);
549 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24);
550
551 // Reset m_req
552 client->m_req = std::make_unique<HTTPRequest>(client);
553 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
554
555 // Read second request
556 client->ReadRequest(*client->m_req);
557 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
558 BOOST_CHECK_EQUAL(client->m_req->m_target, "/endpoint");
559 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0);
560 // Buffer is cleared
561 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0);
562 }
563 {
564 // A Content-Length body is drained out of the receive buffer as it
565 // arrives, instead of accumulating there until the request is complete.
566
567 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
568 client->m_req = std::make_unique<HTTPRequest>(client);
569 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
570
571 client->receive("POST / HTTP/1.0\n"
572 "Content-Length: 30000\n\n");
573 client->ReadRequest(*client->m_req);
574 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
575
576 // Body arrives in 10kB pieces. Each one is copied onto m_body and
577 // erased from the receive buffer, which never holds more than one piece.
578 for (int i = 1; i <= 3; ++i) {
579 client->receive(std::string(10000, 'x'));
580 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 10000);
581 client->ReadRequest(*client->m_req);
582 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 10000 * i);
583 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0);
584 }
585 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
586 }
587 {
588 // A body sent in the same push as the next request is split correctly
589 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
590 client->m_req = std::make_unique<HTTPRequest>(client);
591 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
592
593 client->receive("POST / HTTP/1.0\n"
594 "Content-Length: 4\n\n"
595 "body"
596 "GET /next HTTP/1.0\n\n");
597 client->ReadRequest(*client->m_req);
598 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
599 BOOST_CHECK_EQUAL(client->m_req->m_body, "body");
600 // Only the second request is left over
601 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 20);
602 }
603 {
604 // Chunked transfer with state
605 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
606 client->m_req = std::make_unique<HTTPRequest>(client);
607
608 BOOST_CHECK(!client->m_req->m_chunk_size);
609 BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0);
610 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0);
611 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
612
613 // First chunk is incomplete
614 client->receive("GET / HTTP/1.0\n"
615 "Transfer-Encoding: chunked\n"
616 "\n"
617 "10\n"
618 R"({"method)");
619 client->ReadRequest(*client->m_req);
620 BOOST_CHECK(client->m_req->m_chunk_size);
621 BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 16);
622 BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 8);
623 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 8);
624 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
625
626 // More data arrives, chunk is completed.
627 client->receive(R"(":"getbl)""\n");
628 client->ReadRequest(*client->m_req);
629 // State is reset
630 BOOST_CHECK(!client->m_req->m_chunk_size);
631 BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0);
632 // New data is added to body but body is still incomplete
633 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 16);
634 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
635
636 // Next chunk arrives without terminal CRLF
637 client->receive("a\n"
638 R"(ockcount"})");
639 client->ReadRequest(*client->m_req);
640 BOOST_CHECK(client->m_req->m_chunk_size);
641 BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 10);
642 BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 10);
643 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26);
644 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
645
646 // Chunk terminal CRLF arrives with final (size 0) chunk
647 client->receive("\n0\n\n");
648 client->ReadRequest(*client->m_req);
649 // Body size hasn't changed
650 BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26);
651 // We're done
652 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
653 BOOST_CHECK_EQUAL(client->m_req->m_body, R"({"method":"getblockcount"})");
654 }
655 {
656 // Invalid headers: error state stops reading
657 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
658 client->m_req = std::make_unique<HTTPRequest>(client);
659
660 // Request is in the buffer
661 client->receive("POST / HTTP/1.0\n"
662 "Host: 127.0.0.1\n"
663 "Invalid header with no colon\n"
664 "\n"
665 "body is not read");
666 BOOST_CHECK(!client->m_recv_buffer.empty());
667
668 // Reading throws an error, sets state
669 BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
670 std::runtime_error,
671 HasReason{"HTTP header missing colon (:)"});
672 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
673
674 // We read up to the invalid line
675 BOOST_CHECK_EQUAL(*client->m_req->m_headers.FindFirst("Host"), "127.0.0.1");
676 // Buffer was cleared, client should just be disconnected now
677 BOOST_CHECK(client->m_recv_buffer.empty());
678
679 // Even if more data comes in, trying to read again in error state is a no-op
680 client->receive("Content-Length: 2\n\nok");
681 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21);
682 client->ReadRequest(*client->m_req);
683 BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21);
684 }
685 {
686 // Headers sent in batches that are below MAX_HEADERS_SIZE but the total is excessive
687 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
688 client->m_req = std::make_unique<HTTPRequest>(client);
689 client->ReadRequest(*client->m_req);
690 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
691
692 client->receive("POST /huge HTTP/1.0\n");
693 client->ReadRequest(*client->m_req);
694 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
695
696 for (int i = 0; i < 410; ++i) {
697 client->receive("key:value\n");
698 }
699 client->ReadRequest(*client->m_req);
700 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
701
702 for (int i = 0; i < 409; ++i) {
703 client->receive("key:value\n");
704 }
705 client->ReadRequest(*client->m_req);
706 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
707
708 // We're at 819 x 10-byte headers
709 // The limit is 8192, three more bytes should throw.
710 client->receive("k:\n");
711 BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
712 std::runtime_error,
713 HasReason{"HTTP headers exceed size limit"});
714 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
715 }
716 {
717 // Client sends chunks that are below the limit but the total is excessive
718 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
719 client->m_req = std::make_unique<HTTPRequest>(client);
720 client->ReadRequest(*client->m_req);
721 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
722
723 client->receive("POST /huge HTTP/1.0\n");
724 client->ReadRequest(*client->m_req);
725 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
726
727 client->receive("Transfer-Encoding: chunked\n\n");
728 client->ReadRequest(*client->m_req);
729 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
730
731 // Send 16-byte chunk
732 client->receive("10\nno auto updates!\n");
733 client->ReadRequest(*client->m_req);
734 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
735
736 // The next chunk will be of size 32MiB - 16 + 1, below the limit
737 // on its own but not if it were added to the total cumulative body so far.
738 // We don't need to actually send or prepare this amount of data.
739 client->receive("1fffff1\n");
740 BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
742 HasReason{"Chunk will exceed max body size"});
743 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
744 }
745 {
746 // Ensure chunk trailer is parsed over state lines
747 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
748 client->m_req = std::make_unique<HTTPRequest>(client);
749
750 // Send a 1-byte chunk then send the 0-chunk with a trailer but no terminal CRLF
751 client->receive("GET / HTTP/1.0\n"
752 "Transfer-Encoding: chunked\n"
753 "\n"
754 "1\n"
755 "x\n"
756 "0\n"
757 "Digest: sha-4=deadbeef\n");
758 client->ReadRequest(*client->m_req);
759 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
760
761 // Send first part of another trailer line
762 client->receive("Expires:");
763 client->ReadRequest(*client->m_req);
764 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
765
766 // Finish the trailer line
767 client->receive("never\n");
768 client->ReadRequest(*client->m_req);
769 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
770
771 // Terminate
772 client->receive("\n");
773 client->ReadRequest(*client->m_req);
774 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
775 BOOST_CHECK_EQUAL(client->m_req->m_body, "x");
776 }
777 {
778 // Ensure chunk trailer counts towards the headers size limit
779 std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
780 client->m_req = std::make_unique<HTTPRequest>(client);
781
782 client->receive("POST /huge HTTP/1.0\n"
783 "Transfer-Encoding: chunked\n"); // 27 bytes
784 for (int i = 0; i < 816; ++i) {
785 client->receive("key:value\n"); // 8160
786 }
787 client->receive("\n" // 1
788 "1\n"
789 "x\n"
790 "0\n");
791 client->ReadRequest(*client->m_req);
792 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
793
794 // We're in the trailer section with a total of 8188 bytes of headers.
795 // The limit is 8192, five more bytes should throw.
796 client->receive("k:vv\n");
797 BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
798 std::runtime_error,
799 HasReason{"HTTP headers exceed size limit"});
800 BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
801 }
802}
803
804BOOST_AUTO_TEST_CASE(http_server_socket_tests)
805{
806 // Hard code the timestamp for the Date header in the HTTP response
807 // Wed Dec 11 00:47:09 2024 UTC
808 FakeNodeClock clock{1733878029s};
809
810 // Prepare a request handler that just stores received requests so we can examine them.
811 // Mutex is required to prevent a race between this test's main thread and the server's I/O loop.
812 Mutex requests_mutex;
813 std::deque<std::unique_ptr<HTTPRequest>> requests;
814 auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
815 LOCK(requests_mutex);
816 requests.push_back(std::move(req));
817 };
818
819 HTTPServer server{StoreRequest};
820 server.InitHTTPAllowList();
821
822 {
823 // We can only bind to NET_IPV4 and NET_IPV6
824 CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
825 auto result{server.BindAndStartListening(onion_address)};
826 BOOST_REQUIRE(!result);
827 BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
828 }
829
830 // This VALID address won't actually get used because we stubbed CreateSock()
831 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
832
833 // Init state
834 BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
835 // Bind to mock Listening Socket
836 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
837 // We are bound and listening
838 BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
839
840 // Start the I/O loop
841 server.StartSocketsThreads();
842
843 // No connections yet
844 BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
845
846 // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.
847 // Keep a handle for the mock client's send and receive pipes so we can examine
848 // the data it "receives".
849 std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};
850
851 // Wait up to a minute to find and connect the client in the I/O loop
852 int attempts{6000};
853 while (server.GetConnectionsCount() < 1) {
854 std::this_thread::sleep_for(10ms);
855 BOOST_REQUIRE(--attempts > 0);
856 }
857
858 // Prepare a pointer to the client, we'll assign it from the request itself.
859 std::shared_ptr<HTTPRemoteClient> client;
860
861 // Wait up to a minute to read the request from the client.
862 // Given that the mock client is itself a mock socket
863 // with hard-coded data it should only take a fraction of that.
864 attempts = 6000;
865 while (true) {
866 {
867 LOCK(requests_mutex);
868 // Connected client should have one request already from the static content.
869 if (requests.size() == 1) {
870 // Check the received request
871 BOOST_CHECK_EQUAL(requests.front()->m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
872 BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
873
874 // Inspect the connection pointed to from the request
875 client = requests.front()->m_client.lock();
876 BOOST_REQUIRE(client);
877 BOOST_CHECK_EQUAL(client->m_origin, "5.5.5.5:6789");
878
879 // Respond to request
880 requests.front()->WriteReply(HTTP_OK, "874140\n");
881
882 break;
883 }
884 }
885 std::this_thread::sleep_for(10ms);
886 BOOST_REQUIRE(--attempts > 0);
887 }
888
889 // Check the sent response from the mock client at the other end of the mock socket
890 std::string actual;
891 // Wait up to one minute for all the bytes to appear in the "send" pipe.
892 char buf[0x10000] = {};
893 attempts = 6000;
894 while (attempts > 0)
895 {
896 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
897 if (bytes_read > 0) {
898 actual.append(buf, bytes_read);
899 if (actual.length() == 146) {
900 break;
901 }
902 }
903 std::this_thread::sleep_for(10ms);
904 --attempts;
905 }
906 BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
907 BOOST_CHECK(actual.ends_with("\r\n874140\n"));
908 // Headers can be sorted in any order, and will be, since we use unordered_map
909 BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
910 BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
911 BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
912 BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);
913
914 // Wait up to one minute for connection to be automatically closed, because
915 // keep-alive was not set by the client and we are done responding to their request.
916 attempts = 6000;
917 while (server.GetConnectionsCount() != 0) {
918 std::this_thread::sleep_for(10ms);
919 BOOST_REQUIRE(--attempts > 0);
920 }
921
922 // Stop the I/O loop and shutdown
923 server.InterruptNet();
924 // Wait for I/O loop to finish, after all connected sockets are closed
925 server.JoinSocketsThreads();
926 // Close all listening sockets
927 server.StopListening();
928}
929
930BOOST_AUTO_TEST_CASE(http_socket_error_tests)
931{
932 // Create a tiny threadpool for the HTTPRequest handler
933 ThreadPool workers("http");
934 workers.Start(1);
935
936 // Hard-code the server's request handler to respond to each request with
937 // an incremented block count. Handle the replies in the worker thread.
938 std::atomic<int> height{0};
939 HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
940 auto item = [req, &height]() {
941 const int h = height.fetch_add(1);
942 req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
943 };
944 // Can't call BOOST_REQUIRE from worker thread
945 Assert(workers.Submit(std::move(item)));
946 }};
947 server.InitHTTPAllowList();
948
949 // All replies will be the same size
950 static constexpr std::size_t reply_length = std::string_view{
951 "HTTP/1.1 200 OK\r\n"
952 "Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n" // All RFC1123 dates are 29 characters
953 "Content-Length: 10\r\n"
954 "Content-Type: text/html; charset=ISO-8859-1\r\n"
955 "\r\n"
956 "height: 0\n"
957 }.size();
958
968 class ErrorSock : public DynSock
969 {
970 public:
971 explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
972 DynSock& operator=(Sock&&) override { assert(false); return *this; }
973
974 ssize_t Send(const void* buf, size_t len, int flags) const override
975 {
976 if (len <= reply_length && !m_have_sent) {
977 #ifdef WIN32
978 WSASetLastError(WSAEWOULDBLOCK);
979 #else
980 errno = WSAEAGAIN;
981 #endif
982 return -1;
983 } else {
984 m_have_sent = true;
985 return DynSock::Send(buf, len, flags);
986 }
987 }
988
989 mutable bool m_have_sent{false};
990 };
991
992 // Simpler server startup than the last test
993 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
994 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
995 server.StartSocketsThreads();
996
997 // Prepare initial requests
998 int num_requests = 2;
999 // Use keep-alive so the server holds the connection open for all requests.
1000 std::string keepalive_request{full_request};
1001 keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
1002 // Combine all requests so they are read from the socket on a single iteration of the I/O loop
1003 std::string all_requests;
1004 for (int i = 0; i < num_requests; i++) {
1005 all_requests += keepalive_request;
1006 }
1007
1008 // Watch the log messages to ensure that the first two replies were sent
1009 // together. This indicates the non-optimistic send path was used
1010 // because a reply was already sitting in the send buffer when a second reply
1011 // was added.
1012 DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
1013 [&](const std::string* s) {
1014 return true;
1015 }};
1016 // Last reply should be sent on its own by optimistic send path, because
1017 // the send buffer was empty when the reply was written.
1018 DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
1019 [&](const std::string* s) {
1020 return true;
1021 }};
1022
1023 // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
1024 std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
1025 ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
1026 };
1027
1028 // Wait up to one minute for the last reply from the server
1029 std::string actual;
1030 char buf[0x10000] = {};
1031 int attempts = 6000;
1032 while (attempts > 0)
1033 {
1034 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1035 if (bytes_read > 0) {
1036 actual.append(buf, bytes_read);
1037 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1038 break;
1039 }
1040 }
1041 std::this_thread::sleep_for(10ms);
1042 --attempts;
1043 }
1044
1045 // Send the third request.
1046 // If there was a race between WriteReply() in the worker thread setting m_send_ready=true
1047 // and SocketHandlerConnected() in the I/O thread flushing the send buffer,
1048 // then the socket would be stuck in write mode with nothing to write,
1049 // the server would never read from the socket, and this request would time out.
1050 // Wait a second to ensure both the worker thread and I/O thread are idle.
1051 // If we send the next request too soon it might get accepted by the server before
1052 // it gets wedged shut.
1053 std::this_thread::sleep_for(1000ms);
1054 mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
1055 num_requests++;
1056
1057 // Wait up to one minute for reply
1058 attempts = 6000;
1059 while (attempts > 0)
1060 {
1061 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1062 if (bytes_read > 0) {
1063 actual.append(buf, bytes_read);
1064 if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1065 break;
1066 }
1067 }
1068 std::this_thread::sleep_for(10ms);
1069 --attempts;
1070 }
1071
1072 // All replies were received
1073 for (int i = 0; i < num_requests; i++) {
1074 BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
1075 }
1076
1077 // Close the keep-alive connection
1078 server.DisconnectAllClients();
1079
1080 workers.Stop();
1081
1082 server.InterruptNet();
1083 server.JoinSocketsThreads();
1084 server.StopListening();
1085}
1086
1087BOOST_AUTO_TEST_CASE(http_server_rejects_disallowed_client_before_read)
1088{
1089 // DynSock reports accepted connections as coming from 5.5.5.5.
1090 gArgs.ForceSetArg("-rpcallowip", "4.4.4.4");
1091
1092 std::atomic_bool request_dispatched{false};
1093 HTTPServer server{[&request_dispatched](std::unique_ptr<HTTPRequest>&&) {
1094 request_dispatched = true;
1095 }};
1096 BOOST_REQUIRE(server.InitHTTPAllowList());
1097
1098 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
1099 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
1100 server.StartSocketsThreads();
1101
1102 // Queue a complete request; the server should never read from it.
1103 std::shared_ptr<DynSock::Pipes> client_pipes{
1104 ConnectClient(std::as_bytes(std::span(full_request)))};
1105
1106 // Wait for the socket to close with an EOF (bytes_read == 0)
1107 // 'bytes_read > 0' means the server replied to the prohibited client
1108 // 'bytes_read < 0' is an error, expected until the connection is fully processed by the I/O loop
1109 ssize_t bytes_read{};
1110 char buf[0x10000]{};
1111 for (int attempts{0}; attempts != 1'000; ++attempts) {
1112 bytes_read = client_pipes->send.GetBytes(&buf, sizeof(buf), MSG_PEEK);
1113 if (bytes_read >= 0) break;
1114 std::this_thread::sleep_for(10ms);
1115 }
1116
1117 BOOST_CHECK_EQUAL(bytes_read, 0);
1118 BOOST_CHECK(!request_dispatched);
1119 BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
1120
1121 server.InterruptNet();
1122 server.JoinSocketsThreads();
1123 server.StopListening();
1124
1125 // 'recv' buffer still holds the client's request untouched which
1126 // proves the server never called Recv()
1127 const ssize_t recv_bytes{client_pipes->recv.GetBytes(&buf, sizeof(buf))};
1128 BOOST_REQUIRE_EQUAL(recv_bytes, static_cast<ssize_t>(full_request.size()));
1129 BOOST_CHECK_EQUAL(std::string_view(buf, recv_bytes), full_request);
1130}
1131
1132BOOST_AUTO_TEST_SUITE_END()
A combination of a network address (CNetAddr) and a (TCP) port.
Definition: netaddress.h:530
Helper to initialize the global NodeClock, let a duration elapse, and reset it after use in a test.
Definition: time.h:54
BOOST_CHECK_EXCEPTION predicates to check the specific validation error.
Definition: common.h:19
bool Read(util::LineReader &reader, bool write=true)
Definition: httpserver.cpp:301
void Write(std::string &&key, std::string &&value)
Definition: httpserver.cpp:288
std::optional< std::string > FindFirst(std::string_view key) const
Definition: httpserver.cpp:267
std::string GetURI() const
Definition: httpserver.h:200
HTTPRequestMethod m_method
Definition: httpserver.h:162
bool LoadHeaders(LineReader &reader)
Definition: httpserver.cpp:428
std::pair< bool, std::string > GetHeader(std::string_view hdr) const
Definition: httpserver.cpp:696
bool LoadControlData(LineReader &reader)
Methods that attempt to parse HTTP request fields line-by-line from a receive buffer.
Definition: httpserver.cpp:380
HTTPRequestMethod GetRequestMethod() const
Definition: httpserver.h:202
bool LoadBody(LineReader &reader)
Definition: httpserver.cpp:433
HTTPStatusCode m_status
Definition: httpserver.h:151
std::string StringifyHeaders() const
Definition: httpserver.cpp:370
bool InitHTTPAllowList()
Parse the user's -rpcallowip settings and populate m_allow_subnets.
Definition: httpserver.cpp:90
size_t Remaining() const
Returns remaining size of bytes in buffer.
Definition: string.cpp:68
BOOST_AUTO_TEST_CASE(http_response_tests)
std::string_view excessive_headers
BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE)
BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value")
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"})
util::LineReader reader
constexpr std::string_view full_request
HTTPHeaders headers
std::unique_ptr< ProxyClient< messages::FooInterface > > client
std::optional< std::string > GetQueryParameterFromUri(const std::string_view uri, const std::string_view key)
Definition: httpserver.cpp:671
constexpr uint64_t MAX_BODY_SIZE
Maximum size of an HTTP request body.
Definition: httpserver.h:84
constexpr size_t MAX_HEADERS_SIZE
Maximum size of each headers line in an HTTP request, also the maximum size of all headers total.
Definition: httpserver.h:81
std::string ToString(const T &t)
Locale-independent version of std::to_string.
Definition: string.h:249
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.
Definition: netbase.cpp:191
std::function< std::unique_ptr< Sock >(int, int, int)> CreateSock
Socket factory.
Definition: netbase.cpp:577
#define BOOST_CHECK(expr)
Definition: object.cpp:16
@ HTTP_OK
Definition: protocol.h:12
Thrown when a request body exceeds MAX_BODY_SIZE (or will exceed, in chunked transfer) so the server ...
Definition: httpserver.h:88
uint8_t major
Default HTTP protocol version 1.1 is used by error responses when a request is unreadable.
Definition: httpserver.h:140
#define LOCK(cs)
Definition: sync.h:268