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