-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathupdate_check_tests.cpp
More file actions
417 lines (353 loc) · 16.8 KB
/
Copy pathupdate_check_tests.cpp
File metadata and controls
417 lines (353 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// Copyright (c) 2014-2026 The Reddcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <node/update_check.h>
#include <vector>
#include <util/strencodings.h>
#include <test/util/setup_common.h>
#include <tinyformat.h>
#include <util/string.h>
#include <boost/test/unit_test.hpp>
#include <stdexcept>
#include <string>
using node::ChunkedDecoder;
using node::ExtractHttpBody;
using node::HttpHeaders;
using node::ParseHttpHeaders;
using node::HttpResponse;
using node::ParseHttpResponse;
using node::ResolveRedirect;
using node::Url;
namespace {
//! The body every positive case expects back, shaped like the response the
//! update check actually parses.
const std::string BODY{"{\"tag_name\":\"v4.22.9.4\"}"};
//! Chunk sizes are hexadecimal.
std::string ToHex(std::string::size_type n)
{
return strprintf("%x", n);
}
std::string WithLength(const std::string& body, const std::string& declared_length)
{
return "HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " +
declared_length + "\r\n\r\n" + body;
}
} // namespace
BOOST_FIXTURE_TEST_SUITE(update_check_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(complete_body_is_returned)
{
BOOST_CHECK_EQUAL(ExtractHttpBody(WithLength(BODY, ToString(BODY.size())), true), BODY);
// A truncated stream is fine when Content-Length accounts for every byte:
// plenty of servers close without a TLS shutdown.
BOOST_CHECK_EQUAL(ExtractHttpBody(WithLength(BODY, ToString(BODY.size())), false), BODY);
}
BOOST_AUTO_TEST_CASE(short_body_is_rejected)
{
// The regression this guards. Before the completeness check a body cut off
// mid-flight was returned as a successful result, so the caller saw a well
// formed but empty answer and reported no error at all: an upgrade notice
// that silently never appears.
const std::string cut{BODY.substr(0, BODY.size() - 5)};
const std::string response{WithLength(cut, ToString(BODY.size()))};
BOOST_CHECK_THROW(ExtractHttpBody(response, false), std::runtime_error);
// Clean shutdown or not, a short body is still short.
BOOST_CHECK_THROW(ExtractHttpBody(response, true), std::runtime_error);
// The error has to name the shortfall, otherwise a truncated download is
// indistinguishable from an unreachable server in the logs.
try {
ExtractHttpBody(response, false);
BOOST_ERROR("expected a throw");
} catch (const std::runtime_error& e) {
const std::string what{e.what()};
BOOST_CHECK(what.find("Incomplete response") != std::string::npos);
BOOST_CHECK(what.find(ToString(cut.size())) != std::string::npos);
BOOST_CHECK(what.find(ToString(BODY.size())) != std::string::npos);
}
}
BOOST_AUTO_TEST_CASE(overlong_body_is_rejected)
{
BOOST_CHECK_THROW(ExtractHttpBody(WithLength(BODY + "trailing", ToString(BODY.size())), true),
std::runtime_error);
}
BOOST_AUTO_TEST_CASE(unusable_content_length_is_rejected)
{
BOOST_CHECK_THROW(ExtractHttpBody(WithLength(BODY, "not-a-number"), true), std::runtime_error);
BOOST_CHECK_THROW(ExtractHttpBody(WithLength(BODY, "-1"), true), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(connection_close_framing_needs_a_clean_shutdown)
{
// No Content-Length, so the body runs until the connection closes and only
// a clean shutdown proves all of it arrived.
const std::string response{"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n" + BODY};
BOOST_CHECK_EQUAL(ExtractHttpBody(response, true), BODY);
BOOST_CHECK_THROW(ExtractHttpBody(response, false), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(chunked_body_is_reassembled)
{
// api.github.com answers with Content-Length most of the time and switches
// to chunked intermittently, so both have to work. Before this was decoded
// the chunk sizes were handed back as part of the body, the JSON parse
// failed, and the update check reported nothing at all.
const std::string head{"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"};
// One chunk holding the whole body.
const std::string single{head + ToHex(BODY.size()) + "\r\n" + BODY + "\r\n0\r\n\r\n"};
BOOST_CHECK_EQUAL(ExtractHttpBody(single, true), BODY);
// Split across chunks, which is the shape a real server sends.
const std::string first{BODY.substr(0, 8)};
const std::string rest{BODY.substr(8)};
const std::string split{head + ToHex(first.size()) + "\r\n" + first + "\r\n" +
ToHex(rest.size()) + "\r\n" + rest + "\r\n0\r\n\r\n"};
BOOST_CHECK_EQUAL(ExtractHttpBody(split, true), BODY);
// Chunk extensions are ignored, and trailers after the final chunk too.
const std::string extras{head + ToHex(BODY.size()) + ";name=value\r\n" + BODY +
"\r\n0\r\nX-Trailer: ignored\r\n\r\n"};
BOOST_CHECK_EQUAL(ExtractHttpBody(extras, true), BODY);
// Case of the header value must not decide whether it is decoded.
const std::string upper{"HTTP/1.1 200 OK\r\nTransfer-Encoding: CHUNKED\r\n\r\n" +
ToHex(BODY.size()) + "\r\n" + BODY + "\r\n0\r\n\r\n"};
BOOST_CHECK_EQUAL(ExtractHttpBody(upper, true), BODY);
}
BOOST_AUTO_TEST_CASE(incomplete_chunked_body_is_rejected)
{
const std::string head{"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"};
// The terminating zero length chunk never arrived, so the body is short
// even though everything received parsed cleanly.
const std::string no_last_chunk{head + ToHex(BODY.size()) + "\r\n" + BODY + "\r\n"};
BOOST_CHECK_THROW(ExtractHttpBody(no_last_chunk, true), std::runtime_error);
// Cut off partway through a chunk's data.
const std::string mid_chunk{head + ToHex(BODY.size()) + "\r\n" + BODY.substr(0, 6)};
BOOST_CHECK_THROW(ExtractHttpBody(mid_chunk, true), std::runtime_error);
// A chunk whose declared size overruns what is present.
const std::string overrun{head + "ffff\r\n" + BODY + "\r\n0\r\n\r\n"};
BOOST_CHECK_THROW(ExtractHttpBody(overrun, true), std::runtime_error);
// Size field that is not hexadecimal.
const std::string bad_size{head + "zz\r\n" + BODY + "\r\n0\r\n\r\n"};
BOOST_CHECK_THROW(ExtractHttpBody(bad_size, true), std::runtime_error);
// Chunk data not followed by its CRLF.
const std::string bad_terminator{head + ToHex(BODY.size()) + "\r\n" + BODY + "XX0\r\n\r\n"};
BOOST_CHECK_THROW(ExtractHttpBody(bad_terminator, true), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(other_transfer_encodings_are_rejected)
{
// Only chunked is decoded. Anything else must not be passed off as a body.
const std::string response{"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\n\r\n" + BODY};
BOOST_CHECK_THROW(ExtractHttpBody(response, true), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(headers_match_case_insensitively)
{
// RFC 7230 field names are case insensitive, and the casing a server picks
// must not decide whether the length is checked at all.
const std::string lower{"HTTP/1.1 200 OK\r\ncontent-length: " + ToString(BODY.size()) + "\r\n\r\n" + BODY};
const std::string upper{"HTTP/1.1 200 OK\r\nCONTENT-LENGTH: " + ToString(BODY.size()) + "\r\n\r\n" + BODY};
BOOST_CHECK_EQUAL(ExtractHttpBody(lower, false), BODY);
BOOST_CHECK_EQUAL(ExtractHttpBody(upper, false), BODY);
const std::string short_lower{"HTTP/1.1 200 OK\r\ncontent-length: 999\r\n\r\n" + BODY};
BOOST_CHECK_THROW(ExtractHttpBody(short_lower, false), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(header_name_inside_a_value_is_not_matched)
{
// "content-length" appears in another header's value. Matching the block
// rather than each field name would pick it up and check the wrong number.
const std::string response{
"HTTP/1.1 200 OK\r\n"
"X-Note: content-length: 99999\r\n\r\n" +
BODY};
// No real Content-Length, so this falls through to the clean shutdown rule.
BOOST_CHECK_EQUAL(ExtractHttpBody(response, true), BODY);
BOOST_CHECK_THROW(ExtractHttpBody(response, false), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(malformed_responses_are_rejected)
{
BOOST_CHECK_THROW(ExtractHttpBody("", true), std::runtime_error);
BOOST_CHECK_THROW(ExtractHttpBody("not http at all\r\n\r\n", true), std::runtime_error);
// Headers never terminated.
BOOST_CHECK_THROW(ExtractHttpBody("HTTP/1.1 200 OK\r\nContent-Length: 1\r\n", true), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(non_200_status_is_rejected)
{
for (const std::string& status : {"404 Not Found", "403 Forbidden", "500 Internal Server Error"}) {
const std::string response{"HTTP/1.1 " + status + "\r\nContent-Length: 0\r\n\r\n"};
BOOST_CHECK_THROW(ExtractHttpBody(response, true), std::runtime_error);
}
}
//! Redirects, added with phase 3a. The fetcher follows them now, so where a
//! Location points is a decision the client makes and therefore one to pin down.
BOOST_AUTO_TEST_CASE(an_absolute_redirect_changes_host_and_target)
{
const Url next{ResolveRedirect("api.github.com", "/repos/x/releases/latest",
"https://download.reddcoin.com/bin/file.tar.gz")};
BOOST_CHECK_EQUAL(next.host, "download.reddcoin.com");
BOOST_CHECK_EQUAL(next.target, "/bin/file.tar.gz");
}
BOOST_AUTO_TEST_CASE(an_absolute_path_keeps_the_host)
{
const Url next{ResolveRedirect("download.reddcoin.com", "/bin/old/file", "/bin/new/file")};
BOOST_CHECK_EQUAL(next.host, "download.reddcoin.com");
BOOST_CHECK_EQUAL(next.target, "/bin/new/file");
}
BOOST_AUTO_TEST_CASE(a_relative_redirect_resolves_against_the_directory)
{
// Relative to the directory of the current target, not to its full path,
// which would otherwise produce /bin/reddcoin-core-4.22.9.4/SHA256SUMSother.
const Url next{ResolveRedirect("download.reddcoin.com",
"/bin/reddcoin-core-4.22.9.4/SHA256SUMS", "SHA256SUMS.sig")};
BOOST_CHECK_EQUAL(next.target, "/bin/reddcoin-core-4.22.9.4/SHA256SUMS.sig");
}
BOOST_AUTO_TEST_CASE(a_protocol_relative_redirect_stays_on_https)
{
const Url next{ResolveRedirect("api.github.com", "/x", "//example.org/y")};
BOOST_CHECK_EQUAL(next.host, "example.org");
BOOST_CHECK_EQUAL(next.target, "/y");
}
BOOST_AUTO_TEST_CASE(a_redirect_out_of_https_is_refused)
{
// The one that matters. Following this would discard the certificate
// verification the fetcher exists to perform, and a redirect is exactly
// where an attacker would try to introduce it.
BOOST_CHECK_THROW(ResolveRedirect("api.github.com", "/x", "http://example.org/y"),
std::runtime_error);
BOOST_CHECK_THROW(ResolveRedirect("api.github.com", "/x", "ftp://example.org/y"),
std::runtime_error);
}
BOOST_AUTO_TEST_CASE(a_redirect_to_another_port_is_refused)
{
// The port would have to be carried through to the connect. Ignoring it
// would connect somewhere other than where the server asked.
BOOST_CHECK_THROW(ResolveRedirect("api.github.com", "/x", "https://example.org:8443/y"),
std::runtime_error);
}
BOOST_AUTO_TEST_CASE(a_malformed_redirect_is_refused)
{
BOOST_CHECK_THROW(ResolveRedirect("api.github.com", "/x", ""), std::runtime_error);
BOOST_CHECK_THROW(ResolveRedirect("api.github.com", "/x", "https:///y"), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(a_redirect_response_is_parsed_rather_than_rejected)
{
// ExtractHttpBody throws on any non-200, which is right for a caller that
// wants a body. The fetcher needs to see the status and Location instead.
const std::string raw{
"HTTP/1.1 302 Found\r\n"
"Location: https://download.reddcoin.com/bin/file\r\n"
"\r\n"};
const HttpResponse response{ParseHttpResponse(raw, false)};
BOOST_CHECK_EQUAL(response.status, 302U);
BOOST_CHECK_EQUAL(response.location, "https://download.reddcoin.com/bin/file");
BOOST_CHECK_THROW(ExtractHttpBody(raw, false), std::runtime_error);
}
BOOST_AUTO_TEST_CASE(a_redirect_body_is_not_held_to_the_framing_rules)
{
// No Content-Length and no clean shutdown. For a 200 that is a truncated
// response and must be rejected; for a redirect the body is discarded, so
// failing here would reject a valid redirect over bytes nothing reads.
const std::string redirect{
"HTTP/1.1 301 Moved Permanently\r\n"
"Location: /elsewhere\r\n"
"\r\n"
"partial"};
BOOST_CHECK_NO_THROW(ParseHttpResponse(redirect, false));
const std::string ok{
"HTTP/1.1 200 OK\r\n"
"\r\n"
"partial"};
BOOST_CHECK_THROW(ParseHttpResponse(ok, false), std::runtime_error);
}
//! Streaming, added with phase 3b. The decoder sees the stream in whatever
//! pieces the network hands it, so the cases that matter are the splits.
namespace {
//! Feed a chunked body to the decoder in fixed size pieces, returning what it
//! decoded. step of 0 means feed it all at once.
std::string DecodeInPieces(const std::string& raw, std::size_t step, bool& complete, bool& ok)
{
ChunkedDecoder decoder;
std::string out;
const auto sink = [&out](const char* data, std::size_t size) {
out.append(data, size);
return true;
};
ok = true;
const std::size_t width{step == 0 ? raw.size() : step};
for (std::size_t pos{0}; pos < raw.size() && ok; pos += width) {
ok = decoder.Feed(raw.data() + pos, std::min(width, raw.size() - pos), sink);
}
complete = decoder.Complete();
return out;
}
} // namespace
BOOST_AUTO_TEST_CASE(chunked_decoding_survives_any_split)
{
// The property that matters. Where the reads land is not ours to choose, so
// every boundary has to give the same answer.
const std::string raw{"4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"};
const std::string expected{"Wikipedia in\r\n\r\nchunks."};
for (std::size_t step{1}; step <= raw.size(); ++step) {
bool complete{false}, ok{false};
const std::string got{DecodeInPieces(raw, step, complete, ok)};
BOOST_CHECK_MESSAGE(ok, "decode failed at step " << step);
BOOST_CHECK_MESSAGE(complete, "not complete at step " << step);
BOOST_CHECK_MESSAGE(got == expected, "wrong body at step " << step << ": " << got);
}
}
BOOST_AUTO_TEST_CASE(a_chunked_body_without_its_final_chunk_is_incomplete)
{
// Not an error while reading: the bytes so far are valid. It is incomplete,
// which is what the caller must act on rather than treating it as the end.
bool complete{true}, ok{false};
const std::string got{DecodeInPieces("4\r\nWiki\r\n", 1, complete, ok)};
BOOST_CHECK(ok);
BOOST_CHECK(!complete);
BOOST_CHECK_EQUAL(got, "Wiki");
}
BOOST_AUTO_TEST_CASE(malformed_chunk_framing_is_rejected)
{
bool complete{false}, ok{true};
// Size that is not hexadecimal.
DecodeInPieces("zz\r\ndata\r\n0\r\n\r\n", 0, complete, ok);
BOOST_CHECK(!ok);
// Chunk not followed by its CRLF.
ok = true;
DecodeInPieces("4\r\nWikiXX5\r\n", 0, complete, ok);
BOOST_CHECK(!ok);
}
BOOST_AUTO_TEST_CASE(chunk_extensions_are_ignored)
{
bool complete{false}, ok{false};
const std::string got{DecodeInPieces("4;name=value\r\nWiki\r\n0\r\n\r\n", 3, complete, ok)};
BOOST_CHECK(ok);
BOOST_CHECK(complete);
BOOST_CHECK_EQUAL(got, "Wiki");
}
BOOST_AUTO_TEST_CASE(a_sink_that_stops_stops_the_decode)
{
// How a cancelled download, or a failed write, aborts mid body.
ChunkedDecoder decoder;
const std::string raw{"4\r\nWiki\r\n0\r\n\r\n"};
const bool fed{decoder.Feed(raw.data(), raw.size(),
[](const char*, std::size_t) { return false; })};
BOOST_CHECK(!fed);
BOOST_CHECK(!decoder.Complete());
}
BOOST_AUTO_TEST_CASE(headers_are_parsed_without_a_body)
{
// What a streaming read needs before deciding anything: it must not have to
// see the body to know the status, the length or the framing.
const HttpHeaders plain{ParseHttpHeaders(
"HTTP/1.1 200 OK\r\nContent-Length: 42\r\nContent-Type: application/gzip")};
BOOST_CHECK_EQUAL(plain.status, 200U);
BOOST_CHECK_EQUAL(plain.content_length, 42);
BOOST_CHECK(!plain.chunked);
const HttpHeaders chunked{ParseHttpHeaders(
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked")};
BOOST_CHECK(chunked.chunked);
BOOST_CHECK_EQUAL(chunked.content_length, -1);
const HttpHeaders moved{ParseHttpHeaders(
"HTTP/1.1 302 Found\r\nLocation: https://example.org/x")};
BOOST_CHECK_EQUAL(moved.status, 302U);
BOOST_CHECK_EQUAL(moved.location, "https://example.org/x");
// No length and no chunking is legal: the body runs to the close.
const HttpHeaders open_ended{ParseHttpHeaders("HTTP/1.1 200 OK\r\nServer: x")};
BOOST_CHECK_EQUAL(open_ended.content_length, -1);
BOOST_CHECK(!open_ended.chunked);
}
BOOST_AUTO_TEST_SUITE_END()