/src/curl_fuzzer/proto_fuzzer/smtp_mock_server.cc
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 Implementation of the command-aware in-process SMTP peer. |
9 | | |
10 | | #include "proto_fuzzer/smtp_mock_server.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <memory> |
15 | | #include <string> |
16 | | #include <string_view> |
17 | | |
18 | | #include "proto_fuzzer/mock_server.h" |
19 | | #include "proto_fuzzer/scenario_limits.h" |
20 | | |
21 | | namespace proto_fuzzer { |
22 | | |
23 | | /// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a |
24 | | /// Scenario only for the synchronous DriveScenario call in which socket |
25 | | /// callbacks can run. |
26 | 4.84k | SmtpMockServer::SmtpMockServer() : recipient_count_(0), rcpt_command_count_(0), auth_exchange_active_(false) {} |
27 | | |
28 | | /// Release the server-owned socketpair half after curl has returned its client |
29 | | /// descriptor. The out-of-line destructor keeps MockConnection's complete type |
30 | | /// local to this translation unit. |
31 | 4.84k | SmtpMockServer::~SmtpMockServer() = default; |
32 | | |
33 | | /// Create the default plaintext transport for the SMTP exchange. The SMTPS |
34 | | /// subclass overrides this factory while retaining the command/reply parser. |
35 | | /// @return A new socketpair-backed connection. |
36 | 2.35k | std::unique_ptr<MockConnection> SmtpMockServer::CreateConnection() { return std::make_unique<MockConnection>(); } |
37 | | |
38 | | /// Reset SMTP's recipient bookkeeping for a new drive. The shared base has |
39 | | /// already cleared the transport, reply cursor, transcript, upload capture, |
40 | | /// and any in-flight body phase. |
41 | 4.71k | void SmtpMockServer::ResetProtocolState() { |
42 | 4.71k | const curl::fuzzer::proto::Scenario* script = scenario(); |
43 | 4.71k | recipient_count_ = script == nullptr ? 0 |
44 | 4.71k | : std::min<std::size_t>(scenario_limits::kMaxMailRecipients, |
45 | 4.71k | static_cast<std::size_t>(script->mail_rcpt_size())); |
46 | 4.71k | rcpt_command_count_ = 0; |
47 | 4.71k | auth_exchange_active_ = false; |
48 | 4.71k | } |
49 | | |
50 | | /// Return whether the effective option list enables curl's |
51 | | /// CURLOPT_MAIL_RCPT_ALLOWFAILS behavior. Scan backwards because repeated |
52 | | /// setopt calls are legal and libcurl keeps the last value. |
53 | | /// @return true when recipient errors may be followed by another RCPT TO. |
54 | 500 | bool SmtpMockServer::AllowsRecipientFailures() const { |
55 | 500 | const curl::fuzzer::proto::Scenario* script = scenario(); |
56 | 500 | if (script == nullptr) { |
57 | 0 | return false; |
58 | 0 | } |
59 | 1.07k | for (int index = script->options_size() - 1; index >= 0; --index) { |
60 | 606 | const auto& option = script->options(index); |
61 | 606 | if (option.option_id() != curl::fuzzer::proto::CURLOPT_MAIL_RCPT_ALLOWFAILS) { |
62 | 577 | continue; |
63 | 577 | } |
64 | 29 | if (option.value_case() == curl::fuzzer::proto::SetOption::kBoolValue) { |
65 | 29 | return option.bool_value(); |
66 | 29 | } |
67 | 0 | if (option.value_case() == curl::fuzzer::proto::SetOption::kUintValue) { |
68 | 0 | return option.uint_value() != 0; |
69 | 0 | } |
70 | 0 | return false; |
71 | 0 | } |
72 | 471 | return false; |
73 | 500 | } |
74 | | |
75 | | /// Scan complete lines because a legal SMTP response fragment may start with |
76 | | /// one or more informational lines. curl ends a response at the first line |
77 | | /// whose first three bytes are digits and whose fourth byte is a space. |
78 | | /// @param response Raw control fragment. |
79 | | /// @return numeric status, or -1 when no terminating line is present. |
80 | 7.38k | int SmtpMockServer::FirstReplyCode(std::string_view response) { |
81 | 7.38k | std::size_t line_start = 0; |
82 | 83.8k | while (line_start < response.size()) { |
83 | 82.8k | const std::size_t newline = response.find('\n', line_start); |
84 | 82.8k | if (newline == std::string_view::npos) { |
85 | 993 | return -1; |
86 | 993 | } |
87 | 81.8k | const std::size_t line_size = newline - line_start + 1; |
88 | 81.8k | if (line_size > 3) { |
89 | 14.6k | const unsigned char first = static_cast<unsigned char>(response[line_start]); |
90 | 14.6k | const unsigned char second = static_cast<unsigned char>(response[line_start + 1]); |
91 | 14.6k | const unsigned char third = static_cast<unsigned char>(response[line_start + 2]); |
92 | 14.6k | if (first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' && |
93 | 10.4k | response[line_start + 3] == ' ') { |
94 | 5.38k | return 100 * (first - '0') + 10 * (second - '0') + (third - '0'); |
95 | 5.38k | } |
96 | 14.6k | } |
97 | 76.4k | line_start = newline + 1; |
98 | 76.4k | } |
99 | 1.00k | return -1; |
100 | 7.38k | } |
101 | | |
102 | | /// Advance one reply for every complete command. A successful DATA reply |
103 | | /// switches the parser into body-draining mode. The reply that follows DATA is |
104 | | /// queued immediately: curl's smtp_done() runs a blocking POSTDATA wait inside |
105 | | /// curl_multi_perform, so the outer peer service loop cannot run after the body |
106 | | /// has been written. Pre-queuing it, as the FTP peer does for its final |
107 | | /// transfer reply, keeps that wait deterministic without changing the order |
108 | | /// curl observes (354 first, then the post-body response). |
109 | | /// @param command One newline-terminated command from curl. |
110 | 995k | void SmtpMockServer::HandleLine(std::string_view command) { |
111 | 995k | const std::string_view verb = FirstToken(command); |
112 | 995k | const bool is_auth_continuation_reply = auth_exchange_active_; |
113 | | |
114 | 995k | const std::string* response = NextControlReply(); |
115 | 995k | if (response == nullptr) { |
116 | 987k | return; |
117 | 987k | } |
118 | | |
119 | 7.38k | const int response_code = FirstReplyCode(*response); |
120 | 7.38k | (void)WriteControlBytes(*response); |
121 | | |
122 | 7.38k | if (TokenEquals(verb, "AUTH")) { |
123 | | // A 334 reply is the only wire-visible distinction between SMTP AUTH and |
124 | | // a mutation-selected custom command that happens to start with AUTH. |
125 | 182 | auth_exchange_active_ = response_code == 334; |
126 | 7.20k | } else if (is_auth_continuation_reply) { |
127 | 151 | if (TokenEquals(verb, "*")) { |
128 | | // Curl accepts the cancellation reply before trying another enabled |
129 | | // SASL mechanism, so the exchange remains active across this error. |
130 | 40 | auth_exchange_active_ = true; |
131 | 111 | } else if (response_code != 334) { |
132 | 108 | auth_exchange_active_ = false; |
133 | 108 | } |
134 | 151 | } |
135 | | |
136 | 7.38k | if (TokenEquals(verb, "DATA")) { |
137 | 1.07k | if (response_code == 354) { |
138 | 610 | BeginMarkerBody(kEndOfBody); |
139 | 610 | (void)WriteNextReply(); |
140 | 610 | } else if (response_code > 0) { |
141 | | // curl treats every non-354 DATA reply as terminal. Pre-queue the |
142 | | // shutdown reply so its blocking cleanup QUIT does not wait one second. |
143 | 153 | (void)WriteControlBytes(kShutdownReply); |
144 | 153 | } |
145 | 1.07k | return; |
146 | 1.07k | } |
147 | | |
148 | 6.30k | if (TokenEquals(verb, "RCPT")) { |
149 | 1.65k | ++rcpt_command_count_; |
150 | 1.65k | } |
151 | | |
152 | | // A terminal command error is followed by curl's blocking QUIT during |
153 | | // teardown. EHLO errors fall back to HELO, an early RCPT error may still be |
154 | | // followed by another recipient, and a failed AUTH exchange can cancel and |
155 | | // retry another mechanism. Those replies must not consume the shutdown |
156 | | // reply. Everything else can safely see a queued 221 next. |
157 | 6.30k | const bool final_rcpt_error = |
158 | 6.30k | TokenEquals(verb, "RCPT") && (rcpt_command_count_ >= recipient_count_ || !AllowsRecipientFailures()); |
159 | 6.30k | const bool terminal_error = response_code >= 400 && !TokenEquals(verb, "EHLO") && !is_auth_continuation_reply && |
160 | 32 | (!TokenEquals(verb, "RCPT") || final_rcpt_error); |
161 | 6.30k | if (terminal_error) { |
162 | 26 | (void)WriteControlBytes(kShutdownReply); |
163 | 26 | } |
164 | 6.30k | } |
165 | | |
166 | | /// Queue a 221 before the shared transport half-closes. SMTP's blocking QUIT |
167 | | /// read has a one-second timeout when no reply is available, so even an |
168 | | /// unfinished transfer gets a liveness reply; any real scripted response |
169 | | /// already consumed by curl still determines the transfer result. |
170 | 4.29k | void SmtpMockServer::QueueShutdownReply() { (void)WriteControlBytes(kShutdownReply); } |
171 | | |
172 | | } // namespace proto_fuzzer |