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>
9#include <util/string.h>
10
11#include <boost/test/unit_test.hpp>
12
22
23// HTTP request captured from bitcoin-cli
24constexpr std::string_view full_request = "POST / HTTP/1.1\r\n"
25 "Host: 127.0.0.1\r\n"
26 "Connection: close\r\n"
27 "Content-Type: application/json\r\n"
28 "Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
29 "Content-Length: 46\r\n"
30 "\r\n"
31 R"({"method":"getblockcount","params":[],"id":1})""\n";
32
33BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
34
35BOOST_AUTO_TEST_CASE(test_query_parameters)
36{
37 std::string uri {};
38
39 // Tolerate a URI with invalid characters (% not followed by hex digits)
40 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
41 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
42 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");
43
44 // No parameters
45 uri = "localhost:8080/rest/headers/someresource.json";
46 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
47
48 // Single parameter
49 uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
50 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
51 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));
52
53 // Multiple parameters
54 uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
55 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
56 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");
57
58 // If the query string contains duplicate keys, the first value is returned
59 uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
60 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
61
62 // Invalid query string syntax is the same as not having parameters
63 uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
64 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
65
66 // Multiple parameters, some characters encoded
67 uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
68 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
69 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");
70
71 // Encoded query delimiters are part of the parameter value, not structure.
72 uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
73 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
74 BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");
75
76 // An encoded question mark in the path does not introduce a query section.
77 uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
78 BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
79}
80
81BOOST_AUTO_TEST_CASE(http_headers_tests)
82{
83 {
84 // Writing response headers
85 HTTPHeaders headers{};
86 BOOST_CHECK(!headers.FindFirst("Cache-Control"));
87 headers.Write("Cache-Control", "no-cache");
88 // Check case-insensitive key matching
89 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
90 BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
91 // Additional values are appended, compared case-insensitive
92 headers.Write("cache-control", "max-age=60");
93 BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
94 BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
95 // Add a few more
96 headers.Write("Pie", "apple");
97 headers.Write("Sandwich", "ham");
98 headers.Write("Coffee", "black");
99 BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
100 // Remove
101 headers.RemoveAll("Pie");
102 BOOST_CHECK(!headers.FindFirst("Pie"));
103 // Combine for transmission
104 std::string headers_string{headers.Stringify()};
105 BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
106 "cache-control: max-age=60\r\n"
107 "Sandwich: ham\r\n"
108 "Coffee: black\r\n"
109 "\r\n");
110 }
111 {
112 // Reading request headers captured from bitcoin-cli
113 constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
114 "Connection: close\r\n"
115 "Content-Type: application/json\r\n"
116 "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
117 "Content-Length: 46\r\n";
118 util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
119 HTTPHeaders headers{};
120 headers.Read(reader);
121 BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
122 BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
123 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
124 BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
125 BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
126 BOOST_CHECK(!headers.FindFirst("Pizza"));
127 }
128 // Ensure invalid headers are rejected
129 {
130 // missing a colon
131 util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
132 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
133 }
134 {
135 // missing a key
136 util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
137 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
138 }
139 {
140 // contains NUL
141 util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
142 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
143 }
144 {
145 // contains bare \r (not followed by \n)
146 util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
147 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
148 }
149 {
150 // contains odd \r preceding the expected CRLF
151 util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
152 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
153 }
154 {
155 // key contains whitespace
156 util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
157 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
158 }
159 {
160 // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
161 std::string lines;
162 lines.reserve(820 * 10);
163 for (int i = 0; i < 820; ++i) {
164 lines.append("key:value\n");
165 }
166 std::string_view excessive_headers{lines};
169 BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
170 }
171 {
172 // Ok
173 util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
177 }
178}
179
180BOOST_AUTO_TEST_CASE(http_response_tests)
181{
182 // Typical HTTP 1.1 response headers
184 headers.Write("Content-Length", "41");
185
186 // Response points to headers which already exist because some of them
187 // are set before we even know what the response will be.
188 HTTPResponse res;
189 res.m_version = {.major = 1, .minor = 1};
190 res.m_status = HTTP_OK;
191 res.m_headers = std::move(headers);
193 res.StringifyHeaders(),
194 "HTTP/1.1 200 OK\r\n"
195 "Content-Length: 41\r\n"
196 "\r\n");
197}
198
199BOOST_AUTO_TEST_CASE(http_request_tests)
200{
201 {
202 HTTPRequest req;
209 BOOST_CHECK_EQUAL(req.m_target, "/");
210 BOOST_CHECK_EQUAL(req.GetURI(), "/");
213 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
214 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
215 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
216 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
217 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
218 BOOST_CHECK_EQUAL(req.m_body.size(), 46);
219 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
220 }
221 {
222 // Malformed: no spaces between data
223 HTTPRequest req;
224 LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
225 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
226 }
227 {
228 // Malformed: too many spaces
229 HTTPRequest req;
230 LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
231 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
232 }
233 {
234 // Malformed: slash missing before version
235 HTTPRequest req;
236 LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
237 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
238 }
239 {
240 // Malformed: no decimal in version
241 HTTPRequest req;
242 LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
243 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
244 }
245 {
246 // Malformed: version is not a number
247 HTTPRequest req;
248 LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
249 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
250 }
251 {
252 // Malformed: version is out of range
253 HTTPRequest req;
254 LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
255 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
256 }
257 {
258 // Malformed: version is out of range
259 HTTPRequest req;
260 LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
261 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
262 }
263 {
264 // Malformed: version is out of range
265 HTTPRequest req;
266 LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
267 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
268 }
269 {
270 // Malformed: version is not exactly two integers and a dot
271 HTTPRequest req;
272 LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
273 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
274 }
275 {
276 // Malformed: contains NUL
277 HTTPRequest req;
278 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};
279 BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
280 }
281 {
282 // Malformed: differing Content-Length values, case insensitive
283 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
284 "Host: 127.0.0.1\n"
285 "Content-Length: 8\n"
286 "content-length: 9\n\n"
287 "12345678";
288 HTTPRequest req;
289 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
290 BOOST_CHECK(req.LoadControlData(reader));
291 BOOST_CHECK(req.LoadHeaders(reader));
292 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
293 }
294 {
295 // Ok: multiple same Content-Length values
296 constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
297 "Host: 127.0.0.1\n"
298 "Content-Length: 8\n"
299 "content-length: 8\n\n"
300 "12345678";
301 HTTPRequest req;
302 util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
303 BOOST_CHECK(req.LoadControlData(reader));
304 BOOST_CHECK(req.LoadHeaders(reader));
305 BOOST_CHECK(req.LoadBody(reader));
306 }
307 {
308 // Ok
309 HTTPRequest req;
310 LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
311 BOOST_CHECK(req.LoadControlData(reader));
312 BOOST_CHECK(req.LoadHeaders(reader));
313 BOOST_CHECK(req.LoadBody(reader));
314 BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::GET);
315 BOOST_CHECK_EQUAL(req.m_target, "/");
316 BOOST_CHECK_EQUAL(req.m_version.major, 1);
317 BOOST_CHECK_EQUAL(req.m_version.minor, 0);
318 BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
319 // no body is OK
320 BOOST_CHECK_EQUAL(req.m_body.size(), 0);
321 }
322 {
323 // Malformed: missing colon
324 HTTPRequest req;
325 LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
326 BOOST_CHECK(req.LoadControlData(reader));
327 BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
328 }
329 {
330 // We might not have received enough data from the client which is not
331 // an error. We return false so the caller can try again later when the
332 // buffer has more data.
333 HTTPRequest req;
334 LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
337 }
338 {
339 // No Content-Length: body is not read
340 HTTPRequest req;
341 LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
345 // Don't try to read request body if Content-Length is missing
346 BOOST_CHECK_EQUAL(req.m_body.size(), 0);
347 }
348 {
349 // Malformed: Content-Length is not a number
350 HTTPRequest req;
351 LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
354 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
355 }
356 {
357 // Malformed: Content-Length is negative
358 HTTPRequest req;
359 LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
362 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
363 }
364 {
365 // Content-Length exceeds limit
366 constexpr auto excessive_size{MAX_BODY_SIZE + 1};
367 std::string huge_body(excessive_size, 'x');
368 const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
369 HTTPRequest req;
374 }
375 {
376 // Content-Length exactly on the limit
377 std::string max_body(MAX_BODY_SIZE, 'x');
378 const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
379 HTTPRequest req;
384 }
385 {
386 // Content-Length indicates more data than we have in the buffer.
387 // Not an error; we wait for more data before completing the body.
388 HTTPRequest req;
389 LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
393 }
394 {
395 // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
396 HTTPRequest req;
397 std::string_view ok_chunked = "GET / HTTP/1.0\n"
398 "Transfer-Encoding: chunked\n"
399 "\n"
400 "10\n"
401 R"({"method":"getbl)""\n"
402 " a \n"
403 R"(ockcount"})""\n"
404 "0\n"
405 "\n";
406 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
407 BOOST_CHECK(req.LoadControlData(reader));
408 BOOST_CHECK(req.LoadHeaders(reader));
409 BOOST_CHECK(req.LoadBody(reader));
410 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
411 }
412 {
413 // Prevent "chunked" transfer from exceeding size limit
414 HTTPRequest req;
415 std::string_view excessive_chunk_size = "GET / HTTP/1.0\n"
416 "Transfer-Encoding: chunked\n"
417 "\n"
418 "10\n"
419 R"({"method":"getbl)""\n"
420 "20000000\n"
421 R"(ockcount"})""\n"
422 "0\n"
423 "\n";
424 LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
425 BOOST_CHECK(req.LoadControlData(reader));
426 BOOST_CHECK(req.LoadHeaders(reader));
427 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
428 }
429 {
430 // Allow (but ignore) Chunk Extensions
431 HTTPRequest req;
432 std::string_view ok_chunked = "GET / HTTP/1.0\n"
433 "Transfer-Encoding: chunked\n"
434 "\n"
435 "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
436 R"({"method":"getbl)""\n"
437 " a ; compressed\n"
438 R"(ockcount"})""\n"
439 "0;why;would;anyone;do;this;\n"
440 "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
441 "\n";
442 LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
443 BOOST_CHECK(req.LoadControlData(reader));
444 BOOST_CHECK(req.LoadHeaders(reader));
445 BOOST_CHECK(req.LoadBody(reader));
446 BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
447 // Chunk Trailer was cleared
449 }
450 {
451 // Invalid "chunked" transfer, using roman numerals instead of hex for chunk length
453 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
454 "Transfer-Encoding: chunked\n"
455 "\n"
456 "XVI\n"
457 R"({"method":"getbl)""\n"
458 "X\n"
459 R"(ockcount"})""\n"
460 "0\n"
461 "\n";
462 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
463 BOOST_CHECK(req.LoadControlData(reader));
464 BOOST_CHECK(req.LoadHeaders(reader));
465 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
466 }
467 {
468 // Invalid "chunked" transfer, missing chunk termination \n
469 HTTPRequest req;
470 std::string_view invalid_chunked = "GET / HTTP/1.0\n"
471 "Transfer-Encoding: chunked\n"
472 "\n"
473 "10\n"
474 R"({"method":"getbl)"
475 "a\n" // interpreted as extra data at the end of `0x10`-sized chunk
476 R"(ockcount"})"
477 "0\n"
478 "\n";
479 LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
482 BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
483 }
484 {
485 // End of buffer reached without chunk termination, caller must wait for more data to arrive
486 HTTPRequest req;
487 std::string delayed_chunked = "GET / HTTP/1.0\n"
488 "Transfer-Encoding: chunked\n"
489 "\n"
490 "10\n"
491 R"({"method":"getbl)""\n"
492 "a\n"
493 R"(ockcount"})";
494 LineReader reader1(delayed_chunked, MAX_HEADERS_SIZE);
495 BOOST_CHECK(req.LoadControlData(reader1));
496 BOOST_CHECK(req.LoadHeaders(reader1));
497 BOOST_CHECK(!req.LoadBody(reader1));
498 // more data arrives!
499 delayed_chunked += "\n0\n\n";
500 LineReader reader2(delayed_chunked, MAX_HEADERS_SIZE);
501 BOOST_CHECK(req.LoadControlData(reader2));
502 BOOST_CHECK(req.LoadHeaders(reader2));
503 BOOST_CHECK(req.LoadBody(reader2));
504 }
505}
506
507BOOST_AUTO_TEST_CASE(http_server_socket_tests)
508{
509 // Hard code the timestamp for the Date header in the HTTP response
510 // Wed Dec 11 00:47:09 2024 UTC
511 SetMockTime(1733878029);
512
513 // Prepare a request handler that just stores received requests so we can examine them.
514 // Mutex is required to prevent a race between this test's main thread and the server's I/O loop.
515 Mutex requests_mutex;
516 std::deque<std::unique_ptr<HTTPRequest>> requests;
517 auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
518 LOCK(requests_mutex);
519 requests.push_back(std::move(req));
520 };
521
522 HTTPServer server{StoreRequest};
523
524 {
525 // We can only bind to NET_IPV4 and NET_IPV6
526 CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
527 auto result{server.BindAndStartListening(onion_address)};
528 BOOST_REQUIRE(!result);
529 BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
530 }
531
532 // This VALID address won't actually get used because we stubbed CreateSock()
533 CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
534
535 // Init state
536 BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
537 // Bind to mock Listening Socket
538 BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
539 // We are bound and listening
540 BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
541
542 // Start the I/O loop
543 server.StartSocketsThreads();
544
545 // No connections yet
546 BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
547
548 // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.
549 // Keep a handle for the mock client's send and receive pipes so we can examine
550 // the data it "receives".
551 std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};
552
553 // Wait up to a minute to find and connect the client in the I/O loop
554 int attempts{6000};
555 while (server.GetConnectionsCount() < 1) {
556 std::this_thread::sleep_for(10ms);
557 BOOST_REQUIRE(--attempts > 0);
558 }
559
560 // Prepare a pointer to the client, we'll assign it from the request itself.
561 std::shared_ptr<HTTPRemoteClient> client;
562
563 // Wait up to a minute to read the request from the client.
564 // Given that the mock client is itself a mock socket
565 // with hard-coded data it should only take a fraction of that.
566 attempts = 6000;
567 while (true) {
568 {
569 LOCK(requests_mutex);
570 // Connected client should have one request already from the static content.
571 if (requests.size() == 1) {
572 // Check the received request
573 BOOST_CHECK_EQUAL(requests.front()->m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
574 BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
575
576 // Inspect the connection pointed to from the request
577 client = requests.front()->m_client;
578 BOOST_CHECK_EQUAL(client->m_origin, "5.5.5.5:6789");
579
580 // Respond to request
581 requests.front()->WriteReply(HTTP_OK, "874140\n");
582
583 break;
584 }
585 }
586 std::this_thread::sleep_for(10ms);
587 BOOST_REQUIRE(--attempts > 0);
588 }
589
590 // Check the sent response from the mock client at the other end of the mock socket
591 std::string actual;
592 // Wait up to one minute for all the bytes to appear in the "send" pipe.
593 char buf[0x10000] = {};
594 attempts = 6000;
595 while (attempts > 0)
596 {
597 ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
598 if (bytes_read > 0) {
599 actual.append(buf, bytes_read);
600 if (actual.length() == 146) {
601 break;
602 }
603 }
604 std::this_thread::sleep_for(10ms);
605 --attempts;
606 }
607 BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
608 BOOST_CHECK(actual.ends_with("\r\n874140\n"));
609 // Headers can be sorted in any order, and will be, since we use unordered_map
610 BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
611 BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
612 BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
613 BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);
614
615 // Wait up to one minute for connection to be automatically closed, because
616 // keep-alive was not set by the client and we are done responding to their request.
617 attempts = 6000;
618 while (server.GetConnectionsCount() != 0) {
619 std::this_thread::sleep_for(10ms);
620 BOOST_REQUIRE(--attempts > 0);
621 }
622
623 // Stop the I/O loop and shutdown
624 server.InterruptNet();
625 // Wait for I/O loop to finish, after all connected sockets are closed
626 server.JoinSocketsThreads();
627 // Close all listening sockets
628 server.StopListening();
629}
630
631BOOST_AUTO_TEST_SUITE_END()
A combination of a network address (CNetAddr) and a (TCP) port.
Definition: netaddress.h:530
BOOST_CHECK_EXCEPTION predicates to check the specific validation error.
Definition: common.h:19
bool Read(util::LineReader &reader)
Definition: httpserver.cpp:300
void Write(std::string &&key, std::string &&value)
Definition: httpserver.cpp:287
std::optional< std::string > FindFirst(std::string_view key) const
Definition: httpserver.cpp:266
std::string GetURI() const
Definition: httpserver.h:191
HTTPRequestMethod m_method
Definition: httpserver.h:153
bool LoadHeaders(LineReader &reader)
Definition: httpserver.cpp:415
bool LoadControlData(LineReader &reader)
Methods that attempt to parse HTTP request fields line-by-line from a receive buffer.
Definition: httpserver.cpp:367
HTTPRequestMethod GetRequestMethod() const
Definition: httpserver.h:193
bool LoadBody(LineReader &reader)
Definition: httpserver.cpp:420
HTTPStatusCode m_status
Definition: httpserver.h:142
std::string StringifyHeaders() const
Definition: httpserver.cpp:357
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::optional< std::string > GetQueryParameterFromUri(const std::string_view uri, const std::string_view key)
Definition: httpserver.cpp:636
constexpr uint64_t MAX_BODY_SIZE
Maximum size of an HTTP request body.
Definition: httpserver.h:80
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:77
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
#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:84
uint8_t major
Default HTTP protocol version 1.1 is used by error responses when a request is unreadable.
Definition: httpserver.h:131
size_t Remaining() const
Returns remaining size of bytes in buffer.
Definition: string.cpp:69
#define LOCK(cs)
Definition: sync.h:268
void SetMockTime(int64_t nMockTimeIn)
DEPRECATED Use SetMockTime with chrono type.
Definition: time.cpp:52