curl-fuzzer C++ reference
Loading...
Searching...
No Matches
pingpong_mock_server.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <algorithm>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <memory>
17#include <string>
18#include <string_view>
19
21
22namespace proto_fuzzer {
23
28PingpongMockServer::PingpongMockServer(MultiDrivePolicy drive_policy)
29 : MockServerBase(drive_policy),
30 scenario_(nullptr),
31 control_reply_count_(0),
32 next_control_reply_(0),
33 active_pollset_probe_count_(0),
34 writable_pollset_probe_count_(0),
35 body_length_remaining_(0),
36 body_terminator_remaining_(0),
37 body_mode_(BodyMode::kCommand),
38 control_socket_(CURL_SOCKET_BAD),
39 control_opened_(false) {}
40
45
49const std::string& PingpongMockServer::control_transcript() const { return control_transcript_; }
50
56const std::string& PingpongMockServer::uploaded_data() const { return uploaded_data_; }
57
64std::size_t PingpongMockServer::active_pollset_probe_count() const { return active_pollset_probe_count_; }
65
71std::size_t PingpongMockServer::writable_pollset_probe_count() const { return writable_pollset_probe_count_; }
72
76std::unique_ptr<MockConnection> PingpongMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
77
81void PingpongMockServer::BeginMarkerBody(std::string_view terminator) {
82 body_mode_ = BodyMode::kMarker;
83 body_marker_.assign(terminator.data(), terminator.size());
84 body_length_remaining_ = 0;
85 body_terminator_remaining_ = 0;
86}
87
92void PingpongMockServer::BeginLiteralBody(std::size_t length) {
93 body_mode_ = BodyMode::kLiteral;
94 body_marker_.clear();
95 body_length_remaining_ = length;
96 body_terminator_remaining_ = 2;
97}
98
103void PingpongMockServer::BeginExactBody(std::size_t length) {
104 body_mode_ = BodyMode::kExact;
105 body_marker_.clear();
106 body_length_remaining_ = length;
107 body_terminator_remaining_ = 0;
108}
109
113 body_mode_ = BodyMode::kOpaque;
114 body_marker_.clear();
115 body_length_remaining_ = 0;
116 body_terminator_remaining_ = 0;
117}
118
124void PingpongMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) {
125 if (data == nullptr || size == 0) {
126 return;
127 }
128 pending_control_bytes_.append(reinterpret_cast<const char*>(data), size);
129}
130
138curl_socket_t PingpongMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* /*address*/) {
139 if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN || control_opened_) {
140 return CURL_SOCKET_BAD;
141 }
142
144 if (!connection_ || !connection_->ok()) {
145 connection_.reset();
146 return CURL_SOCKET_BAD;
147 }
148 connection_->SetIncomingDataObserver(this);
149
150 ApplyScriptBackpressure(connection_.get(), scenario_->connection());
151 const std::string& greeting = scenario_->connection().initial_response();
152 if (!greeting.empty() &&
153 !connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) {
154 connection_.reset();
155 return CURL_SOCKET_BAD;
156 }
157
158 control_opened_ = true;
159 control_socket_ = connection_->take_client_fd();
160 return control_socket_;
161}
162
167void PingpongMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) {
168 connection_.reset();
169 scenario_ = &scenario;
170 control_reply_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks,
171 static_cast<std::size_t>(scenario.connection().on_readable_size()));
172 next_control_reply_ = 0;
173 pending_control_bytes_.clear();
174 control_transcript_.clear();
175 uploaded_data_.clear();
176 active_pollset_probe_count_ = 0;
177 writable_pollset_probe_count_ = 0;
178 body_marker_.clear();
179 body_length_remaining_ = 0;
180 body_terminator_remaining_ = 0;
181 body_mode_ = BodyMode::kCommand;
182 control_socket_ = CURL_SOCKET_BAD;
183 control_opened_ = false;
185}
186
192void PingpongMockServer::ApplyScriptBackpressure(MockConnection* connection,
193 const curl::fuzzer::proto::Connection& script) {
194 if (connection == nullptr) {
195 return;
196 }
197
198 const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
199 const auto& backpressure = script.backpressure();
200 const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
201 connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit()));
202}
203
209 if (scenario_ == nullptr || next_control_reply_ >= control_reply_count_) {
210 return nullptr;
211 }
212 return &scenario_->connection().on_readable(static_cast<int>(next_control_reply_++));
213}
214
220bool PingpongMockServer::WriteControlBytes(std::string_view bytes) {
221 return bytes.empty() || (connection_ != nullptr &&
222 connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size()));
223}
224
230 const std::string* response = NextControlReply();
231 return response == nullptr ? false : WriteControlBytes(*response);
232}
233
238 if (connection_ != nullptr) {
239 connection_->ShutdownWrite();
240 }
241}
242
247std::string_view PingpongMockServer::FirstToken(std::string_view line) {
248 std::size_t begin = 0;
249 while (begin < line.size() && (line[begin] == ' ' || line[begin] == '\t')) {
250 ++begin;
251 }
252
253 std::size_t end = begin;
254 while (end < line.size() && line[end] != ' ' && line[end] != '\t' && line[end] != '\r' && line[end] != '\n') {
255 ++end;
256 }
257 return line.substr(begin, end - begin);
258}
259
264std::string_view PingpongMockServer::SecondToken(std::string_view line) {
265 std::size_t position = 0;
266 while (position < line.size() && (line[position] == ' ' || line[position] == '\t')) {
267 ++position;
268 }
269 while (position < line.size() && line[position] != ' ' && line[position] != '\t' && line[position] != '\r' &&
270 line[position] != '\n') {
271 ++position;
272 }
273 while (position < line.size() && (line[position] == ' ' || line[position] == '\t')) {
274 ++position;
275 }
276
277 std::size_t end = position;
278 while (end < line.size() && line[end] != ' ' && line[end] != '\t' && line[end] != '\r' && line[end] != '\n') {
279 ++end;
280 }
281 return line.substr(position, end - position);
282}
283
290bool PingpongMockServer::TokenEquals(std::string_view token, std::string_view expected_uppercase) {
291 if (token.size() != expected_uppercase.size()) {
292 return false;
293 }
294 for (std::size_t index = 0; index < token.size(); ++index) {
295 unsigned char actual = static_cast<unsigned char>(token[index]);
296 if (actual >= 'a' && actual <= 'z') {
297 actual = static_cast<unsigned char>(actual - 'a' + 'A');
298 }
299 if (actual != static_cast<unsigned char>(expected_uppercase[index])) {
300 return false;
301 }
302 }
303 return true;
304}
305
312void PingpongMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) {
313 if (destination == nullptr || destination->size() >= limit) {
314 return;
315 }
316 const std::size_t available = limit - destination->size();
317 destination->append(source.data(), std::min(source.size(), available));
318}
319
325bool PingpongMockServer::ServiceControlConnection() {
326 if (connection_ == nullptr) {
327 return false;
328 }
329
330 const std::size_t size_before_read = pending_control_bytes_.size();
331 const std::size_t transport_progress = connection_->DrainIncoming();
332 bool made_progress = pending_control_bytes_.size() != size_before_read || transport_progress != 0;
333
334 std::size_t consumed = 0;
335 while (consumed < pending_control_bytes_.size()) {
336 if (body_mode_ == BodyMode::kOpaque) {
337 consumed = pending_control_bytes_.size();
338 made_progress = true;
339 break;
340 }
341
342 if (body_mode_ == BodyMode::kMarker) {
343 const std::size_t marker = pending_control_bytes_.find(body_marker_, consumed);
344 if (marker == std::string::npos) {
345 break;
346 }
347 const std::size_t body_end = marker + body_marker_.size();
348 CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, body_end - consumed),
349 kMaxCapturedUploadBytes, &uploaded_data_);
350 consumed = body_end;
351 body_mode_ = BodyMode::kCommand;
352 made_progress = true;
354 continue;
355 }
356
357 if (body_mode_ == BodyMode::kExact) {
358 const std::size_t available = pending_control_bytes_.size() - consumed;
359 const std::size_t take = std::min(available, body_length_remaining_);
360 CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, take), kMaxCapturedUploadBytes,
361 &uploaded_data_);
362 consumed += take;
363 body_length_remaining_ -= take;
364 if (take != 0) {
365 made_progress = true;
366 }
367 if (body_length_remaining_ == 0) {
368 body_mode_ = BodyMode::kCommand;
369 made_progress = true;
371 continue;
372 }
373 break;
374 }
375
376 if (body_mode_ == BodyMode::kLiteral) {
377 if (body_length_remaining_ != 0) {
378 const std::size_t available = pending_control_bytes_.size() - consumed;
379 const std::size_t take = std::min(available, body_length_remaining_);
380 CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, take), kMaxCapturedUploadBytes,
381 &uploaded_data_);
382 consumed += take;
383 body_length_remaining_ -= take;
384 made_progress = true;
385 continue;
386 }
387
388 const std::size_t available = pending_control_bytes_.size() - consumed;
389 if (body_terminator_remaining_ == 2 && available >= 2 && pending_control_bytes_[consumed] == '\r' &&
390 pending_control_bytes_[consumed + 1] == '\n') {
391 consumed += 2;
392 body_terminator_remaining_ = 0;
393 body_mode_ = BodyMode::kCommand;
394 made_progress = true;
396 continue;
397 }
398 if (available >= 1) {
399 const char byte = pending_control_bytes_[consumed];
400 if (byte == '\r' || byte == '\n') {
401 ++consumed;
402 if (byte == '\n' || body_terminator_remaining_ == 0) {
403 body_terminator_remaining_ = 0;
404 body_mode_ = BodyMode::kCommand;
406 } else {
407 body_terminator_remaining_ = 1;
408 }
409 made_progress = true;
410 continue;
411 }
412 // No terminating empty line is recognisable. Resume command parsing so
413 // malformed uploads still make bounded progress.
414 body_terminator_remaining_ = 0;
415 body_mode_ = BodyMode::kCommand;
416 continue;
417 }
418 break;
419 }
420
421 const std::size_t newline = pending_control_bytes_.find('\n', consumed);
422 if (newline == std::string::npos) {
423 break;
424 }
425 const std::size_t command_size = newline - consumed + 1;
426 const std::string_view command(pending_control_bytes_.data() + consumed, command_size);
427 CapturePrefix(command, kMaxCapturedControlBytes, &control_transcript_);
428 HandleLine(command);
429 consumed += command_size;
430 made_progress = true;
431 }
432
433 if (consumed != 0) {
434 pending_control_bytes_.erase(0, consumed);
435 }
436 return made_progress;
437}
438
442void PingpongMockServer::FinishConnections() {
443 if (connection_ != nullptr) {
445 connection_->ShutdownWrite();
446 }
447}
448
456 multi.reset();
457 (void)ServiceControlConnection();
458}
459
467void PingpongMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
468 (void)easy;
469 ResetForScenario(scenario);
470
471 int still_running = 1;
472 int idle_iterations = 0;
473 int drive_iterations = 0;
474 CURLMcode result = CURLM_OK;
475 while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) {
476 const int running_before = still_running;
477 result = curl_multi_perform(multi, &still_running);
478 if (result != CURLM_OK) {
479 break;
480 }
481
482 // Pollset construction is coverage work, not part of the default driver:
483 // keep the extra public-API call behind the scenario opt-in. Waiting until
484 // the socket callback has run and the transfer remains active ensures the
485 // pollset belongs to a live protocol state rather than setup or cleanup.
486 if (scenario.probe_multi_pollset() && still_running && control_opened_) {
487 ProbeMultiPollset(multi);
488 ++active_pollset_probe_count_;
489 const bool incomplete_command = body_mode_ == BodyMode::kCommand && !pending_control_bytes_.empty() &&
490 pending_control_bytes_.find('\n') == std::string::npos;
491 if (incomplete_command && MultiPollsetWantsWrite(multi, control_socket_)) {
492 ++writable_pollset_probe_count_;
493 }
494 }
495
496 bool made_progress = still_running != running_before;
497 made_progress = ServiceControlConnection() || made_progress;
498 if (made_progress) {
499 idle_iterations = 0;
500 } else {
501 ++idle_iterations;
502 }
503 }
504
505 // The final perform may have emitted the last command or body bytes
506 // immediately before marking the transfer done. Drain them before cleanup.
507 (void)ServiceControlConnection();
508 FinishConnections();
509 scenario_ = nullptr;
510}
511
512} // namespace proto_fuzzer
std::unique_ptr< MockConnection > connection_
The per-scenario MockConnection, lazily created by HandleOpenSocket().
static void ProbeMultiPollset(CURLM *multi)
Ask curl to construct and inspect its connection-filter pollset without waiting.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
static bool MultiPollsetWantsWrite(CURLM *multi, curl_socket_t socket)
Inspect one socket's current writable interest through curl's public multi fdset API.
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Create the single control socketpair and queue the server greeting.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Alternate curl transitions with immediate peer service until the transfer completes or deterministic ...
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Append decoded application bytes to the pending parser buffer.
void HandleDetachedMulti(CurlMultiPtr multi) override
Give curl's graceful shutdown one last chance to emit its teardown command while the peer still owns ...
const std::string & control_transcript() const
Expose the bounded command capture from the most recent drive so tests can verify protocol states wit...
void BeginExactBody(std::size_t length)
Enter exact-length body mode.
bool WriteNextReply()
Consume the next scripted reply.
const curl::fuzzer::proto::Scenario * scenario() const
Borrowed scenario for the synchronous drive, or nullptr outside RunLoop().
void BeginLiteralBody(std::size_t length)
Enter length-announced literal body mode.
std::size_t active_pollset_probe_count() const
Report how many requested zero-timeout multi-poll probes ran during the most recent drive.
virtual void HandleBodyComplete()
Called after a body phase reached its terminator and the parser returned to command mode.
static std::string_view SecondToken(std::string_view line)
Skip the first token and return the second whitespace-delimited token without allocating.
void BeginOpaqueDrain()
Enter an opaque drain after a transport upgrade.
virtual void HandleLine(std::string_view line)=0
Answer one complete, newline-terminated command line.
void ShutdownControlWrite()
Half-close the server-to-client direction while retaining the read half.
virtual std::unique_ptr< MockConnection > CreateConnection()
Create the default plaintext transport.
static std::string_view FirstToken(std::string_view line)
Remove optional leading horizontal whitespace and return the first token without allocating.
~PingpongMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
const std::string * NextControlReply()
Return the next command-aligned response within the shared response-count budget.
std::size_t writable_pollset_probe_count() const
Report how often curl retained writable interest while the peer held an incomplete control command.
virtual void QueueShutdownReply()
Queue protocol-specific teardown liveness bytes before the control socket is half-closed.
const std::string & uploaded_data() const
Expose a bounded prefix of bytes drained during the most recent body phase while continuing to drain ...
static bool TokenEquals(std::string_view token, std::string_view expected_uppercase)
Fold only ASCII lowercase command letters.
bool WriteControlBytes(std::string_view bytes)
Keep failed peer writes local to the mock.
void BeginMarkerBody(std::string_view terminator)
Enter marker-terminated body mode.
virtual void ResetProtocolState()
Reset protocol-specific parser state for a new drive.
std::unique_ptr< CURLM, CurlMultiDeleter > CurlMultiPtr
Unique ownership of a CURL multi handle.
Definition curl_raii.h:58
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
MultiDrivePolicy
Fixed policy for interpreting a scenario's multi-drive fields.
PingpongMockServer — shared line-oriented command/response transport for the mail-style protocol peer...
constexpr std::size_t kMaxResponseChunks
Enough response boundaries to exercise incremental parsers while keeping per-perform delivery work fi...