22namespace proto_fuzzer {
26constexpr long kSplitRtpReadBufferBytes = 1024;
27constexpr char kCrossOriginReuseUrl[] =
"rtsp://rtsp-other.test/session-reuse";
33std::string_view TrimLineEnding(std::string_view line) {
34 while (!line.empty() && (line.back() ==
'\n' || line.back() ==
'\r')) {
35 line.remove_suffix(1);
45bool EqualsIgnoreCase(std::string_view actual, std::string_view expected) {
46 if (actual.size() != expected.size()) {
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');
55 if (right >=
'a' && right <=
'z') {
56 right =
static_cast<unsigned char>(right -
'a' +
'A');
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)) {
79 std::size_t begin = name.size() + 1;
80 while (begin < trimmed.size() && (trimmed[begin] ==
' ' || trimmed[begin] ==
'\t')) {
83 std::string_view parsed = trimmed.substr(begin);
84 while (!parsed.empty() && (parsed.back() ==
' ' || parsed.back() ==
'\t')) {
85 parsed.remove_suffix(1);
97bool ParseDecimal(std::string_view text, std::uint64_t* result) {
101 std::uint64_t value = 0;
102 for (
const char byte : text) {
103 if (
byte <
'0' ||
byte >
'9') {
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();
110 value = value * 10U + digit;
120bool IsBlankLine(std::string_view line) {
return TrimLineEnding(line).empty(); }
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();
133 response = scenario.connection().on_readable(response_index);
135 connection->clear_on_readable();
137 connection->add_on_readable(std::move(response));
147 : in_request_(false),
149 have_client_cseq_(false),
150 have_content_length_(false),
152 last_client_cseq_(0),
156 probe_mode_(curl::fuzzer::proto::RTSP_PROBE_DEFAULT),
157 client_write_failure_count_(0),
158 opened_transport_count_(0) {}
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) {
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);
188std::size_t RtspMockServer::ProbeWriteCallback(
char* contents, std::size_t size, std::size_t nmemb,
void* 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_;
210 if (easy ==
nullptr) {
211 return CURLE_FAILED_INIT;
215 return CURLE_FAILED_INIT;
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) {
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;
231 (void)curl_multi_remove_handle(multi.get(), easy);
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) {
242 result = drive(second);
243 if (result != CURLE_OK) {
246 (void)curl_easy_setopt(easy, CURLOPT_URL, kCrossOriginReuseUrl);
291 if (socket != CURL_SOCKET_BAD) {
292 ++opened_transport_count_;
303 have_client_cseq_ =
false;
304 have_content_length_ =
false;
306 last_client_cseq_ = 0;
310 client_write_failure_count_ = 0;
311 last_request_line_.clear();
312 last_method_.clear();
313 last_stream_uri_.clear();
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());
329 have_client_cseq_ =
false;
330 have_content_length_ =
false;
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;
345 static_cast<std::uint32_t
>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::uint32_t>::max()));
350 if (HeaderValue(line,
"Content-Length", &value)) {
351 std::uint64_t parsed = 0;
352 if (ParseDecimal(value, &parsed)) {
353 have_content_length_ =
true;
355 static_cast<std::size_t
>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::size_t>::max()));
363void RtspMockServer::FinishRequest() {
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));
378 if (wrote_response) {
384 have_client_cseq_ =
false;
385 have_content_length_ =
false;
401 if (IsBlankLine(line)) {
403 if (have_content_length_ && content_length_ != 0) {
421#if defined(PROTO_FUZZER_OBSERVE_STATE)
423 const volatile std::size_t rtsp_observation[] = {
request_count(),
431 (void)rtsp_observation;
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.
Request-aligned in-process peer for curl's RTSP state machine.