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