Repository navigation
Expand file tree
/
Copy pathhttp_request.cpp
More file actions
211 lines (185 loc) · 7.87 KB
/
Copy pathhttp_request.cpp
File metadata and controls
211 lines (185 loc) · 7.87 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
// Copyright (c) 2020-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <httpserver.h>
#include <netaddress.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/util/net.h>
#include <test/util/time.h>
#include <util/check.h>
#include <util/signalinterrupt.h>
#include <util/strencodings.h>
#include <cassert>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
std::string_view RequestMethodString(HTTPRequestMethod m);
using namespace bitcoin_http;
namespace {
//! A fully parsed request must have a body consistent with its framing headers.
void CheckBodyMatchesFraming(const HTTPRequest& req)
{
const std::string body{req.ReadBody()};
const auto transfer_encoding{req.GetHeader("Transfer-Encoding")};
const auto content_length{req.GetHeader("Content-Length")};
if (transfer_encoding && ToLower(*transfer_encoding) == "chunked") {
assert(body.size() <= MAX_BODY_SIZE);
} else if (content_length) {
const auto parsed_length{ToIntegral<uint64_t>(*content_length)};
assert(parsed_length);
assert(body.size() == *parsed_length);
} else {
assert(body.empty());
}
}
//! Parse the whole buffer in a single pass, as one I/O cycle would.
void SingleShotParse(const std::string& http_buffer, FuzzedDataProvider& provider)
{
using util::LineReader;
HTTPRequest http_request;
LineReader reader(http_buffer, MAX_HEADERS_SIZE);
try {
if (!http_request.LoadControlData(reader)) return;
if (!http_request.LoadHeaders(reader)) return;
if (!http_request.LoadBody(reader)) return;
} catch (const std::runtime_error&) {
return;
}
const HTTPRequestMethod request_method = http_request.GetRequestMethod();
(void)RequestMethodString(request_method);
(void)http_request.GetURI();
(void)http_request.GetHeader("Host");
std::string header = provider.ConsumeRandomLengthString(16);
(void)http_request.GetHeader(header);
(void)http_request.WriteHeader(std::string(header), provider.ConsumeRandomLengthString(16));
(void)http_request.GetHeader(header);
CheckBodyMatchesFraming(http_request);
}
//! Expose HTTPRemoteClient with the receive buffer.
class FuzzClient : public HTTPRemoteClient
{
public:
FuzzClient() : HTTPRemoteClient{/*id=*/0, /*addr=*/CService(), /*socket=*/std::make_unique<ZeroSock>()} {}
void Receive(std::string_view s) { MutateRecvBuffer().append(s); }
};
int StateRank(HTTPRequest::State s)
{
switch (s) {
case HTTPRequest::State::Init: return 0;
case HTTPRequest::State::NeedsHeaders: return 1;
case HTTPRequest::State::NeedsBody: return 2;
case HTTPRequest::State::Complete: return 3;
case HTTPRequest::State::Error: return 4;
}
assert(false);
}
struct ParsedRequest {
HTTPRequestMethod method;
std::string uri;
std::string host;
std::string body;
bool operator==(const ParsedRequest&) const = default;
};
struct RunResult {
std::vector<ParsedRequest> requests;
bool errored{false};
std::string remainder;
HTTPRequest::State last_state{HTTPRequest::State::Init};
};
//! Take every request the client can parse right now, checking what must hold
//! at each I/O cycle boundary.
void Drain(const std::shared_ptr<FuzzClient>& client, RunResult& out)
{
while (true) {
// A failed request is inert: further reads consume nothing.
if (const HTTPRequest* cur{client->GetRequest()};
cur && cur->GetState() == HTTPRequest::State::Error) {
const size_t buffered{client->GetRecvBuffer().size()};
Assert(HTTPRemoteClient::TryReadRequest(client) == nullptr);
assert(client->GetRecvBuffer().size() == buffered);
out.errored = true;
break;
}
std::unique_ptr<HTTPRequest> req{HTTPRemoteClient::TryReadRequest(client)};
if (!req) {
if (const HTTPRequest* cur{client->GetRequest()}) {
// TryReadRequest() returns empty if the HTTPRemoteClient is still
// holding on to the request. In this test, the request is guaranteed
// to be incomplete or failed, although in production it may be a result of
// a busy worker or a full send buffer.
assert(cur->GetState() != HTTPRequest::State::Complete);
assert(StateRank(cur->GetState()) >= StateRank(out.last_state));
out.last_state = cur->GetState();
if (cur->GetState() == HTTPRequest::State::Error) out.errored = true;
if (const auto chunk_size{cur->GetChunkSize()}) {
assert(cur->GetChunkProgress() <= *chunk_size);
}
assert(cur->ReadBody().size() <= MAX_BODY_SIZE);
}
break;
}
assert(req->GetState() == HTTPRequest::State::Complete);
CheckBodyMatchesFraming(*req);
out.requests.emplace_back(req->GetRequestMethod(), req->GetURI(),
req->GetHeader("Host").value_or(""), req->ReadBody());
// While a request is with a worker, nothing new is parsed or consumed.
const size_t buffered{client->GetRecvBuffer().size()};
Assert(HTTPRemoteClient::TryReadRequest(client) == nullptr);
assert(client->GetRecvBuffer().size() == buffered);
req->WriteReply(HTTP_OK, ""); // clears m_req_busy
out.last_state = HTTPRequest::State::Init;
}
}
//! HTTP framing is defined by the byte stream, not by how TCP happened to split
//! it, so parsing the same bytes whole and in pieces must give the same result.
//! Only the piecewise run reaches the resumable parse paths.
void CheckSegmentationIndependence(const std::string& input, FuzzedDataProvider& provider)
{
// WriteReply() stamps a wall-clock Date header and the client stamps a
// steady-clock idle time, both of which the fuzz determinism check rejects.
FakeNodeClock clock{1610000000s};
FakeSteadyClock steady_clock;
// The whole stream arrives in one I/O cycle. This is similar to
// SingleShotParse(), although here we pipe it through the client and also
// parse multiple requests.
RunResult one_shot;
{
auto client{std::make_shared<FuzzClient>()};
client->Receive(input);
Drain(client, one_shot);
one_shot.remainder = client->GetRecvBuffer();
}
// The same stream arrives in arbitrary pieces. Once the provider runs dry
// this becomes one byte per cycle, the maximally fragmented case.
RunResult sliced;
{
auto client{std::make_shared<FuzzClient>()};
std::string_view remaining{input};
while (!remaining.empty()) {
const size_t n{provider.ConsumeIntegralInRange<size_t>(1, remaining.size())};
client->Receive(remaining.substr(0, n));
remaining = remaining.substr(n);
Drain(client, sliced);
}
sliced.remainder = client->GetRecvBuffer();
}
assert(one_shot.requests == sliced.requests);
assert(one_shot.errored == sliced.errored);
// The whole receive buffer is discarded on a parse error, so in the sliced case
// what is left over depends on how much had arrived when the error fired.
if (!one_shot.errored) assert(one_shot.remainder == sliced.remainder);
}
} // namespace
FUZZ_TARGET(http_request)
{
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
// MAX_HEADERS_SIZE is 8192: leave room for a headers section that can
// reach the limit, plus a body.
const std::string http_buffer{fuzzed_data_provider.ConsumeRandomLengthString(2 * MAX_HEADERS_SIZE)};
SingleShotParse(http_buffer, fuzzed_data_provider);
CheckSegmentationIndependence(http_buffer, fuzzed_data_provider);
}