Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/pingpong_mock_server.h
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief PingpongMockServer — shared line-oriented command/response transport
9
///        for the mail-style protocol peers.
10
11
#ifndef PROTO_FUZZER_PINGPONG_MOCK_SERVER_H_
12
#define PROTO_FUZZER_PINGPONG_MOCK_SERVER_H_
13
14
#include <curl/curl.h>
15
16
#include <cstddef>
17
#include <memory>
18
#include <string>
19
#include <string_view>
20
21
#include "curl_fuzzer.pb.h"
22
#include "proto_fuzzer/incoming_data_observer.h"
23
#include "proto_fuzzer/mock_server_base.h"
24
#include "proto_fuzzer/scenario_limits.h"
25
26
namespace proto_fuzzer {
27
28
class MockConnection;
29
30
/// @class proto_fuzzer::PingpongMockServer
31
/// @brief Drives one control socketpair that speaks a line-oriented
32
///        command/response protocol.
33
///
34
/// The base owns the transport lifecycle, the bounded perform loop, line
35
/// buffering, the reply cursor, and the test-facing transcript. A concrete
36
/// peer supplies HandleLine() and may switch the parser into body mode after a
37
/// command that uploads data (SMTP DATA, IMAP APPEND). Replies are emitted
38
/// exactly as scripted; a peer that needs protocol-specific rewriting, such as
39
/// IMAP tag insertion, does so inside HandleLine().
40
///
41
/// Scenario.connection is a command-aligned script: initial_response is the
42
/// server greeting and each on_readable entry answers the next command in
43
/// order. Subclasses replace the transport through CreateConnection(); the
44
/// implicit-TLS SMTPS peer uses that hook without touching the parser.
45
class PingpongMockServer : public MockServerBase, public IncomingDataObserver {
46
 public:
47
  ~PingpongMockServer() override;
48
49
  PingpongMockServer(const PingpongMockServer&) = delete;
50
  PingpongMockServer& operator=(const PingpongMockServer&) = delete;
51
52
  // Bounded record of complete command lines sent by curl.
53
  const std::string& control_transcript() const;
54
55
  // Bounded record of the body most recently drained by a body phase. Peers
56
  // without an upload direction keep this empty.
57
  const std::string& uploaded_data() const;
58
59
  // Number of opt-in multi-poll probes issued while the most recent transfer
60
  // was active and its control socket had been handed to curl.
61
  std::size_t active_pollset_probe_count() const;
62
63
 protected:
64
  explicit PingpongMockServer(MultiDrivePolicy drive_policy = MultiDrivePolicy::kPerform);
65
66
  // Create the transport that carries the exchange. Overridden by TLS peers.
67
  virtual std::unique_ptr<MockConnection> CreateConnection();
68
69
  /// Answer one complete, newline-terminated command line.
70
  /// @param line Complete command line, including its line ending.
71
  virtual void HandleLine(std::string_view line) = 0;
72
73
  // Body phase control. Only valid while HandleLine() is running; the parser
74
  // consumes the body before processing further command lines.
75
76
  // Consume bytes through the first occurrence of `terminator`, capturing the
77
  // body and the terminator itself. Used by SMTP's "\r\n.\r\n" end-of-body.
78
  void BeginMarkerBody(std::string_view terminator);
79
80
  // Consume exactly `length` bytes, capturing them, followed by a CRLF line
81
  // terminator that is not captured. Used by IMAP's {N} literal uploads.
82
  void BeginLiteralBody(std::size_t length);
83
84
  // Consume exactly `length` bytes, capturing them, with no trailing
85
  // terminator. Used by protocols such as RTSP whose announced entity body is
86
  // followed immediately by the next request or by the peer's close.
87
  void BeginExactBody(std::size_t length);
88
89
  /// Called after a body phase reached its terminator and the parser returned
90
  /// to command mode. The default does nothing.
91
976
  virtual void HandleBodyComplete() {}
92
93
  /// Queue protocol-specific teardown liveness bytes before the control socket
94
  /// is half-closed.
95
8.19k
  virtual void QueueShutdownReply() {}
96
97
  /// Reset protocol-specific parser state for a new drive. The transport,
98
  /// cursors, transcript, upload capture, and body phase are already reset.
99
3.69k
  virtual void ResetProtocolState() {}
100
101
  curl_socket_t HandleOpenSocket(curlsocktype purpose = CURLSOCKTYPE_IPCXN,
102
                                 struct curl_sockaddr* address = nullptr) override;
103
104
  void RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override;
105
106
  void HandleDetachedMulti(CurlMultiPtr multi) override;
107
108
  void ObserveIncomingData(const unsigned char* data, std::size_t size) override;
109
110
  /// Borrowed scenario for the synchronous drive, or nullptr outside RunLoop().
111
  /// @return The Scenario borrowed by the active drive, or nullptr.
112
96.9k
  const curl::fuzzer::proto::Scenario* scenario() const { return scenario_; }
113
114
  // Return the next command-aligned reply, or nullptr once the bounded script
115
  // is exhausted. Empty entries still advance the cursor.
116
  const std::string* NextControlReply();
117
118
  // Queue one reply fragment, returning false only when the transport write
119
  // failed. An empty fragment is a successful no-op.
120
  bool WriteControlBytes(std::string_view bytes);
121
122
  // Write the next scripted reply. Returns false when none remains.
123
  bool WriteNextReply();
124
125
  // First and second whitespace-delimited tokens of a command line, without
126
  // interpreting any protocol-specific arguments.
127
  static std::string_view FirstToken(std::string_view line);
128
  static std::string_view SecondToken(std::string_view line);
129
130
  // Compare an ASCII protocol token to an uppercase literal, ignoring case.
131
  static bool TokenEquals(std::string_view token, std::string_view expected_uppercase);
132
133
 private:
134
  /// Parser mode after the most recent command.
135
  enum class BodyMode {
136
    kCommand,  ///< Splitting newline-terminated command lines.
137
    kMarker,   ///< Scanning for an opaque end-of-body marker.
138
    kLiteral,  ///< Draining an announced number of bytes, then CRLF.
139
    kExact,    ///< Draining exactly an announced number of bytes.
140
  };
141
142
  // Observability must not grow with a future curl loop; these caps exceed the
143
  // normalized upload and command budgets without affecting draining.
144
  static constexpr std::size_t kMaxCapturedControlBytes = 64 * 1024;
145
  static constexpr std::size_t kMaxCapturedUploadBytes = 2 * scenario_limits::kMaxUploadBytes;
146
147
  void ResetForScenario(const curl::fuzzer::proto::Scenario& scenario);
148
149
  static void ApplyScriptBackpressure(MockConnection* connection, const curl::fuzzer::proto::Connection& script);
150
151
  bool ServiceControlConnection();
152
153
  void FinishConnections();
154
155
  // Retain a bounded prefix for assertions without affecting parser progress.
156
  static void CapturePrefix(std::string_view source, std::size_t limit, std::string* destination);
157
158
  /// Borrowed only while RunLoop is active, including every socket callback.
159
  const curl::fuzzer::proto::Scenario* scenario_;
160
  /// Runtime-visible prefix of on_readable response fragments.
161
  std::size_t control_reply_count_;
162
  /// Cursor advanced once per command, plus any pre-queued body completion.
163
  std::size_t next_control_reply_;
164
  /// Partial command or body bytes retained until the next read.
165
  std::string pending_control_bytes_;
166
  /// Bounded test-facing record of complete commands sent by curl.
167
  std::string control_transcript_;
168
  /// Bounded test-facing prefix of the body drained in the last body phase.
169
  std::string uploaded_data_;
170
  /// Opt-in probes made only while the transfer and control socket were live.
171
  std::size_t active_pollset_probe_count_;
172
  /// Opaque terminator for BodyMode::kMarker.
173
  std::string body_marker_;
174
  /// Literal bytes still to drain for BodyMode::kLiteral.
175
  std::size_t body_length_remaining_;
176
  /// Remaining bytes of the CRLF sent after a literal body.
177
  std::size_t body_terminator_remaining_;
178
  /// Current parser phase.
179
  BodyMode body_mode_;
180
  /// Prevent a second control socket from displacing the live first one.
181
  bool control_opened_;
182
};
183
184
}  // namespace proto_fuzzer
185
186
#endif  // PROTO_FUZZER_PINGPONG_MOCK_SERVER_H_