curl-fuzzer C++ reference
Loading...
Searching...
No Matches
smtp_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 <memory>
15#include <string>
16#include <string_view>
17
18#include "proto_fuzzer/allocation_fault_injection.h"
21
22namespace proto_fuzzer {
23
28SmtpMockServer::SmtpMockServer(curl::fuzzer::proto::SmtpProbeMode mode)
29 : recipient_count_(0),
30 rcpt_command_count_(0),
31 auth_exchange_active_(false),
32 mode_(mode),
33 tls_upgrade_started_(false) {}
34
39
40namespace {
41
45const std::string& SendBackpressureCommand() {
46 static const std::string command = "NOOP " + std::string(16 * 1024, 'S');
47 return command;
48}
49
50} // namespace
51
55void SmtpMockServer::ConfigureClient(CURL* easy) const {
56 if (easy == nullptr) {
57 return;
58 }
59 switch (mode_) {
60 case curl::fuzzer::proto::SMTP_PROBE_STARTTLS_TRY:
61 (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_TRY));
62 break;
63 case curl::fuzzer::proto::SMTP_PROBE_STARTTLS_REQUIRE:
64 case curl::fuzzer::proto::SMTP_PROBE_STARTTLS_FILTER_ALLOCATION_FAILURE:
65 (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL));
66 break;
67 case curl::fuzzer::proto::SMTP_PROBE_SEND_BACKPRESSURE:
68 (void)curl_easy_setopt(easy, CURLOPT_UPLOAD, 0L);
69 (void)curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, SendBackpressureCommand().c_str());
70 break;
71 case curl::fuzzer::proto::SMTP_PROBE_DEFAULT:
72 default:
73 break;
74 }
75}
76
80std::unique_ptr<MockConnection> SmtpMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
81
86 const curl::fuzzer::proto::Scenario* script = scenario();
87 recipient_count_ = script == nullptr ? 0
88 : std::min<std::size_t>(scenario_limits::kMaxMailRecipients,
89 static_cast<std::size_t>(script->mail_rcpt_size()));
90 rcpt_command_count_ = 0;
91 auth_exchange_active_ = false;
92 tls_upgrade_started_ = false;
93}
94
97bool SmtpMockServer::IsStartTlsMode() const {
98 return mode_ == curl::fuzzer::proto::SMTP_PROBE_STARTTLS_TRY ||
99 mode_ == curl::fuzzer::proto::SMTP_PROBE_STARTTLS_REQUIRE ||
100 mode_ == curl::fuzzer::proto::SMTP_PROBE_STARTTLS_FILTER_ALLOCATION_FAILURE;
101}
102
107bool SmtpMockServer::AllowsRecipientFailures() const {
108 const curl::fuzzer::proto::Scenario* script = scenario();
109 if (script == nullptr) {
110 return false;
111 }
112 for (int index = script->options_size() - 1; index >= 0; --index) {
113 const auto& option = script->options(index);
114 if (option.option_id() != curl::fuzzer::proto::CURLOPT_MAIL_RCPT_ALLOWFAILS) {
115 continue;
116 }
117 if (option.value_case() == curl::fuzzer::proto::SetOption::kBoolValue) {
118 return option.bool_value();
119 }
120 if (option.value_case() == curl::fuzzer::proto::SetOption::kUintValue) {
121 return option.uint_value() != 0;
122 }
123 return false;
124 }
125 return false;
126}
127
133int SmtpMockServer::FirstReplyCode(std::string_view response) {
134 std::size_t line_start = 0;
135 while (line_start < response.size()) {
136 const std::size_t newline = response.find('\n', line_start);
137 if (newline == std::string_view::npos) {
138 return -1;
139 }
140 const std::size_t line_size = newline - line_start + 1;
141 if (line_size > 3) {
142 const unsigned char first = static_cast<unsigned char>(response[line_start]);
143 const unsigned char second = static_cast<unsigned char>(response[line_start + 1]);
144 const unsigned char third = static_cast<unsigned char>(response[line_start + 2]);
145 if (first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' &&
146 response[line_start + 3] == ' ') {
147 return 100 * (first - '0') + 10 * (second - '0') + (third - '0');
148 }
149 }
150 line_start = newline + 1;
151 }
152 return -1;
153}
154
163void SmtpMockServer::HandleLine(std::string_view command) {
164 const std::string_view verb = FirstToken(command);
165 const bool is_auth_continuation_reply = auth_exchange_active_;
166
167 const std::string* response = NextControlReply();
168 if (response == nullptr) {
169 return;
170 }
171
172 const int response_code = FirstReplyCode(*response);
173 const bool response_written = WriteControlBytes(*response);
174
175 if (TokenEquals(verb, "STARTTLS") && IsStartTlsMode() && response_code == 220) {
176 if (response_written && mode_ == curl::fuzzer::proto::SMTP_PROBE_STARTTLS_FILTER_ALLOCATION_FAILURE) {
177 allocation_fault_injection::ArmNextAllocationFailure();
178 }
179 tls_upgrade_started_ = true;
182 return;
183 }
184
185 if (TokenEquals(verb, "AUTH")) {
186 // A 334 reply is the only wire-visible distinction between SMTP AUTH and
187 // a mutation-selected custom command that happens to start with AUTH.
188 auth_exchange_active_ = response_code == 334;
189 } else if (is_auth_continuation_reply) {
190 if (TokenEquals(verb, "*")) {
191 // Curl accepts the cancellation reply before trying another enabled
192 // SASL mechanism, so the exchange remains active across this error.
193 auth_exchange_active_ = true;
194 } else if (response_code != 334) {
195 auth_exchange_active_ = false;
196 }
197 }
198
199 if (TokenEquals(verb, "DATA")) {
200 if (response_code == 354) {
201 BeginMarkerBody(kEndOfBody);
202 (void)WriteNextReply();
203 } else if (response_code > 0) {
204 // curl treats every non-354 DATA reply as terminal. Pre-queue the
205 // shutdown reply so its blocking cleanup QUIT does not wait one second.
206 (void)WriteControlBytes(kShutdownReply);
207 }
208 return;
209 }
210
211 if (TokenEquals(verb, "RCPT")) {
212 ++rcpt_command_count_;
213 }
214
215 // A terminal command error is followed by curl's blocking QUIT during
216 // teardown. EHLO errors fall back to HELO, an early RCPT error may still be
217 // followed by another recipient, and a failed AUTH exchange can cancel and
218 // retry another mechanism. Those replies must not consume the shutdown
219 // reply. Everything else can safely see a queued 221 next.
220 const bool final_rcpt_error =
221 TokenEquals(verb, "RCPT") && (rcpt_command_count_ >= recipient_count_ || !AllowsRecipientFailures());
222 const bool starttls_denial = TokenEquals(verb, "STARTTLS") && IsStartTlsMode();
223 const bool terminal_error = response_code >= 400 && !TokenEquals(verb, "EHLO") && !starttls_denial &&
224 !is_auth_continuation_reply && (!TokenEquals(verb, "RCPT") || final_rcpt_error);
225 if (terminal_error) {
226 (void)WriteControlBytes(kShutdownReply);
227 }
228}
229
235 if (!tls_upgrade_started_) {
236 (void)WriteControlBytes(kShutdownReply);
237 }
238}
239
242#if defined(PROTO_FUZZER_OBSERVE_STATE)
244 const volatile std::size_t smtp_observation[] = {control_transcript().size(), uploaded_data().size()};
245 (void)smtp_observation;
246#endif
247}
248
249} // namespace proto_fuzzer
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
const std::string & control_transcript() const
Expose the bounded command capture from the most recent drive so tests can verify protocol states wit...
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 BeginOpaqueDrain()
Enter an opaque drain after a transport upgrade.
void ShutdownControlWrite()
Half-close the server-to-client direction while retaining the read half.
static std::string_view FirstToken(std::string_view line)
Remove optional leading horizontal whitespace and return the first token without allocating.
const std::string * NextControlReply()
Return the next command-aligned response within the shared response-count budget.
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.
SmtpMockServer(curl::fuzzer::proto::SmtpProbeMode mode=curl::fuzzer::proto::SMTP_PROBE_DEFAULT)
Begin without a borrowed protobuf or open socketpair.
~SmtpMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
std::unique_ptr< MockConnection > CreateConnection() override
Create the default plaintext transport for the SMTP exchange.
void HandleLine(std::string_view line) override
Advance one reply for every complete command.
void ResetProtocolState() override
Reset SMTP's recipient bookkeeping for a new drive.
void ConfigureClient(CURL *easy) const
Apply only closed, harness-owned client controls.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
void QueueShutdownReply() override
Queue a 221 before the shared transport half-closes.
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxMailRecipients
SMTP envelope recipients are schema-native because CURLOPT_MAIL_RCPT is a curl_slist.
Command-aware in-process peer for curl's SMTP state machine.