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 : :
493 : 1 : {
494 : : // Step through state machine
495 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
496 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
497 : :
498 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n");
499 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
500 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
501 : :
502 [ + - ]: 1 : client->receive("Host: 127.0.0.1\n"
503 : : "Content-Length: 10\n\n");
504 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
505 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
506 : :
507 [ + - ]: 1 : client->receive("I miss you\n");
508 [ + - + - ]: 2 : auto req{HTTPRemoteClient::TryReadRequest(client)};
509 [ + - + - : 2 : BOOST_REQUIRE(req);
+ - ]
510 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
511 [ + - ]: 1 : }
512 : 1 : {
513 : : // Read body over multiple data pushes, multiple requests in same push
514 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
515 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
516 : :
517 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
518 : : "Host: 127.0.0.1\n"
519 : : "Content-Length: 10\n\n"
520 : : "I miss");
521 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
522 : : // Because of the Content-Length header we know the body is not complete
523 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
524 : :
525 : : // Finish sending first request and include second request in the same buffer
526 [ + - ]: 1 : client->receive(" you"
527 : : "GET /endpoint HTTP/1.0\n\n");
528 [ + - + - ]: 2 : auto req{HTTPRemoteClient::TryReadRequest(client)};
529 [ + - + - : 2 : BOOST_REQUIRE(req);
+ - ]
530 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
531 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->GetURI(), "/");
+ - ]
532 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
533 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody(), "I miss you");
+ - ]
534 [ + - ]: 1 : req->WriteReply(HTTP_OK, ""); // Mark client as no longer busy
535 : : // Next request sitting in buffer
536 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 24);
+ - ]
537 : :
538 : : // Read second request
539 [ + - + - : 3 : req = HTTPRemoteClient::TryReadRequest(client);
+ - ]
540 [ + - + - : 2 : BOOST_REQUIRE(req);
+ - ]
541 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
542 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
543 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->GetURI(), "/endpoint");
+ - ]
544 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody().size(), 0);
- + + - ]
545 : : // Buffer is cleared
546 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 0);
+ - ]
547 [ + - ]: 1 : }
548 : 1 : {
549 : : // A Content-Length body is drained out of the receive buffer as it
550 : : // arrives, instead of accumulating there until the request is complete.
551 : :
552 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
553 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
554 : :
555 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
556 : : "Content-Length: 30000\n\n");
557 [ + - + - : 2 : HTTPRemoteClient::TryReadRequest(client);
+ - ]
558 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
559 : :
560 : : // Body arrives in 10kB pieces. Each one is copied onto m_body and
561 : : // erased from the receive buffer, which never holds more than one piece.
562 [ + + ]: 4 : for (int i = 1; i <= 3; ++i) {
563 [ + - - + : 6 : client->receive(std::string(10000, 'x'));
+ - ]
564 [ + - - + : 3 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 10000);
+ - ]
565 [ + - + - ]: 6 : auto req{HTTPRemoteClient::TryReadRequest(client)};
566 [ + + ]: 3 : if (i < 3) {
567 [ + - + - : 4 : BOOST_CHECK(!req.get());
+ - ]
568 [ + - - + : 4 : BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 10000 * i);
- + + - ]
569 : : } else {
570 [ + - + - : 2 : BOOST_CHECK(req.get());
+ - ]
571 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody().size(), 10000 * i);
- + + - ]
572 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
573 [ + - + - ]: 2 : BOOST_CHECK(!client->GetRequest());
574 : : }
575 [ + - - + : 3 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 0);
+ - ]
576 : 3 : }
577 : 0 : }
578 : 1 : {
579 : : // A body sent in the same push as the next request is split correctly
580 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
581 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
582 : :
583 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
584 : : "Content-Length: 4\n\n"
585 : : "body"
586 : : "GET /next HTTP/1.0\n\n");
587 [ + - + - ]: 2 : auto req{HTTPRemoteClient::TryReadRequest(client)};
588 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
589 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody(), "body");
+ - ]
590 : : // Only the second request is left over
591 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 20);
+ - ]
592 [ + - ]: 1 : }
593 : 1 : {
594 : : // Chunked transfer with state
595 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
596 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
597 : :
598 : : // First chunk is incomplete
599 [ + - ]: 1 : client->receive("GET / HTTP/1.0\n"
600 : : "Transfer-Encoding: chunked\n"
601 : : "\n"
602 : : "10\n"
603 : : R"({"method)");
604 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
605 [ + - + - : 2 : BOOST_REQUIRE(client->GetRequest()->GetChunkSize());
+ - ]
606 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*client->GetRequest()->GetChunkSize(), 16);
607 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetChunkProgress(), 8);
608 [ + - - + : 2 : BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 8);
- + + - ]
609 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
610 : :
611 : : // More data arrives, chunk is completed.
612 [ + - ]: 1 : client->receive(R"(":"getbl)""\n");
613 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
614 : : // State is reset
615 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest()->GetChunkSize());
+ - ]
616 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetChunkProgress(), 0);
617 : : // New data is added to body but body is still incomplete
618 [ + - - + : 2 : BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 16);
- + + - ]
619 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
620 : :
621 : : // Next chunk arrives without terminal CRLF
622 [ + - ]: 1 : client->receive("a\n"
623 : : R"(ockcount"})");
624 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
625 [ + - + - : 2 : BOOST_CHECK(client->GetRequest()->GetChunkSize());
+ - ]
626 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*client->GetRequest()->GetChunkSize(), 10);
627 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetChunkProgress(), 10);
628 [ + - - + : 2 : BOOST_CHECK_EQUAL(client->GetRequest()->ReadBody().size(), 26);
- + + - ]
629 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
630 : :
631 : : // Chunk terminal CRLF arrives with final (size 0) chunk
632 [ + - ]: 1 : client->receive("\n0\n\n");
633 [ + - + - ]: 2 : auto req{HTTPRemoteClient::TryReadRequest(client)};
634 : : // Body size hasn't changed
635 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody().size(), 26);
- + + - ]
636 : : // We're done
637 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
638 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody(), R"({"method":"getblockcount"})");
+ - ]
639 [ + - ]: 1 : }
640 : 1 : {
641 : : // Invalid headers: error state stops reading
642 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
643 : :
644 : : // Request is in the buffer
645 [ + - ]: 1 : client->receive("POST / HTTP/1.0\n"
646 : : "Host: 127.0.0.1\n");
647 [ + - + - : 2 : BOOST_CHECK(!client->GetRecvBuffer().empty());
+ - ]
648 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
649 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
650 [ + - ]: 1 : client->receive("Invalid header with no colon\n"
651 : : "\n"
652 : : "body is not read");
653 : : // Reading throws an error, sets state
654 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
655 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Error);
656 : :
657 : : // We read up to the invalid line
658 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetHeader("Host"), "127.0.0.1");
+ - ]
659 : : // Buffer was cleared, client should just be disconnected now
660 [ + - + - : 2 : BOOST_CHECK(client->GetRecvBuffer().empty());
+ - ]
661 : :
662 : : // Even if more data comes in, trying to read again in error state is a no-op
663 [ + - ]: 1 : client->receive("Content-Length: 2\n\nok");
664 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 21);
+ - ]
665 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
666 [ + - - + : 1 : BOOST_CHECK_EQUAL(client->GetRecvBuffer().size(), 21);
+ - + - ]
667 : 0 : }
668 : 1 : {
669 : : // Headers sent in batches that are below MAX_HEADERS_SIZE but the total is excessive
670 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
671 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
672 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
673 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Init);
674 : :
675 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n");
676 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
677 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
678 : :
679 [ + + ]: 411 : for (int i = 0; i < 410; ++i) {
680 [ + - ]: 820 : client->receive("key:value\n");
681 : : }
682 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
683 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
684 : :
685 [ + + ]: 410 : for (int i = 0; i < 409; ++i) {
686 [ + - ]: 818 : client->receive("key:value\n");
687 : : }
688 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
689 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
690 : :
691 : : // We're at 819 x 10-byte headers
692 : : // The limit is 8192, three more bytes should throw.
693 [ + - ]: 1 : client->receive("k:\n");
694 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
695 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Error);
+ - ]
696 : 0 : }
697 : 1 : {
698 : : // Client sends chunks that are below the limit but the total is excessive
699 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
700 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
701 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Init);
702 : :
703 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n");
704 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
705 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsHeaders);
706 : :
707 [ + - ]: 1 : client->receive("Transfer-Encoding: chunked\n\n");
708 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
709 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
710 : :
711 : : // Send 16-byte chunk
712 [ + - ]: 1 : client->receive("10\nno auto updates!\n");
713 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
714 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
715 : :
716 : : // The next chunk will be of size 32MiB - 16 + 1, below the limit
717 : : // on its own but not if it were added to the total cumulative body so far.
718 : : // We don't need to actually send or prepare this amount of data.
719 [ + - ]: 1 : client->receive("1fffff1\n");
720 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
721 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Error);
+ - ]
722 : 0 : }
723 : 1 : {
724 : : // Ensure chunk trailer is parsed over state lines
725 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
726 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
727 : :
728 : : // Send a 1-byte chunk then send the 0-chunk with a trailer but no terminal CRLF
729 [ + - ]: 1 : client->receive("GET / HTTP/1.0\n"
730 : : "Transfer-Encoding: chunked\n"
731 : : "\n"
732 : : "1\n"
733 : : "x\n"
734 : : "0\n"
735 : : "Digest: sha-4=deadbeef\n");
736 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
737 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
738 : :
739 : : // Send first part of another trailer line
740 [ + - ]: 1 : client->receive("Expires:");
741 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
742 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
743 : :
744 : : // Finish the trailer line
745 [ + - ]: 1 : client->receive("never\n");
746 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
747 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
748 : :
749 : : // Terminate
750 [ + - ]: 1 : client->receive("\n");
751 [ + - + - ]: 2 : auto req{HTTPRemoteClient::TryReadRequest(client)};
752 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(req->GetState(), HTTPRequest::State::Complete);
753 [ + - - + : 2 : BOOST_CHECK_EQUAL(req->ReadBody(), "x");
+ - ]
754 [ + - ]: 1 : }
755 : 1 : {
756 : : // Ensure chunk trailer counts towards the headers size limit
757 : 1 : std::shared_ptr<DummyClient> client{std::make_shared<DummyClient>()};
758 [ + - + - : 2 : BOOST_CHECK(!client->GetRequest());
+ - ]
759 : :
760 [ + - ]: 1 : client->receive("POST /huge HTTP/1.0\n"
761 : : "Transfer-Encoding: chunked\n"); // 27 bytes
762 [ + + ]: 817 : for (int i = 0; i < 816; ++i) {
763 [ + - ]: 1632 : client->receive("key:value\n"); // 8160
764 : : }
765 [ + - ]: 1 : client->receive("\n" // 1
766 : : "1\n"
767 : : "x\n"
768 : : "0\n");
769 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
770 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::NeedsBody);
771 : :
772 : : // We're in the trailer section with a total of 8188 bytes of headers.
773 : : // The limit is 8192, five more bytes should throw.
774 [ + - ]: 1 : client->receive("k:vv\n");
775 [ + - + - : 4 : BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(client));
+ - + - +
- + - ]
776 [ + - + - : 1 : BOOST_CHECK_EQUAL(client->GetRequest()->GetState(), HTTPRequest::State::Error);
+ - ]
777 : 1 : }
778 : 1 : }
779 : :
780 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_server_socket_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
781 : : {
782 : : // Hard code the timestamp for the Date header in the HTTP response
783 : : // Wed Dec 11 00:47:09 2024 UTC
784 : 1 : FakeNodeClock clock{1733878029s};
785 : :
786 : : // Prepare a request handler that just stores received requests so we can examine them.
787 : : // Mutex is required to prevent a race between this test's main thread and the server's I/O loop.
788 : 1 : Mutex requests_mutex;
789 [ + - ]: 1 : std::deque<std::unique_ptr<HTTPRequest>> requests;
790 : 2 : auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
791 : 1 : LOCK(requests_mutex);
792 [ + - + - ]: 1 : requests.push_back(std::move(req));
793 : 2 : };
794 : :
795 [ + - ]: 1 : HTTPServer server{StoreRequest};
796 [ + - ]: 1 : server.InitHTTPAllowList();
797 : :
798 : 1 : {
799 : : // We can only bind to NET_IPV4 and NET_IPV6
800 [ + - + - : 2 : CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
801 [ + - ]: 1 : auto result{server.BindAndStartListening(onion_address)};
802 [ + - + - : 2 : BOOST_REQUIRE(!result);
+ - ]
803 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
804 : 1 : }
805 : :
806 : : // This VALID address won't actually get used because we stubbed CreateSock()
807 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
808 : :
809 : : // Init state
810 [ + - - + : 1 : BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
+ - ]
811 : : // Bind to mock Listening Socket
812 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
813 : : // We are bound and listening
814 [ + - - + : 1 : BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
+ - ]
815 : :
816 : : // Start the I/O loop
817 [ + - ]: 1 : server.StartSocketsThreads();
818 : :
819 : : // No connections yet
820 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
821 : :
822 : : // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.
823 : : // Keep a handle for the mock client's send and receive pipes so we can examine
824 : : // the data it "receives".
825 [ + - ]: 1 : std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};
826 : :
827 : : // Wait up to a minute to find and connect the client in the I/O loop
828 : : int attempts{6000};
829 : 2 : while (server.GetConnectionsCount() < 1) {
830 [ + - ]: 1 : std::this_thread::sleep_for(10ms);
831 [ + - + - : 3 : BOOST_REQUIRE(--attempts > 0);
+ + ]
832 : : }
833 : :
834 : : // Prepare a pointer to the client, we'll assign it from the request itself.
835 : 1 : std::shared_ptr<HTTPRemoteClient> client;
836 : :
837 : : // Wait up to a minute to read the request from the client.
838 : : // Given that the mock client is itself a mock socket
839 : : // with hard-coded data it should only take a fraction of that.
840 : 1 : attempts = 6000;
841 : 0 : while (true) {
842 : 1 : {
843 [ + - ]: 1 : LOCK(requests_mutex);
844 : : // Connected client should have one request already from the static content.
845 [ - + + - ]: 1 : if (requests.size() == 1) {
846 : : // Check the received request
847 [ + - - + : 2 : BOOST_CHECK_EQUAL(requests.front()->ReadBody(), R"({"method":"getblockcount","params":[],"id":1})""\n");
+ - ]
848 [ + - + - : 1 : BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
+ - + - ]
849 : :
850 : : // Inspect the connection pointed to from the request
851 [ - + ]: 1 : client = requests.front()->GetClient();
852 [ + - + - : 2 : BOOST_REQUIRE(client);
+ - ]
853 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(client->GetOrigin(), "5.5.5.5:6789");
854 : :
855 : : // Respond to request
856 [ + - ]: 1 : requests.front()->WriteReply(HTTP_OK, "874140\n");
857 : :
858 [ + - ]: 1 : break;
859 : : }
860 : 0 : }
861 [ # # ]: 0 : std::this_thread::sleep_for(10ms);
862 [ # # # # ]: 0 : BOOST_REQUIRE(--attempts > 0);
863 : : }
864 : :
865 : : // Check the sent response from the mock client at the other end of the mock socket
866 : 1 : std::string actual;
867 : : // Wait up to one minute for all the bytes to appear in the "send" pipe.
868 : 1 : char buf[0x10000] = {};
869 : 1 : attempts = 6000;
870 [ + - ]: 1 : while (attempts > 0)
871 : : {
872 [ + - ]: 1 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
873 [ + - ]: 1 : if (bytes_read > 0) {
874 [ + - ]: 1 : actual.append(buf, bytes_read);
875 [ - + - + ]: 1 : if (actual.length() == 146) {
876 : : break;
877 : : }
878 : : }
879 [ # # ]: 0 : std::this_thread::sleep_for(10ms);
880 : 0 : --attempts;
881 : : }
882 [ + - - + : 2 : BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
+ - + - ]
883 [ + - - + : 2 : BOOST_CHECK(actual.ends_with("\r\n874140\n"));
+ - + - ]
884 : : // Headers can be sorted in any order, and will be, since we use unordered_map
885 [ + - + - : 2 : BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
+ - ]
886 [ + - + - : 2 : BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
+ - ]
887 [ + - + - : 2 : BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
+ - ]
888 [ + - + - ]: 2 : BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);
889 : :
890 : : // Wait up to one minute for connection to be automatically closed, because
891 : : // keep-alive was not set by the client and we are done responding to their request.
892 : 1 : attempts = 6000;
893 : 1 : while (server.GetConnectionsCount() != 0) {
894 [ + - ]: 5 : std::this_thread::sleep_for(10ms);
895 [ + - + - : 11 : BOOST_REQUIRE(--attempts > 0);
+ + ]
896 : : }
897 : :
898 : : // Stop the I/O loop and shutdown
899 [ + - ]: 1 : server.InterruptNet();
900 : : // Wait for I/O loop to finish, after all connected sockets are closed
901 [ + - ]: 1 : server.JoinSocketsThreads();
902 : : // Close all listening sockets
903 [ + - ]: 1 : server.StopListening();
904 [ + - + - ]: 3 : }
905 : :
906 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_socket_error_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
907 : : {
908 : : // Create a tiny threadpool for the HTTPRequest handler
909 [ + - ]: 1 : ThreadPool workers("http");
910 [ + - ]: 1 : workers.Start(1);
911 : :
912 : : // Hard-code the server's request handler to respond to each request with
913 : : // an incremented block count. Handle the replies in the worker thread.
914 : 1 : std::atomic<int> height{0};
915 [ + - ]: 4 : HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
916 [ + - - - ]: 3 : auto item = [req, &height]() {
917 : 3 : const int h = height.fetch_add(1);
918 [ - + + - ]: 3 : req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
919 : 3 : };
920 : : // Can't call BOOST_REQUIRE from worker thread
921 [ - + - + ]: 3 : Assert(workers.Submit(std::move(item)));
922 [ + - ]: 4 : }};
923 [ + - ]: 1 : server.InitHTTPAllowList();
924 : :
925 : : // All replies will be the same size
926 : 1 : static constexpr std::size_t reply_length = std::string_view{
927 : : "HTTP/1.1 200 OK\r\n"
928 : : "Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n" // All RFC1123 dates are 29 characters
929 : : "Content-Length: 10\r\n"
930 : : "Content-Type: text/html; charset=ISO-8859-1\r\n"
931 : : "\r\n"
932 : : "height: 0\n"
933 : : }.size();
934 : :
935 : : /**
936 : : * A mocked Sock derived from DynSock whose Send() only succeeds when there is more than
937 : : * one reply being sent (send buffer length > reply_length). Otherwise it returns
938 : : * a recoverable error (WSAEAGAIN).
939 : : *
940 : : * After it sends successfully once, it continues to always succeed.
941 : : *
942 : : * Useful for testing "try again" logic around non-blocking socket Send() failures.
943 : : */
944 : 1 : class ErrorSock : public DynSock
945 : : {
946 : : public:
947 [ + - ]: 2 : explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
948 : 0 : DynSock& operator=(Sock&&) override { assert(false); return *this; }
949 : :
950 : 3 : ssize_t Send(const void* buf, size_t len, int flags) const override
951 : : {
952 [ + + + + ]: 3 : if (len <= reply_length && !m_have_sent) {
953 : : #ifdef WIN32
954 : : WSASetLastError(WSAEWOULDBLOCK);
955 : : #else
956 : 1 : errno = WSAEAGAIN;
957 : : #endif
958 : 1 : return -1;
959 : : } else {
960 : 2 : m_have_sent = true;
961 : 2 : return DynSock::Send(buf, len, flags);
962 : : }
963 : : }
964 : :
965 : : mutable bool m_have_sent{false};
966 : : };
967 : :
968 : : // Simpler server startup than the last test
969 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
970 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
971 [ + - ]: 1 : server.StartSocketsThreads();
972 : :
973 : : // Prepare initial requests
974 : 1 : int num_requests = 2;
975 : : // Use keep-alive so the server holds the connection open for all requests.
976 [ + - ]: 1 : std::string keepalive_request{full_request};
977 [ + - ]: 1 : keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
978 : : // Combine all requests so they are read from the socket on a single iteration of the I/O loop
979 : 1 : std::string all_requests;
980 [ + + ]: 3 : for (int i = 0; i < num_requests; i++) {
981 [ - + ]: 4 : all_requests += keepalive_request;
982 : : }
983 : :
984 : : // Watch the log messages to ensure that the first two replies were sent
985 : : // together. This indicates the non-optimistic send path was used
986 : : // because a reply was already sitting in the send buffer when a second reply
987 : : // was added.
988 [ + - ]: 1 : DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
989 : 1 : [&](const std::string* s) {
990 : : return true;
991 [ + - ]: 1 : }};
992 : : // Last reply should be sent on its own by optimistic send path, because
993 : : // the send buffer was empty when the reply was written.
994 [ + - ]: 1 : DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
995 : 1 : [&](const std::string* s) {
996 : : return true;
997 [ + - ]: 1 : }};
998 : :
999 : : // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
1000 : 1 : std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
1001 [ + - ]: 1 : ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
1002 [ - + + - ]: 1 : };
1003 : :
1004 : : // Wait up to one minute for the last reply from the server
1005 : 1 : std::string actual;
1006 : 1 : char buf[0x10000] = {};
1007 : 1 : int attempts = 6000;
1008 [ + - ]: 7 : while (attempts > 0)
1009 : : {
1010 [ + - ]: 7 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1011 [ + + ]: 7 : if (bytes_read > 0) {
1012 [ + - ]: 1 : actual.append(buf, bytes_read);
1013 [ + - - + ]: 2 : if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1014 : : break;
1015 : : }
1016 : : }
1017 [ + - ]: 6 : std::this_thread::sleep_for(10ms);
1018 : 6 : --attempts;
1019 : : }
1020 : :
1021 : : // Send the third request.
1022 : : // If there was a race between WriteReply() in the worker thread setting m_send_ready=true
1023 : : // and SocketHandlerConnected() in the I/O thread flushing the send buffer,
1024 : : // then the socket would be stuck in write mode with nothing to write,
1025 : : // the server would never read from the socket, and this request would time out.
1026 : : // Wait a second to ensure both the worker thread and I/O thread are idle.
1027 : : // If we send the next request too soon it might get accepted by the server before
1028 : : // it gets wedged shut.
1029 [ + - ]: 1 : std::this_thread::sleep_for(1000ms);
1030 [ - + + - ]: 1 : mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
1031 : 2 : num_requests++;
1032 : :
1033 : : // Wait up to one minute for reply
1034 : : attempts = 6000;
1035 [ + - ]: 2 : while (attempts > 0)
1036 : : {
1037 [ + - ]: 2 : ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
1038 [ + + ]: 2 : if (bytes_read > 0) {
1039 [ + - ]: 1 : actual.append(buf, bytes_read);
1040 [ + - - + ]: 2 : if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
1041 : : break;
1042 : : }
1043 : : }
1044 [ + - ]: 1 : std::this_thread::sleep_for(10ms);
1045 : 1 : --attempts;
1046 : : }
1047 : :
1048 : : // All replies were received
1049 [ + + ]: 4 : for (int i = 0; i < num_requests; i++) {
1050 [ + - + - : 9 : BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
+ - ]
1051 : : }
1052 : :
1053 : : // Close the keep-alive connection
1054 [ + - ]: 1 : server.DisconnectAllClients();
1055 : :
1056 [ + - ]: 1 : workers.Stop();
1057 : :
1058 [ + - ]: 1 : server.InterruptNet();
1059 [ + - ]: 1 : server.JoinSocketsThreads();
1060 [ + - ]: 1 : server.StopListening();
1061 [ + - ]: 2 : }
1062 : :
1063 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(http_server_rejects_disallowed_client_before_read)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
1064 : : {
1065 : : // DynSock reports accepted connections as coming from 5.5.5.5.
1066 [ + - + - ]: 2 : gArgs.ForceSetArg("-rpcallowip", "4.4.4.4");
1067 : :
1068 : 1 : std::atomic_bool request_dispatched{false};
1069 [ + - ]: 1 : HTTPServer server{[&request_dispatched](std::unique_ptr<HTTPRequest>&&) {
1070 : 0 : request_dispatched = true;
1071 [ + - ]: 1 : }};
1072 [ + - + - : 2 : BOOST_REQUIRE(server.InitHTTPAllowList());
+ - + - ]
1073 : :
1074 [ + - + - : 2 : CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ - ]
1075 [ + - + - : 2 : BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ - + - ]
1076 [ + - ]: 1 : server.StartSocketsThreads();
1077 : :
1078 : : // Queue a complete request; the server should never read from it.
1079 : 1 : std::shared_ptr<DynSock::Pipes> client_pipes{
1080 [ + - ]: 1 : ConnectClient(std::as_bytes(std::span(full_request)))};
1081 : :
1082 : : // Wait for the socket to close with an EOF (bytes_read == 0)
1083 : : // 'bytes_read > 0' means the server replied to the prohibited client
1084 : : // 'bytes_read < 0' is an error, expected until the connection is fully processed by the I/O loop
1085 : 1 : ssize_t bytes_read{};
1086 : 1 : char buf[0x10000]{};
1087 [ + - ]: 2 : for (int attempts{0}; attempts != 1'000; ++attempts) {
1088 [ + - ]: 2 : bytes_read = client_pipes->send.GetBytes(&buf, sizeof(buf), MSG_PEEK);
1089 [ + + ]: 2 : if (bytes_read >= 0) break;
1090 [ + - ]: 1 : std::this_thread::sleep_for(10ms);
1091 : : }
1092 : :
1093 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(bytes_read, 0);
1094 [ + - + - : 2 : BOOST_CHECK(!request_dispatched);
+ - ]
1095 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
1096 : :
1097 [ + - ]: 1 : server.InterruptNet();
1098 [ + - ]: 1 : server.JoinSocketsThreads();
1099 [ + - ]: 1 : server.StopListening();
1100 : :
1101 : : // 'recv' buffer still holds the client's request untouched which
1102 : : // proves the server never called Recv()
1103 [ + - ]: 1 : const ssize_t recv_bytes{client_pipes->recv.GetBytes(&buf, sizeof(buf))};
1104 [ + - + - ]: 1 : BOOST_REQUIRE_EQUAL(recv_bytes, static_cast<ssize_t>(full_request.size()));
1105 [ + - + - : 1 : BOOST_CHECK_EQUAL(std::string_view(buf, recv_bytes), full_request);
+ - ]
1106 : 1 : }
1107 : :
1108 : : BOOST_AUTO_TEST_SUITE_END()
|