curl-fuzzer C++ reference
Loading...
Searching...
No Matches
rtsp_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#include <utility>
19
21
22namespace proto_fuzzer {
23
24namespace {
25
26constexpr long kSplitRtpReadBufferBytes = 1024;
27constexpr char kCrossOriginReuseUrl[] = "rtsp://rtsp-other.test/session-reuse";
28
33std::string_view TrimLineEnding(std::string_view line) {
34 while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
35 line.remove_suffix(1);
36 }
37 return line;
38}
39
45bool EqualsIgnoreCase(std::string_view actual, std::string_view expected) {
46 if (actual.size() != expected.size()) {
47 return false;
48 }
49 for (std::size_t index = 0; index < actual.size(); ++index) {
50 unsigned char left = static_cast<unsigned char>(actual[index]);
51 unsigned char right = static_cast<unsigned char>(expected[index]);
52 if (left >= 'a' && left <= 'z') {
53 left = static_cast<unsigned char>(left - 'a' + 'A');
54 }
55 if (right >= 'a' && right <= 'z') {
56 right = static_cast<unsigned char>(right - 'a' + 'A');
57 }
58 if (left != right) {
59 return false;
60 }
61 }
62 return true;
63}
64
72bool HeaderValue(std::string_view line, std::string_view name, std::string_view* value) {
73 const std::string_view trimmed = TrimLineEnding(line);
74 if (trimmed.size() <= name.size() || trimmed[name.size()] != ':' ||
75 !EqualsIgnoreCase(trimmed.substr(0, name.size()), name)) {
76 return false;
77 }
78
79 std::size_t begin = name.size() + 1;
80 while (begin < trimmed.size() && (trimmed[begin] == ' ' || trimmed[begin] == '\t')) {
81 ++begin;
82 }
83 std::string_view parsed = trimmed.substr(begin);
84 while (!parsed.empty() && (parsed.back() == ' ' || parsed.back() == '\t')) {
85 parsed.remove_suffix(1);
86 }
87 *value = parsed;
88 return true;
89}
90
97bool ParseDecimal(std::string_view text, std::uint64_t* result) {
98 if (text.empty()) {
99 return false;
100 }
101 std::uint64_t value = 0;
102 for (const char byte : text) {
103 if (byte < '0' || byte > '9') {
104 return false;
105 }
106 const std::uint64_t digit = static_cast<std::uint64_t>(byte - '0');
107 if (value > (std::numeric_limits<std::uint64_t>::max() - digit) / 10U) {
108 value = std::numeric_limits<std::uint64_t>::max();
109 } else {
110 value = value * 10U + digit;
111 }
112 }
113 *result = value;
114 return true;
115}
116
120bool IsBlankLine(std::string_view line) { return TrimLineEnding(line).empty(); }
121
126curl::fuzzer::proto::Scenario SelectTransferResponse(const curl::fuzzer::proto::Scenario& scenario,
127 int response_index) {
128 curl::fuzzer::proto::Scenario selected = scenario;
129 auto* connection = selected.mutable_connection();
130 std::string response;
131 const bool has_response = response_index >= 0 && response_index < scenario.connection().on_readable_size();
132 if (has_response) {
133 response = scenario.connection().on_readable(response_index);
134 }
135 connection->clear_on_readable();
136 if (has_response) {
137 connection->add_on_readable(std::move(response));
138 }
139 return selected;
140}
141
142} // namespace
143
147 : in_request_(false),
148 in_headers_(false),
149 have_client_cseq_(false),
150 have_content_length_(false),
151 client_cseq_(0),
152 last_client_cseq_(0),
153 content_length_(0),
154 request_count_(0),
155 response_count_(0),
156 probe_mode_(curl::fuzzer::proto::RTSP_PROBE_DEFAULT),
157 client_write_failure_count_(0),
158 opened_transport_count_(0) {}
159
164
172void RtspMockServer::ConfigureClient(CURL* easy, curl::fuzzer::proto::RtspProbeMode mode) {
173 probe_mode_ = mode;
174 client_write_failure_count_ = 0;
175 opened_transport_count_ = 0;
176 if (easy == nullptr || mode != curl::fuzzer::proto::RTSP_PROBE_SPLIT_INVALID_RTP_WRITE_FAILURE) {
177 return;
178 }
179 (void)curl_easy_setopt(easy, CURLOPT_BUFFERSIZE, kSplitRtpReadBufferBytes);
180 (void)curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &RtspMockServer::ProbeWriteCallback);
181 (void)curl_easy_setopt(easy, CURLOPT_WRITEDATA, this);
182}
183
188std::size_t RtspMockServer::ProbeWriteCallback(char* contents, std::size_t size, std::size_t nmemb, void* userdata) {
189 auto* server = static_cast<RtspMockServer*>(userdata);
190 const std::size_t bytes = size * nmemb;
191 if (server != nullptr && server->client_write_failure_count_ == 0 && bytes == 1 && contents != nullptr &&
192 contents[0] == '$') {
193 ++server->client_write_failure_count_;
194 return 0;
195 }
196 return bytes;
197}
198
209CURLcode RtspMockServer::DriveCrossOriginReuseScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
210 if (easy == nullptr) {
211 return CURLE_FAILED_INIT;
212 }
213 CurlMultiPtr multi(curl_multi_init());
214 if (!multi) {
215 return CURLE_FAILED_INIT;
216 }
217
218 const auto drive = [&](const curl::fuzzer::proto::Scenario& transfer) {
219 CURLcode result = CURLE_FAILED_INIT;
220 if (curl_multi_add_handle(multi.get(), easy) != CURLM_OK) {
221 return result;
222 }
223 PingpongMockServer::RunLoop(multi.get(), easy, transfer);
224 int messages_remaining = 0;
225 CURLMsg* message = nullptr;
226 while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) {
227 if (message->msg == CURLMSG_DONE && message->easy_handle == easy) {
228 result = message->data.result;
229 }
230 }
231 (void)curl_multi_remove_handle(multi.get(), easy);
232 return result;
233 };
234
235 const curl::fuzzer::proto::Scenario first = SelectTransferResponse(scenario, 0);
236 const curl::fuzzer::proto::Scenario second = SelectTransferResponse(scenario, 1);
237 const curl::fuzzer::proto::Scenario third = SelectTransferResponse(scenario, 2);
238 CURLcode result = drive(first);
239 if (result != CURLE_OK) {
240 return result;
241 }
242 result = drive(second);
243 if (result != CURLE_OK) {
244 return result;
245 }
246 (void)curl_easy_setopt(easy, CURLOPT_URL, kCrossOriginReuseUrl);
247 return drive(third);
248}
249
253std::size_t RtspMockServer::request_count() const { return request_count_; }
254
258std::size_t RtspMockServer::response_count() const { return response_count_; }
259
263std::uint32_t RtspMockServer::last_client_cseq() const { return last_client_cseq_; }
264
266const std::string& RtspMockServer::last_request_line() const { return last_request_line_; }
267
269const std::string& RtspMockServer::last_method() const { return last_method_; }
270
272const std::string& RtspMockServer::last_stream_uri() const { return last_stream_uri_; }
273
276std::size_t RtspMockServer::client_write_failure_count() const { return client_write_failure_count_; }
277
279std::size_t RtspMockServer::opened_transport_count() const { return opened_transport_count_; }
280
289curl_socket_t RtspMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
290 const curl_socket_t socket = PingpongMockServer::HandleOpenSocket(purpose, address);
291 if (socket != CURL_SOCKET_BAD) {
292 ++opened_transport_count_;
293 }
294 return socket;
295}
296
301 in_request_ = false;
302 in_headers_ = false;
303 have_client_cseq_ = false;
304 have_content_length_ = false;
305 client_cseq_ = 0;
306 last_client_cseq_ = 0;
307 content_length_ = 0;
308 request_count_ = 0;
309 response_count_ = 0;
310 client_write_failure_count_ = 0;
311 last_request_line_.clear();
312 last_method_.clear();
313 last_stream_uri_.clear();
314}
315
319void RtspMockServer::BeginRequest(std::string_view line) {
320 const std::string_view trimmed = TrimLineEnding(line);
321 last_request_line_.assign(trimmed.data(), trimmed.size());
322 const std::string_view method = FirstToken(trimmed);
323 const std::string_view stream_uri = SecondToken(trimmed);
324 last_method_.assign(method.data(), method.size());
325 last_stream_uri_.assign(stream_uri.data(), stream_uri.size());
326
327 in_request_ = true;
328 in_headers_ = true;
329 have_client_cseq_ = false;
330 have_content_length_ = false;
331 client_cseq_ = 0;
332 content_length_ = 0;
333}
334
338void RtspMockServer::ParseHeader(std::string_view line) {
339 std::string_view value;
340 if (HeaderValue(line, "CSeq", &value)) {
341 std::uint64_t parsed = 0;
342 if (ParseDecimal(value, &parsed)) {
343 have_client_cseq_ = true;
344 client_cseq_ =
345 static_cast<std::uint32_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::uint32_t>::max()));
346 }
347 return;
348 }
349
350 if (HeaderValue(line, "Content-Length", &value)) {
351 std::uint64_t parsed = 0;
352 if (ParseDecimal(value, &parsed)) {
353 have_content_length_ = true;
354 content_length_ =
355 static_cast<std::size_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::size_t>::max()));
356 }
357 }
358}
359
363void RtspMockServer::FinishRequest() {
364 ++request_count_;
365 last_client_cseq_ = have_client_cseq_ ? client_cseq_ : 0;
366 bool wrote_response = false;
367 if (probe_mode_ == curl::fuzzer::proto::RTSP_PROBE_SPLIT_INVALID_RTP_WRITE_FAILURE) {
368 std::string response = "RTSP/1.0 200 OK\r\nCSeq: 1\r\nContent-Length: 4096\r\n\r\n";
369 if (response.size() < static_cast<std::size_t>(kSplitRtpReadBufferBytes)) {
370 response.append(static_cast<std::size_t>(kSplitRtpReadBufferBytes) - response.size() - 1, 'x');
371 response.push_back('$');
372 response.push_back(static_cast<char>(0xff));
373 wrote_response = WriteControlBytes(response);
374 }
375 } else {
376 wrote_response = WriteNextReply();
377 }
378 if (wrote_response) {
379 ++response_count_;
380 }
381
382 in_request_ = false;
383 in_headers_ = false;
384 have_client_cseq_ = false;
385 have_content_length_ = false;
386 client_cseq_ = 0;
387 content_length_ = 0;
388}
389
395void RtspMockServer::HandleLine(std::string_view line) {
396 if (!in_request_) {
397 BeginRequest(line);
398 return;
399 }
400
401 if (IsBlankLine(line)) {
402 in_headers_ = false;
403 if (have_content_length_ && content_length_ != 0) {
404 BeginExactBody(content_length_);
405 return;
406 }
407 FinishRequest();
408 return;
409 }
410
411 if (in_headers_) {
412 ParseHeader(line);
413 }
414}
415
417void RtspMockServer::HandleBodyComplete() { FinishRequest(); }
418
421#if defined(PROTO_FUZZER_OBSERVE_STATE)
423 const volatile std::size_t rtsp_observation[] = {request_count(),
426 control_transcript().size(),
427 last_request_line().size(),
428 last_stream_uri().size(),
431 (void)rtsp_observation;
432#endif
433}
434
435} // namespace proto_fuzzer
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Create the single control socketpair and queue the server greeting.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Alternate curl transitions with immediate peer service until the transfer completes or deterministic ...
const std::string & control_transcript() const
Expose the bounded command capture from the most recent drive so tests can verify protocol states wit...
void BeginExactBody(std::size_t length)
Enter exact-length body mode.
bool WriteNextReply()
Consume the next scripted reply.
const curl::fuzzer::proto::Scenario * scenario() const
Borrowed scenario for the synchronous drive, or nullptr outside RunLoop().
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.
bool WriteControlBytes(std::string_view bytes)
Keep failed peer writes local to the mock.
Services curl's single-socket RTSP exchange.
void HandleLine(std::string_view line) override
Consume one complete request line or header line.
std::size_t opened_transport_count() const
void HandleBodyComplete() override
Release the reply after an announced request body was drained exactly.
void ConfigureClient(CURL *easy, curl::fuzzer::proto::RtspProbeMode mode)
Install only the callback and receive-buffer boundary required by the split-RTP probe.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
std::size_t response_count() const
Expose how many scripted replies could be released before the bounded script ran out.
void ResetProtocolState() override
Clear RTSP's per-request parse state before the next drive.
RtspMockServer()
Begin without a borrowed protobuf or open socketpair.
std::size_t client_write_failure_count() const
~RtspMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
const std::string & last_method() const
std::size_t request_count() const
Expose how far the bounded request stream progressed.
CURLcode DriveCrossOriginReuseScenario(CURL *easy, const curl::fuzzer::proto::Scenario &scenario)
Run three ordinary OPTIONS-style exchanges through one multi connection cache.
std::uint32_t last_client_cseq() const
Expose the last parsed client sequence number so tests can verify that one request advanced the excha...
const std::string & last_stream_uri() const
const std::string & last_request_line() const
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Count successfully handed-off transports without changing the shared request-aligned socket construct...
Common ownership types for independently cleaned-up curl handles.
std::unique_ptr< CURLM, CurlMultiDeleter > CurlMultiPtr
Unique ownership of a CURL multi handle.
Definition curl_raii.h:58
Request-aligned in-process peer for curl's RTSP state machine.