curl-fuzzer C++ reference
Loading...
Searching...
No Matches
imap_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 <string>
17#include <string_view>
18
19namespace proto_fuzzer {
20
21namespace {
22
26bool AsciiEqualsIgnoreCase(std::string_view value, std::string_view expected_lowercase) {
27 if (value.size() != expected_lowercase.size()) {
28 return false;
29 }
30 for (std::size_t index = 0; index < value.size(); ++index) {
31 unsigned char actual = static_cast<unsigned char>(value[index]);
32 if (actual >= 'A' && actual <= 'Z') {
33 actual = static_cast<unsigned char>(actual - 'A' + 'a');
34 }
35 if (actual != static_cast<unsigned char>(expected_lowercase[index])) {
36 return false;
37 }
38 }
39 return true;
40}
41
42} // namespace
43
48
53
58 current_tag_.clear();
59 authentication_continuation_active_ = false;
60}
61
67bool ImapMockServer::StatusLineNeedsTag(std::string_view line) {
68 const std::size_t space = line.find_first_of(" \r\n");
69 const std::string_view keyword = line.substr(0, space);
70 if (keyword.empty()) {
71 return false;
72 }
73 if (!AsciiEqualsIgnoreCase(keyword, "ok") && !AsciiEqualsIgnoreCase(keyword, "no") &&
74 !AsciiEqualsIgnoreCase(keyword, "bad") && !AsciiEqualsIgnoreCase(keyword, "preauth")) {
75 return false;
76 }
77 return space != std::string_view::npos;
78}
79
85bool ImapMockServer::ResponseHasContinuation(std::string_view response) {
86 std::size_t position = 0;
87 while (position < response.size()) {
88 const std::size_t newline = response.find('\n', position);
89 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
90 const std::string_view line = response.substr(position, line_end - position);
91 if (!line.empty() && line.front() == '+') {
92 return true;
93 }
94
95 const std::size_t literal = LiteralSize(line);
96 const std::size_t available = response.size() - line_end;
97 position = line_end + std::min(literal, available);
98 }
99 return false;
100}
101
109bool ImapMockServer::ResponseHasTaggedCompletion(std::string_view response) const {
110 std::size_t position = 0;
111 while (position < response.size()) {
112 const std::size_t newline = response.find('\n', position);
113 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
114 const std::string_view line = response.substr(position, line_end - position);
115 const bool tag_relative = StatusLineNeedsTag(line);
116 const bool has_current_tag = !current_tag_.empty() && line.size() > current_tag_.size() &&
117 line.compare(0, current_tag_.size(), current_tag_) == 0 &&
118 line[current_tag_.size()] == ' ' &&
119 StatusLineNeedsTag(line.substr(current_tag_.size() + 1));
120 if (tag_relative || has_current_tag) {
121 return true;
122 }
123
124 const std::size_t literal = LiteralSize(line);
125 const std::size_t available = response.size() - line_end;
126 position = line_end + std::min(literal, available);
127 }
128 return false;
129}
130
134std::size_t ImapMockServer::LiteralSize(std::string_view line) {
135 while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
136 line.remove_suffix(1);
137 }
138 if (line.empty() || line.back() != '}') {
139 return 0;
140 }
141 const std::size_t open = line.rfind('{');
142 if (open == std::string_view::npos || open + 2 > line.size()) {
143 return 0;
144 }
145
146 std::size_t position = open + 1;
147 std::size_t value = 0;
148 bool digits = false;
149 while (position < line.size() && line[position] >= '0' && line[position] <= '9') {
150 const std::size_t digit = static_cast<std::size_t>(line[position] - '0');
151 if (value > (std::numeric_limits<std::size_t>::max() - digit) / 10) {
152 return 0;
153 }
154 value = value * 10 + digit;
155 digits = true;
156 ++position;
157 }
158 if (!digits) {
159 return 0;
160 }
161 if (position < line.size() && line[position] == '+') {
162 ++position;
163 }
164 return position + 1 == line.size() ? value : 0;
165}
166
172std::size_t ImapMockServer::AppendLiteralSize(std::string_view command) {
173 while (!command.empty() && (command.back() == '\r' || command.back() == '\n')) {
174 command.remove_suffix(1);
175 }
176 if (command.empty() || command.back() != '}') {
177 return std::numeric_limits<std::size_t>::max();
178 }
179 const std::size_t open = command.rfind('{');
180 if (open == std::string_view::npos) {
181 return std::numeric_limits<std::size_t>::max();
182 }
183
184 std::size_t position = open + 1;
185 std::size_t value = 0;
186 bool digits = false;
187 while (position < command.size() && command[position] >= '0' && command[position] <= '9') {
188 const std::size_t digit = static_cast<std::size_t>(command[position] - '0');
189 if (value > (std::numeric_limits<std::size_t>::max() - digit) / 10) {
190 return std::numeric_limits<std::size_t>::max();
191 }
192 value = value * 10 + digit;
193 digits = true;
194 ++position;
195 }
196 if (!digits) {
197 return std::numeric_limits<std::size_t>::max();
198 }
199 if (position < command.size() && command[position] == '+') {
200 ++position;
201 }
202 return position + 1 == command.size() ? value : std::numeric_limits<std::size_t>::max();
203}
204
210std::string ImapMockServer::RewriteReply(std::string_view response) const {
211 std::string output;
212 output.reserve(response.size() + 16);
213
214 std::size_t position = 0;
215 while (position < response.size()) {
216 const std::size_t newline = response.find('\n', position);
217 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
218 const std::string_view line = response.substr(position, line_end - position);
219
220 if (!current_tag_.empty() && StatusLineNeedsTag(line)) {
221 output.append(current_tag_);
222 output.push_back(' ');
223 }
224 output.append(line);
225
226 const std::size_t literal = LiteralSize(line);
227 if (literal == 0) {
228 position = line_end;
229 continue;
230 }
231
232 const std::size_t body_start = line_end;
233 const std::size_t available = response.size() - body_start;
234 const std::size_t body_size = std::min(literal, available);
235 output.append(response.substr(body_start, body_size));
236 position = body_start + body_size;
237 }
238 return output;
239}
240
247std::string ImapMockServer::NextCommandTag() const {
248 std::size_t digits_start = current_tag_.size();
249 while (digits_start > 0 && current_tag_[digits_start - 1] >= '0' && current_tag_[digits_start - 1] <= '9') {
250 --digits_start;
251 }
252 if (digits_start == current_tag_.size() || digits_start == 0) {
253 return std::string();
254 }
255
256 std::uint64_t number = 0;
257 for (std::size_t index = digits_start; index < current_tag_.size(); ++index) {
258 number = number * 10 + static_cast<std::uint64_t>(current_tag_[index] - '0');
259 if (number > 999999) {
260 return std::string();
261 }
262 }
263 ++number;
264
265 std::string digits = std::to_string(number);
266 const std::size_t width = current_tag_.size() - digits_start;
267 if (digits.size() < width) {
268 digits.insert(0, width - digits.size(), '0');
269 }
270 return current_tag_.substr(0, digits_start) + digits;
271}
272
280void ImapMockServer::HandleLine(std::string_view command) {
281 if (scenario() == nullptr) {
282 return;
283 }
284
285 const bool authentication_payload = authentication_continuation_active_;
286 if (!authentication_payload) {
287 current_tag_ = std::string(FirstToken(command));
288 }
289
290 const std::string* response = NextControlReply();
291 if (response == nullptr) {
292 return;
293 }
294
295 const bool continuation = ResponseHasContinuation(*response);
296 const bool tagged_completion = ResponseHasTaggedCompletion(*response);
297 (void)WriteControlBytes(RewriteReply(*response));
298
299 if (authentication_payload) {
300 if (tagged_completion) {
301 authentication_continuation_active_ = false;
302 }
303 return;
304 }
305
306 const std::string_view verb = SecondToken(command);
307 if (continuation && TokenEquals(verb, "AUTHENTICATE")) {
308 authentication_continuation_active_ = !tagged_completion;
309 return;
310 }
311
312 if (!continuation || !TokenEquals(verb, "APPEND")) {
313 return;
314 }
315
316 const std::size_t literal = AppendLiteralSize(command);
317 if (literal == std::numeric_limits<std::size_t>::max()) {
318 // Curl always announces the literal size here. Without one, leave the
319 // transfer to the bounded idle budget and final half-close instead of
320 // guessing where an arbitrary body ends.
321 return;
322 }
323
324 BeginLiteralBody(literal);
325
326 const std::string* final_response = NextControlReply();
327 if (final_response != nullptr) {
328 (void)WriteControlBytes(RewriteReply(*final_response));
329 }
330}
331
336 const std::string next_tag = NextCommandTag();
337 if (!next_tag.empty()) {
338 (void)WriteControlBytes(next_tag + std::string(kShutdownTail));
339 } else {
340 (void)WriteControlBytes("* BYE imap mock closing\r\n");
341 }
342}
343
346#if defined(PROTO_FUZZER_OBSERVE_STATE)
348 const volatile std::size_t imap_observation[] = {control_transcript().size(), uploaded_data().size()};
349 (void)imap_observation;
350#endif
351}
352
353} // namespace proto_fuzzer
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
void HandleLine(std::string_view line) override
Advance one reply for every complete command or AUTHENTICATE continuation.
void QueueShutdownReply() override
Queue a tagged completion for curl's blocking LOGOUT during teardown.
~ImapMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
void ResetProtocolState() override
Clear the tag remembered from the previous drive.
ImapMockServer()
Begin without a borrowed protobuf or open socketpair.
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...
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.
static std::string_view SecondToken(std::string_view line)
Skip the first token and return the second whitespace-delimited token without allocating.
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.
Command-aware in-process peer for curl's IMAP state machine.