Branch data Line data Source code
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>
9 : : #include <test/util/setup_common.h>
10 : : #include <util/string.h>
11 : : #include <util/threadpool.h>
12 : :
13 : : #include <boost/test/unit_test.hpp>
14 : :
15 : : using http_bitcoin::GetQueryParameterFromUri;
16 : : using http_bitcoin::HTTPHeaders;
17 : : using http_bitcoin::HTTPRemoteClient;
18 : : using http_bitcoin::HTTPRequest;
19 : : using http_bitcoin::HTTPResponse;
20 : : using http_bitcoin::HTTPServer;
21 : : using http_bitcoin::MAX_BODY_SIZE;
22 : : using http_bitcoin::MAX_HEADERS_SIZE;
23 : : using util::LineReader;
24 : :
25 : : // HTTP request captured from bitcoin-cli
26 : : constexpr 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 : :
35 : : BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
36 : :
37 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(test_query_parameters)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
38 : : {
39 [ + - ]: 1 : std::string uri {};
40 : :
41 : : // Tolerate a URI with invalid characters (% not followed by hex digits)
42 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
43 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
+ - + - ]
44 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");
+ - + - ]
45 : :
46 : : // No parameters
47 [ + - ]: 1 : uri = "localhost:8080/rest/headers/someresource.json";
48 [ + - - + : 2 : BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
+ - + - +
- ]
49 : :
50 : : // Single parameter
51 [ + - ]: 1 : uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
52 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
+ - + - ]
53 [ + - - + : 2 : BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));
+ - + - +
- ]
54 : :
55 : : // Multiple parameters
56 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
57 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
+ - + - ]
58 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");
+ - + - ]
59 : :
60 : : // If the query string contains duplicate keys, the first value is returned
61 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
62 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
+ - + - ]
63 : :
64 : : // Invalid query string syntax is the same as not having parameters
65 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
66 [ + - - + : 2 : BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
+ - + - +
- ]
67 : :
68 : : // Multiple parameters, some characters encoded
69 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
70 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
+ - + - ]
71 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");
+ - + - ]
72 : :
73 : : // Encoded query delimiters are part of the parameter value, not structure.
74 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
75 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
+ - + - ]
76 [ + - - + : 1 : BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");
+ - + - ]
77 : :
78 : : // An encoded question mark in the path does not introduce a query section.
79 [ + - ]: 1 : uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
80 [ + - - + : 2 : BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
+ - + - ]
81 : 1 : }
82 : :
83 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_headers_tests)
+ - + - -
+ + - + -
+ - + - +
- + - + -
- + + - +
- + - + -
+ - + - +
- - + + -
+ - + - +
- + - + -
+ - + - -
+ + - + -
+ - + - +
- + - + -
- + + - +
- ]
84 : : {
85 : 1 : {
86 : : // Writing response headers
87 : 1 : HTTPHeaders headers{};
88 [ + - + - : 2 : BOOST_CHECK(!headers.FindFirst("Cache-Control"));
+ - + - ]
89 [ + - + - : 2 : headers.Write("Cache-Control", "no-cache");
+ - ]
90 : : // Check case-insensitive key matching
91 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
+ - ]
92 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
+ - ]
93 : : // Additional values are appended, compared case-insensitive
94 [ + - + - : 2 : headers.Write("cache-control", "max-age=60");
+ - ]
95 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
+ - ]
96 [ + - + - : 2 : BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
+ - + - +
- ]
97 : : // Add a few more
98 [ + - + - : 2 : headers.Write("Pie", "apple");
+ - ]
99 [ + - + - : 2 : headers.Write("Sandwich", "ham");
+ - ]
100 [ + - + - : 2 : headers.Write("Coffee", "black");
+ - ]
101 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
+ - ]
102 : : // Remove
103 [ + - ]: 1 : headers.RemoveAll("Pie");
104 [ + - + - : 2 : BOOST_CHECK(!headers.FindFirst("Pie"));
+ - + - ]
105 : : // Combine for transmission
106 [ + - ]: 1 : std::string headers_string{headers.Stringify()};
107 [ + - + - ]: 1 : 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 : 1 : }
113 : 1 : {
114 : : // Reading request headers captured from bitcoin-cli
115 : 1 : 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 : 1 : util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
121 : 1 : HTTPHeaders headers{};
122 [ + - ]: 1 : headers.Read(reader);
123 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
+ - ]
124 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
+ - ]
125 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
+ - ]
126 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
+ - ]
127 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
+ - ]
128 [ + - + - : 2 : BOOST_CHECK(!headers.FindFirst("Pizza"));
+ - ]
129 : 0 : }
130 : : // Ensure invalid headers are rejected
131 : 1 : {
132 : : // missing a colon
133 : 1 : util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
134 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
- - - - -
+ + - + -
+ - ]
135 : : }
136 : 1 : {
137 : : // missing a key
138 : 1 : util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
139 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
- - - - -
+ + - + -
+ - ]
140 : : }
141 : 1 : {
142 : : // contains NUL
143 : 1 : util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
144 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
- - - - -
+ + - + -
+ - ]
145 : : }
146 : 1 : {
147 : : // contains bare \r (not followed by \n)
148 : 1 : util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
149 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
- - - - -
+ + - + -
+ - ]
150 : : }
151 : 1 : {
152 : : // contains odd \r preceding the expected CRLF
153 : 1 : util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
154 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
- - - - -
+ + - + -
+ - ]
155 : : }
156 : 1 : {
157 : : // key contains whitespace
158 : 1 : util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
159 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
- - - - -
+ + - + -
+ - ]
160 : : }
161 : 1 : {
162 : : // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
163 [ + - ]: 1 : std::string lines;
164 [ + - ]: 1 : lines.reserve(820 * 10);
165 [ + + ]: 821 : for (int i = 0; i < 820; ++i) {
166 [ + - ]: 820 : lines.append("key:value\n");
167 : : }
168 [ - + ]: 1 : std::string_view excessive_headers{lines};
169 [ + - + - ]: 1 : BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE);
170 [ + - ]: 1 : util::LineReader reader{excessive_headers, /*max_line_length=*/MAX_HEADERS_SIZE};
171 [ + - - + : 3 : BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
- - - - -
+ + - + -
+ - ]
172 : 0 : }
173 : 1 : {
174 : : // Ok
175 : 1 : util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
176 : 1 : HTTPHeaders headers{};
177 [ + - ]: 1 : headers.Read(reader);
178 [ + - + - : 1 : BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value");
+ - ]
179 : 1 : }
180 : 1 : }
181 : :
182 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_response_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
183 : : {
184 : : // Typical HTTP 1.1 response headers
185 : 1 : HTTPHeaders headers{};
186 [ + - + - : 2 : 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 : 1 : HTTPResponse res;
191 : 1 : res.m_version = {.major = 1, .minor = 1};
192 : 1 : res.m_status = HTTP_OK;
193 : 1 : res.m_headers = std::move(headers);
194 [ + - + - : 1 : BOOST_CHECK_EQUAL(
+ - ]
195 : : res.StringifyHeaders(),
196 : : "HTTP/1.1 200 OK\r\n"
197 : : "Content-Length: 41\r\n"
198 : : "\r\n");
199 : 1 : }
200 : :
201 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_request_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
202 : : {
203 : 1 : {
204 : 1 : HTTPRequest req;
205 [ + - ]: 1 : LineReader reader(full_request, MAX_HEADERS_SIZE);
206 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
207 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
208 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - + - ]
209 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::POST);
210 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::POST);
211 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_target, "/");
212 [ + - - + : 2 : BOOST_CHECK_EQUAL(req.GetURI(), "/");
+ - ]
213 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_version.major, 1);
214 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_version.minor, 1);
215 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
+ - ]
216 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
+ - ]
217 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
+ - ]
218 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
+ - ]
219 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
+ - ]
220 [ + - - + : 1 : BOOST_CHECK_EQUAL(req.m_body.size(), 46);
+ - ]
221 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
222 : 1 : }
223 : 1 : {
224 : : // Malformed: no spaces between data
225 : 1 : HTTPRequest req;
226 [ + - ]: 1 : LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
227 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
- - - - -
+ + - + -
+ - ]
228 : 1 : }
229 : 1 : {
230 : : // Malformed: too many spaces
231 : 1 : HTTPRequest req;
232 [ + - ]: 1 : LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
233 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
- - - - -
+ + - + -
+ - ]
234 : 1 : }
235 : 1 : {
236 : : // Malformed: slash missing before version
237 : 1 : HTTPRequest req;
238 [ + - ]: 1 : LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
239 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
- - - - -
+ + - + -
+ - ]
240 : 1 : }
241 : 1 : {
242 : : // Malformed: no decimal in version
243 : 1 : HTTPRequest req;
244 [ + - ]: 1 : LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
245 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
- - - - -
+ + - + -
+ - ]
246 : 1 : }
247 : 1 : {
248 : : // Malformed: version is not a number
249 : 1 : HTTPRequest req;
250 [ + - ]: 1 : LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
251 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
- - - - -
+ + - + -
+ - ]
252 : 1 : }
253 : 1 : {
254 : : // Malformed: version is out of range
255 : 1 : HTTPRequest req;
256 [ + - ]: 1 : LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
257 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
- - - - -
+ + - + -
+ - ]
258 : 1 : }
259 : 1 : {
260 : : // Malformed: version is out of range
261 : 1 : HTTPRequest req;
262 [ + - ]: 1 : LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
263 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
- - - - -
+ + - + -
+ - ]
264 : 1 : }
265 : 1 : {
266 : : // Malformed: version is out of range
267 : 1 : HTTPRequest req;
268 [ + - ]: 1 : LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
269 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
- - - - -
+ + - + -
+ - ]
270 : 1 : }
271 : 1 : {
272 : : // Malformed: version is not exactly two integers and a dot
273 : 1 : HTTPRequest req;
274 [ + - ]: 1 : LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
275 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
- - - - -
+ + - + -
+ - ]
276 : 1 : }
277 : 1 : {
278 : : // Malformed: contains NUL
279 : 1 : HTTPRequest req;
280 [ + - ]: 1 : 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 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
- - - - -
+ + - + -
+ - ]
282 : 1 : }
283 : 1 : {
284 : : // Malformed: differing Content-Length values, case insensitive
285 : 1 : 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 : 1 : HTTPRequest req;
291 [ + - ]: 1 : util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
292 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
293 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
294 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
- - - - -
+ + - + -
+ - ]
295 : 1 : }
296 : 1 : {
297 : : // Ok: multiple same Content-Length values
298 : 1 : 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 : 1 : HTTPRequest req;
304 [ + - ]: 1 : util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
305 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
306 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
307 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - ]
308 : 1 : }
309 : 1 : {
310 : : // Ok
311 : 1 : HTTPRequest req;
312 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
313 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
314 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
315 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - + - ]
316 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::GET);
317 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_target, "/");
318 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_version.major, 1);
319 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_version.minor, 0);
320 [ + - + - : 1 : BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
+ - ]
321 : : // no body is OK
322 [ + - - + : 1 : BOOST_CHECK_EQUAL(req.m_body.size(), 0);
+ - ]
323 : 1 : }
324 : 1 : {
325 : : // Malformed: missing colon
326 : 1 : HTTPRequest req;
327 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
328 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
329 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
- - - - -
+ + - + -
+ - ]
330 : 1 : }
331 : 1 : {
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 : 1 : HTTPRequest req;
336 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
337 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
338 [ + - + - : 2 : BOOST_CHECK(!req.LoadHeaders(reader));
+ - ]
339 : 1 : }
340 : 1 : {
341 : : // No Content-Length: body is not read
342 : 1 : HTTPRequest req;
343 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
344 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
345 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
346 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - + - ]
347 : : // Don't try to read request body if Content-Length is missing
348 [ + - - + : 1 : BOOST_CHECK_EQUAL(req.m_body.size(), 0);
+ - ]
349 : 1 : }
350 : 1 : {
351 : : // Malformed: Content-Length is not a number
352 : 1 : HTTPRequest req;
353 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
354 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
355 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
356 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
- - - - -
+ + - + -
+ - ]
357 : 1 : }
358 : 1 : {
359 : : // Malformed: Content-Length is negative
360 : 1 : HTTPRequest req;
361 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
362 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
363 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
364 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
- - - - -
+ + - + -
+ - ]
365 : 1 : }
366 : 1 : {
367 : : // Content-Length exceeds limit
368 : 1 : constexpr auto excessive_size{MAX_BODY_SIZE + 1};
369 [ + - ]: 1 : std::string huge_body(excessive_size, 'x');
370 [ + - + - : 3 : const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
+ - ]
371 : 1 : HTTPRequest req;
372 [ - + + - ]: 1 : LineReader reader(request, MAX_HEADERS_SIZE);
373 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
374 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
375 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Max body size exceeded"});
- - - - -
+ + - + -
+ - + - ]
376 : 1 : }
377 : 1 : {
378 : : // Content-Length exactly on the limit
379 [ + - ]: 1 : std::string max_body(MAX_BODY_SIZE, 'x');
380 [ + - + - : 3 : 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 : 1 : HTTPRequest req;
382 [ - + + - ]: 1 : LineReader reader(request, MAX_HEADERS_SIZE);
383 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
384 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
385 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - ]
386 : 1 : }
387 : 1 : {
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 : 1 : HTTPRequest req;
391 [ + - ]: 1 : LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
392 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
393 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
394 [ + - + - : 2 : BOOST_CHECK(!req.LoadBody(reader));
+ - ]
395 : 1 : }
396 : 1 : {
397 : : // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
398 : 1 : HTTPRequest req;
399 : 1 : 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 [ + - ]: 1 : LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
409 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
410 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
411 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - + - ]
412 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
413 : 1 : }
414 : 1 : {
415 : : // Prevent "chunked" transfer from exceeding size limit
416 : 1 : HTTPRequest req;
417 : 1 : 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 [ + - ]: 1 : LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
427 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
428 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
429 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
- - - - -
+ + - + -
+ - + - ]
430 : 1 : }
431 : 1 : {
432 : : // Allow (but ignore) Chunk Extensions
433 : 1 : HTTPRequest req;
434 : 1 : 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 [ + - ]: 1 : LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
445 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
446 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
447 [ + - + - : 2 : BOOST_CHECK(req.LoadBody(reader));
+ - + - ]
448 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
449 : : // Chunk Trailer was parsed, but ignored
450 [ + - + - : 1 : BOOST_CHECK_EQUAL(reader.Remaining(), 0);
+ - ]
451 [ + - + - : 2 : BOOST_CHECK(!req.GetHeader("Expires").first);
+ - ]
452 : 1 : }
453 : 1 : {
454 : : // Invalid "chunked" transfer, using roman numerals instead of hex for chunk length
455 : 1 : HTTPRequest req;
456 : 1 : 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 [ + - ]: 1 : LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
466 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
467 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
468 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
- - - - -
+ + - + -
+ - ]
469 : 1 : }
470 : 1 : {
471 : : // Invalid "chunked" transfer, missing chunk termination \n
472 : 1 : HTTPRequest req;
473 : 1 : 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 [ + - ]: 1 : LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
483 [ + - + - : 2 : BOOST_CHECK(req.LoadControlData(reader));
+ - + - ]
484 [ + - + - : 2 : BOOST_CHECK(req.LoadHeaders(reader));
+ - + - ]
485 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
- - - - -
+ + - + -
+ - ]
486 : 1 : }
487 : 1 : }
488 : :
489 [ + - + - : 7 : BOOST_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 : 11 : class DummyClient : public HTTPRemoteClient
493 : : {
494 : : public:
495 [ + - + - ]: 10 : DummyClient() : HTTPRemoteClient{/*id=*/0, /*addr=*/CService(), /*socket=*/CreateSock(0, 0, 0)} {}
496 : :
497 : 1664 : void receive(std::string_view s)
498 : : {
499 [ - + ]: 1664 : m_recv_buffer.insert(
500 [ - + ]: 1664 : m_recv_buffer.end(),
501 : : s.begin(),
502 : : s.end());
503 : 1664 : }
504 : : };
505 : :
506 : 1 : {
507 : : // Step through state machine
508 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
509 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
510 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
511 : :
512 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n");
513 [ + - ]: 1 : client->ReadRequest(*client->m_req);
514 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
515 : :
516 [ + - ]: 1 : client->receive("Host: 127.0.0.1\n"
517 : : "Content-Length: 10\n\n");
518 [ + - ]: 1 : client->ReadRequest(*client->m_req);
519 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
520 : :
521 [ + - ]: 1 : client->receive("I miss you\n");
522 [ + - ]: 1 : client->ReadRequest(*client->m_req);
523 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
+ - ]
524 : 0 : }
525 : 1 : {
526 : : // Read body over multiple data pushes, multiple requests in same push
527 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
528 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
529 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
530 : :
531 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
532 : : "Host: 127.0.0.1\n"
533 : : "Content-Length: 10\n\n"
534 : : "I miss");
535 [ + - ]: 1 : client->ReadRequest(*client->m_req);
536 : : // Because of the Content-Length header we know the body is not complete
537 [ + - + - ]: 1 : 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 [ + - ]: 1 : client->receive(" you"
541 : : "GET /endpoint HTTP/1.0\n\n");
542 [ + - ]: 1 : client->ReadRequest(*client->m_req);
543 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
544 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_body, "I miss you");
545 : : // Next request sitting in buffer
546 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24);
+ - ]
547 : : // Complete first request hasn't been moved yet, expect no-op
548 [ + - ]: 1 : client->ReadRequest(*client->m_req);
549 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 24);
+ - ]
550 : :
551 : : // Reset m_req
552 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
553 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
554 : :
555 : : // Read second request
556 [ + - ]: 1 : client->ReadRequest(*client->m_req);
557 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
558 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_target, "/endpoint");
559 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0);
+ - ]
560 : : // Buffer is cleared
561 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0);
+ - + - ]
562 : 0 : }
563 : 1 : {
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 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
568 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
569 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
570 : :
571 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
572 : : "Content-Length: 30000\n\n");
573 [ + - ]: 1 : client->ReadRequest(*client->m_req);
574 [ + - + - ]: 1 : 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 [ + + ]: 4 : for (int i = 1; i <= 3; ++i) {
579 [ + - - + : 3 : client->receive(std::string(10000, 'x'));
+ - ]
580 [ + - - + : 3 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 10000);
+ - ]
581 [ + - ]: 3 : client->ReadRequest(*client->m_req);
582 [ + - - + : 3 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 10000 * i);
+ - ]
583 [ + - - + : 3 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 0);
+ - ]
584 : : }
585 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
+ - ]
586 : 0 : }
587 : 1 : {
588 : : // A body sent in the same push as the next request is split correctly
589 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
590 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
591 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
592 : :
593 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
594 : : "Content-Length: 4\n\n"
595 : : "body"
596 : : "GET /next HTTP/1.0\n\n");
597 [ + - ]: 1 : client->ReadRequest(*client->m_req);
598 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
599 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_body, "body");
600 : : // Only the second request is left over
601 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 20);
+ - + - ]
602 : 0 : }
603 : 1 : {
604 : : // Chunked transfer with state
605 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
606 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
607 : :
608 [ + - + - : 2 : BOOST_CHECK(!client->m_req->m_chunk_size);
+ - ]
609 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0);
610 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 0);
+ - ]
611 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
612 : :
613 : : // First chunk is incomplete
614 [ + - ]: 1 : client->receive("GET / HTTP/1.0\n"
615 : : "Transfer-Encoding: chunked\n"
616 : : "\n"
617 : : "10\n"
618 : : R"({"method)");
619 [ + - ]: 1 : client->ReadRequest(*client->m_req);
620 [ + - + - : 2 : BOOST_CHECK(client->m_req->m_chunk_size);
+ - ]
621 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 16);
622 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 8);
623 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 8);
+ - ]
624 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
625 : :
626 : : // More data arrives, chunk is completed.
627 [ + - ]: 1 : client->receive(R"(":"getbl)""\n");
628 [ + - ]: 1 : client->ReadRequest(*client->m_req);
629 : : // State is reset
630 [ + - + - : 2 : BOOST_CHECK(!client->m_req->m_chunk_size);
+ - ]
631 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 0);
632 : : // New data is added to body but body is still incomplete
633 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 16);
+ - ]
634 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
635 : :
636 : : // Next chunk arrives without terminal CRLF
637 [ + - ]: 1 : client->receive("a\n"
638 : : R"(ockcount"})");
639 [ + - ]: 1 : client->ReadRequest(*client->m_req);
640 [ + - + - : 2 : BOOST_CHECK(client->m_req->m_chunk_size);
+ - ]
641 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*client->m_req->m_chunk_size, 10);
642 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->m_chunk_read, 10);
643 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26);
+ - ]
644 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
645 : :
646 : : // Chunk terminal CRLF arrives with final (size 0) chunk
647 [ + - ]: 1 : client->receive("\n0\n\n");
648 [ + - ]: 1 : client->ReadRequest(*client->m_req);
649 : : // Body size hasn't changed
650 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body.size(), 26);
+ - ]
651 : : // We're done
652 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
653 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body, R"({"method":"getblockcount"})");
+ - ]
654 : 0 : }
655 : 1 : {
656 : : // Invalid headers: error state stops reading
657 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
658 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
659 : :
660 : : // Request is in the buffer
661 [ + - ]: 1 : 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 [ + - + - : 2 : BOOST_CHECK(!client->m_recv_buffer.empty());
+ - ]
667 : :
668 : : // Reading throws an error, sets state
669 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
- - - - -
+ + - + -
+ - ]
670 : : std::runtime_error,
671 : : HasReason{"HTTP header missing colon (:)"});
672 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
673 : :
674 : : // We read up to the invalid line
675 [ + - + - : 1 : 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 [ + - + - : 2 : 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 [ + - ]: 1 : client->receive("Content-Length: 2\n\nok");
681 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21);
+ - ]
682 [ + - ]: 1 : client->ReadRequest(*client->m_req);
683 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->m_recv_buffer.size(), 21);
+ - + - ]
684 : 0 : }
685 : 1 : {
686 : : // Headers sent in batches that are below MAX_HEADERS_SIZE but the total is excessive
687 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
688 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
689 [ + - ]: 1 : client->ReadRequest(*client->m_req);
690 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
691 : :
692 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n");
693 [ + - ]: 1 : client->ReadRequest(*client->m_req);
694 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
695 : :
696 [ + + ]: 411 : for (int i = 0; i < 410; ++i) {
697 [ + - ]: 410 : client->receive("key:value\n");
698 : : }
699 [ + - ]: 1 : client->ReadRequest(*client->m_req);
700 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
701 : :
702 [ + + ]: 410 : for (int i = 0; i < 409; ++i) {
703 [ + - ]: 409 : client->receive("key:value\n");
704 : : }
705 [ + - ]: 1 : client->ReadRequest(*client->m_req);
706 [ + - + - ]: 1 : 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 [ + - ]: 1 : client->receive("k:\n");
711 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
- - - - -
+ + - + -
+ - ]
712 : : std::runtime_error,
713 : : HasReason{"HTTP headers exceed size limit"});
714 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
+ - ]
715 : 0 : }
716 : 1 : {
717 : : // Client sends chunks that are below the limit but the total is excessive
718 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
719 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
720 [ + - ]: 1 : client->ReadRequest(*client->m_req);
721 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Init);
722 : :
723 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n");
724 [ + - ]: 1 : client->ReadRequest(*client->m_req);
725 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsHeaders);
726 : :
727 [ + - ]: 1 : client->receive("Transfer-Encoding: chunked\n\n");
728 [ + - ]: 1 : client->ReadRequest(*client->m_req);
729 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
730 : :
731 : : // Send 16-byte chunk
732 [ + - ]: 1 : client->receive("10\nno auto updates!\n");
733 [ + - ]: 1 : client->ReadRequest(*client->m_req);
734 [ + - + - ]: 1 : 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 [ + - ]: 1 : client->receive("1fffff1\n");
740 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
- - - - -
+ + - + -
+ - + - ]
741 : : http_bitcoin::ContentTooLargeError,
742 : : HasReason{"Chunk will exceed max body size"});
743 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
+ - ]
744 : 0 : }
745 : 1 : {
746 : : // Ensure chunk trailer is parsed over state lines
747 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
748 [ + - ]: 1 : 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 [ + - ]: 1 : 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 [ + - ]: 1 : client->ReadRequest(*client->m_req);
759 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
760 : :
761 : : // Send first part of another trailer line
762 [ + - ]: 1 : client->receive("Expires:");
763 [ + - ]: 1 : client->ReadRequest(*client->m_req);
764 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
765 : :
766 : : // Finish the trailer line
767 [ + - ]: 1 : client->receive("never\n");
768 [ + - ]: 1 : client->ReadRequest(*client->m_req);
769 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::NeedsBody);
770 : :
771 : : // Terminate
772 [ + - ]: 1 : client->receive("\n");
773 [ + - ]: 1 : client->ReadRequest(*client->m_req);
774 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Complete);
775 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->m_body, "x");
+ - ]
776 : 0 : }
777 : 1 : {
778 : : // Ensure chunk trailer counts towards the headers size limit
779 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
780 [ + - ]: 1 : client->m_req = std::make_unique<HTTPRequest>(client);
781 : :
782 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n"
783 : : "Transfer-Encoding: chunked\n"); // 27 bytes
784 [ + + ]: 817 : for (int i = 0; i < 816; ++i) {
785 [ + - ]: 816 : client->receive("key:value\n"); // 8160
786 : : }
787 [ + - ]: 1 : client->receive("\n" // 1
788 : : "1\n"
789 : : "x\n"
790 : : "0\n");
791 [ + - ]: 1 : client->ReadRequest(*client->m_req);
792 [ + - + - ]: 1 : 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 [ + - ]: 1 : client->receive("k:vv\n");
797 [ + - - + : 2 : BOOST_CHECK_EXCEPTION(client->ReadRequest(*client->m_req),
- - - - -
+ + - + -
+ - ]
798 : : std::runtime_error,
799 : : HasReason{"HTTP headers exceed size limit"});
800 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->m_req->GetState(), HTTPRequest::State::Error);
+ - ]
801 : 1 : }
802 : 1 : }
803 : :
804 [ + - + - : 7 : BOOST_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 : 1 : 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 : 1 : Mutex requests_mutex;
813 [ + - ]: 1 : std::deque<std::unique_ptr<HTTPRequest>> requests;
814 : 2 : auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
815 : 1 : LOCK(requests_mutex);
816 [ + - + - ]: 1 : requests.push_back(std::move(req));
817 : 2 : };
818 : :
819 [ + - ]: 1 : HTTPServer server{StoreRequest};
820 [ + - ]: 1 : server.InitHTTPAllowList();
821 : :
822 : 1 : {
823 : : // We can only bind to NET_IPV4 and NET_IPV6
824 [ + - + - : 2 : CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
825 [ + - ]: 1 : auto result{server.BindAndStartListening(onion_address)};
826 [ + - + - : 2 : BOOST_REQUIRE(!result);
+ - ]
827 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
828 : 1 : }
829 : :
830 : : // This VALID address won't actually get used because we stubbed CreateSock()
831 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
832 : :
833 : : // Init state
834 [ + - - + : 1 : BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
+ - ]
835 : : // Bind to mock Listening Socket
836 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
837 : : // We are bound and listening
838 [ + - - + : 1 : BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
+ - ]
839 : :
840 : : // Start the I/O loop
841 [ + - ]: 1 : server.StartSocketsThreads();
842 : :
843 : : // No connections yet
844 [ + - + - ]: 1 : 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 [ + - ]: 1 : 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 : 2 : while (server.GetConnectionsCount() < 1) {
854 [ + - ]: 1 : std::this_thread::sleep_for(10ms);
855 [ + - + - : 3 : BOOST_REQUIRE(--attempts > 0);
+ + ]
856 : : }
857 : :
858 : : // Prepare a pointer to the client, we'll assign it from the request itself.
859 : 1 : 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 : 1 : attempts = 6000;
865 : 0 : while (true) {
866 : 1 : {
867 [ + - ]: 1 : LOCK(requests_mutex);
868 : : // Connected client should have one request already from the static content.
869 [ - + + - ]: 1 : if (requests.size() == 1) {
870 : : // Check the received request
871 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(requests.front()->m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
872 [ + - + - : 1 : BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
+ - + - ]
873 : :
874 : : // Inspect the connection pointed to from the request
875 [ - + ]: 1 : client = requests.front()->m_client.lock();
876 [ + - + - : 2 : BOOST_REQUIRE(client);
+ - ]
877 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->m_origin, "5.5.5.5:6789");
878 : :
879 : : // Respond to request
880 [ + - ]: 1 : requests.front()->WriteReply(HTTP_OK, "874140\n");
881 : :
882 [ + - ]: 1 : break;
883 : : }
884 : 0 : }
885 [ # # ]: 0 : std::this_thread::sleep_for(10ms);
886 [ # # # # ]: 0 : BOOST_REQUIRE(--attempts > 0);
887 : : }
888 : :
889 : : // Check the sent response from the mock client at the other end of the mock socket
890 : 1 : std::string actual;
891 : : // Wait up to one minute for all the bytes to appear in the "send" pipe.
892 : 1 : char buf[0x10000] = {};
893 : 1 : attempts = 6000;
894 [ + - ]: 1 : while (attempts > 0)
895 : : {
896 [ + - ]: 1 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
897 [ + - ]: 1 : if (bytes_read > 0) {
898 [ + - ]: 1 : actual.append(buf, bytes_read);
899 [ - + - + ]: 1 : if (actual.length() == 146) {
900 : : break;
901 : : }
902 : : }
903 [ # # ]: 0 : std::this_thread::sleep_for(10ms);
904 : 0 : --attempts;
905 : : }
906 [ + - - + : 2 : BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
+ - + - ]
907 [ + - - + : 2 : 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 [ + - + - : 2 : BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
+ - ]
910 [ + - + - : 2 : BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
+ - ]
911 [ + - + - : 2 : BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
+ - ]
912 [ + - + - ]: 2 : 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 : 1 : attempts = 6000;
917 : 1 : while (server.GetConnectionsCount() != 0) {
918 [ + - ]: 5 : std::this_thread::sleep_for(10ms);
919 [ + - + - : 11 : BOOST_REQUIRE(--attempts > 0);
+ + ]
920 : : }
921 : :
922 : : // Stop the I/O loop and shutdown
923 [ + - ]: 1 : server.InterruptNet();
924 : : // Wait for I/O loop to finish, after all connected sockets are closed
925 [ + - ]: 1 : server.JoinSocketsThreads();
926 : : // Close all listening sockets
927 [ + - ]: 1 : server.StopListening();
928 [ + - + - ]: 3 : }
929 : :
930 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_socket_error_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
931 : : {
932 : : // Create a tiny threadpool for the HTTPRequest handler
933 [ + - ]: 1 : ThreadPool workers("http");
934 [ + - ]: 1 : 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 : 1 : std::atomic<int> height{0};
939 [ + - ]: 4 : HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
940 [ + - - - ]: 3 : auto item = [req, &height]() {
941 : 3 : const int h = height.fetch_add(1);
942 [ - + + - ]: 3 : req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
943 : 3 : };
944 : : // Can't call BOOST_REQUIRE from worker thread
945 [ - + - + ]: 3 : Assert(workers.Submit(std::move(item)));
946 [ + - ]: 4 : }};
947 [ + - ]: 1 : server.InitHTTPAllowList();
948 : :
949 : : // All replies will be the same size
950 : 1 : 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 : :
959 : : /**
960 : : * A mocked Sock derived from DynSock whose Send() only succeeds when there is more than
961 : : * one reply being sent (send buffer length > reply_length). Otherwise it returns
962 : : * a recoverable error (WSAEAGAIN).
963 : : *
964 : : * After it sends successfully once, it continues to always succeed.
965 : : *
966 : : * Useful for testing "try again" logic around non-blocking socket Send() failures.
967 : : */
968 : 1 : class ErrorSock : public DynSock
969 : : {
970 : : public:
971 [ + - ]: 2 : explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
972 : 0 : DynSock& operator=(Sock&&) override { assert(false); return *this; }
973 : :
974 : 27 : ssize_t Send(const void* buf, size_t len, int flags) const override
975 : : {
976 [ + + + + ]: 27 : if (len <= reply_length && !m_have_sent) {
977 : : #ifdef WIN32
978 : : WSASetLastError(WSAEWOULDBLOCK);
979 : : #else
980 : 25 : errno = WSAEAGAIN;
981 : : #endif
982 : 25 : return -1;
983 : : } else {
984 : 2 : m_have_sent = true;
985 : 2 : 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 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
994 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
995 [ + - ]: 1 : server.StartSocketsThreads();
996 : :
997 : : // Prepare initial requests
998 : 1 : int num_requests = 2;
999 : : // Use keep-alive so the server holds the connection open for all requests.
1000 [ + - ]: 1 : std::string keepalive_request{full_request};
1001 [ + - ]: 1 : 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 : 1 : std::string all_requests;
1004 [ + + ]: 3 : for (int i = 0; i < num_requests; i++) {
1005 [ - + ]: 4 : 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 [ + - ]: 1 : DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
1013 : 1 : [&](const std::string* s) {
1014 : : return true;
1015 [ + - ]: 1 : }};
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 [ + - ]: 1 : DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
1019 : 1 : [&](const std::string* s) {
1020 : : return true;
1021 [ + - ]: 1 : }};
1022 : :
1023 : : // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
1024 : 1 : std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
1025 [ + - ]: 1 : ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
1026 [ - + + - ]: 1 : };
1027 : :
1028 : : // Wait up to one minute for the last reply from the server
1029 : 1 : std::string actual;
1030 : 1 : char buf[0x10000] = {};
1031 : 1 : int attempts = 6000;
1032 [ + - ]: 7 : while (attempts > 0)
1033 : : {
1034 [ + - ]: 7 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1035 [ + + ]: 7 : if (bytes_read > 0) {
1036 [ + - ]: 1 : actual.append(buf, bytes_read);
1037 [ + - - + ]: 2 : if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1038 : : break;
1039 : : }
1040 : : }
1041 [ + - ]: 6 : std::this_thread::sleep_for(10ms);
1042 : 6 : --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 [ + - ]: 1 : std::this_thread::sleep_for(1000ms);
1054 [ - + + - ]: 1 : mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
1055 : 2 : num_requests++;
1056 : :
1057 : : // Wait up to one minute for reply
1058 : : attempts = 6000;
1059 [ + - ]: 2 : while (attempts > 0)
1060 : : {
1061 [ + - ]: 2 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1062 [ + + ]: 2 : if (bytes_read > 0) {
1063 [ + - ]: 1 : actual.append(buf, bytes_read);
1064 [ + - - + ]: 2 : if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1065 : : break;
1066 : : }
1067 : : }
1068 [ + - ]: 1 : std::this_thread::sleep_for(10ms);
1069 : 1 : --attempts;
1070 : : }
1071 : :
1072 : : // All replies were received
1073 [ + + ]: 4 : for (int i = 0; i < num_requests; i++) {
1074 [ + - + - : 9 : BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
+ - ]
1075 : : }
1076 : :
1077 : : // Close the keep-alive connection
1078 [ + - ]: 1 : server.DisconnectAllClients();
1079 : :
1080 [ + - ]: 1 : workers.Stop();
1081 : :
1082 [ + - ]: 1 : server.InterruptNet();
1083 [ + - ]: 1 : server.JoinSocketsThreads();
1084 [ + - ]: 1 : server.StopListening();
1085 [ + - ]: 2 : }
1086 : :
1087 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_server_rejects_disallowed_client_before_read)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
1088 : : {
1089 : : // DynSock reports accepted connections as coming from 5.5.5.5.
1090 [ + - + - ]: 2 : gArgs.ForceSetArg("-rpcallowip", "4.4.4.4");
1091 : :
1092 : 1 : std::atomic_bool request_dispatched{false};
1093 [ + - ]: 1 : HTTPServer server{[&request_dispatched](std::unique_ptr<HTTPRequest>&&) {
1094 : 0 : request_dispatched = true;
1095 [ + - ]: 1 : }};
1096 [ + - + - : 2 : BOOST_REQUIRE(server.InitHTTPAllowList());
+ - + - ]
1097 : :
1098 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
1099 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
1100 [ + - ]: 1 : server.StartSocketsThreads();
1101 : :
1102 : : // Queue a complete request; the server should never read from it.
1103 : 1 : std::shared_ptr<DynSock::Pipes> client_pipes{
1104 [ + - ]: 1 : 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 : 1 : ssize_t bytes_read{};
1110 : 1 : char buf[0x10000]{};
1111 [ + - ]: 3 : for (int attempts{0}; attempts != 1'000; ++attempts) {
1112 [ + - ]: 3 : bytes_read = client_pipes->send.GetBytes(&buf, sizeof(buf), MSG_PEEK);
1113 [ + + ]: 3 : if (bytes_read >= 0) break;
1114 [ + - ]: 2 : std::this_thread::sleep_for(10ms);
1115 : : }
1116 : :
1117 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(bytes_read, 0);
1118 [ + - + - : 2 : BOOST_CHECK(!request_dispatched);
+ - ]
1119 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
1120 : :
1121 [ + - ]: 1 : server.InterruptNet();
1122 [ + - ]: 1 : server.JoinSocketsThreads();
1123 [ + - ]: 1 : server.StopListening();
1124 : :
1125 : : // 'recv' buffer still holds the client's request untouched which
1126 : : // proves the server never called Recv()
1127 [ + - ]: 1 : const ssize_t recv_bytes{client_pipes->recv.GetBytes(&buf, sizeof(buf))};
1128 [ + - + - ]: 1 : BOOST_REQUIRE_EQUAL(recv_bytes, static_cast<ssize_t>(full_request.size()));
1129 [ + - + - : 1 : BOOST_CHECK_EQUAL(std::string_view(buf, recv_bytes), full_request);
+ - ]
1130 : 1 : }
1131 : :
1132 : : BOOST_AUTO_TEST_SUITE_END()
|