/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_ |