/src/curl_fuzzer/proto_fuzzer/rtsp_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 request-aligned in-process RTSP peer. |
9 | | |
10 | | #include "proto_fuzzer/rtsp_mock_server.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <cstdint> |
15 | | #include <limits> |
16 | | #include <string> |
17 | | #include <string_view> |
18 | | |
19 | | namespace proto_fuzzer { |
20 | | |
21 | | namespace { |
22 | | |
23 | | /// Remove a command line's trailing CR/LF so header parsing does not treat the |
24 | | /// terminator as part of a value. |
25 | | /// @param line Complete newline-terminated line. |
26 | | /// @return The line without its line ending. |
27 | 136k | std::string_view TrimLineEnding(std::string_view line) { |
28 | 365k | while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) { |
29 | 228k | line.remove_suffix(1); |
30 | 228k | } |
31 | 136k | return line; |
32 | 136k | } |
33 | | |
34 | | /// Compare two ASCII tokens ignoring case. Header names are case-insensitive, |
35 | | /// and protocol bytes outside ASCII have no valid interpretation here. |
36 | | /// @param actual Observed token. |
37 | | /// @param expected Literal comparison token. |
38 | | /// @return true when both tokens contain the same case-folded ASCII bytes. |
39 | 15.8k | bool EqualsIgnoreCase(std::string_view actual, std::string_view expected) { |
40 | 15.8k | if (actual.size() != expected.size()) { |
41 | 0 | return false; |
42 | 0 | } |
43 | 101k | for (std::size_t index = 0; index < actual.size(); ++index) { |
44 | 88.0k | unsigned char left = static_cast<unsigned char>(actual[index]); |
45 | 88.0k | unsigned char right = static_cast<unsigned char>(expected[index]); |
46 | 88.0k | if (left >= 'a' && left <= 'z') { |
47 | 55.3k | left = static_cast<unsigned char>(left - 'a' + 'A'); |
48 | 55.3k | } |
49 | 88.0k | if (right >= 'a' && right <= 'z') { |
50 | 55.1k | right = static_cast<unsigned char>(right - 'a' + 'A'); |
51 | 55.1k | } |
52 | 88.0k | if (left != right) { |
53 | 2.58k | return false; |
54 | 2.58k | } |
55 | 88.0k | } |
56 | 13.3k | return true; |
57 | 15.8k | } |
58 | | |
59 | | /// Extract a header value from one complete header line. Only an exact |
60 | | /// case-insensitive field name followed by a colon matches, so a value that |
61 | | /// happens to contain the same text cannot be mistaken for the header. |
62 | | /// @param line Complete header line, including its line ending. |
63 | | /// @param name Case-insensitive field name without the colon. |
64 | | /// @param value Receives the trimmed value when the header matches. |
65 | | /// @return true when the field name matched. |
66 | 65.7k | bool HeaderValue(std::string_view line, std::string_view name, std::string_view* value) { |
67 | 65.7k | const std::string_view trimmed = TrimLineEnding(line); |
68 | 65.7k | if (trimmed.size() <= name.size() || trimmed[name.size()] != ':' || |
69 | 52.4k | !EqualsIgnoreCase(trimmed.substr(0, name.size()), name)) { |
70 | 52.4k | return false; |
71 | 52.4k | } |
72 | | |
73 | 13.3k | std::size_t begin = name.size() + 1; |
74 | 49.3k | while (begin < trimmed.size() && (trimmed[begin] == ' ' || trimmed[begin] == '\t')) { |
75 | 36.0k | ++begin; |
76 | 36.0k | } |
77 | 13.3k | std::string_view parsed = trimmed.substr(begin); |
78 | 19.4k | while (!parsed.empty() && (parsed.back() == ' ' || parsed.back() == '\t')) { |
79 | 6.10k | parsed.remove_suffix(1); |
80 | 6.10k | } |
81 | 13.3k | *value = parsed; |
82 | 13.3k | return true; |
83 | 65.7k | } |
84 | | |
85 | | /// Parse a non-negative decimal field without relying on locale-sensitive or |
86 | | /// throwing library conversions. Oversized values saturate, because the |
87 | | /// caller only needs a bounded amount of subsequent work. |
88 | | /// @param text Decimal digits with no sign or other characters. |
89 | | /// @param result Receives the parsed or saturated value. |
90 | | /// @return true when every byte was a decimal digit and the text was nonempty. |
91 | 13.3k | bool ParseDecimal(std::string_view text, std::uint64_t* result) { |
92 | 13.3k | if (text.empty()) { |
93 | 847 | return false; |
94 | 847 | } |
95 | 12.4k | std::uint64_t value = 0; |
96 | 26.8k | for (const char byte : text) { |
97 | 26.8k | if (byte < '0' || byte > '9') { |
98 | 1.80k | return false; |
99 | 1.80k | } |
100 | 25.0k | const std::uint64_t digit = static_cast<std::uint64_t>(byte - '0'); |
101 | 25.0k | if (value > (std::numeric_limits<std::uint64_t>::max() - digit) / 10U) { |
102 | 1.22k | value = std::numeric_limits<std::uint64_t>::max(); |
103 | 23.7k | } else { |
104 | 23.7k | value = value * 10U + digit; |
105 | 23.7k | } |
106 | 25.0k | } |
107 | 10.6k | *result = value; |
108 | 10.6k | return true; |
109 | 12.4k | } |
110 | | |
111 | | /// Recognize the empty line that terminates an RTSP request header block. |
112 | | /// @param line Complete line, including its line ending. |
113 | | /// @return true when the line contains no bytes before its line ending. |
114 | 54.4k | bool IsBlankLine(std::string_view line) { return TrimLineEnding(line).empty(); } |
115 | | |
116 | | } // namespace |
117 | | |
118 | | /// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a |
119 | | /// Scenario only for the synchronous drive in which socket callbacks can run. |
120 | | RtspMockServer::RtspMockServer() |
121 | 6.30k | : in_request_(false), |
122 | 6.30k | in_headers_(false), |
123 | 6.30k | have_client_cseq_(false), |
124 | 6.30k | have_content_length_(false), |
125 | 6.30k | client_cseq_(0), |
126 | 6.30k | last_client_cseq_(0), |
127 | 6.30k | content_length_(0), |
128 | 6.30k | request_count_(0), |
129 | 6.30k | response_count_(0) {} |
130 | | |
131 | | /// Release the server-owned socketpair half after curl has returned its client |
132 | | /// descriptor. The out-of-line destructor keeps inherited transport ownership |
133 | | /// local to this translation unit. |
134 | 6.30k | RtspMockServer::~RtspMockServer() = default; |
135 | | |
136 | | /// Expose how far the bounded request stream progressed. The parser counts a |
137 | | /// request only after its header block and any announced body were consumed. |
138 | | /// @return Complete requests observed in the most recent drive. |
139 | 6.27k | std::size_t RtspMockServer::request_count() const { return request_count_; } |
140 | | |
141 | | /// Expose how many scripted replies could be released before the bounded |
142 | | /// script ran out. |
143 | | /// @return Scripted replies written in the most recent drive. |
144 | 6.27k | std::size_t RtspMockServer::response_count() const { return response_count_; } |
145 | | |
146 | | /// Expose the last parsed client sequence number so tests can verify that one |
147 | | /// request advanced the exchange exactly once. |
148 | | /// @return Last request CSeq, or zero when no parseable CSeq was present. |
149 | 6.27k | std::uint32_t RtspMockServer::last_client_cseq() const { return last_client_cseq_; } |
150 | | |
151 | | /// @return Start line of the most recently completed request. |
152 | 6.27k | const std::string& RtspMockServer::last_request_line() const { return last_request_line_; } |
153 | | |
154 | | /// @return Method token of the most recently completed request. |
155 | 0 | const std::string& RtspMockServer::last_method() const { return last_method_; } |
156 | | |
157 | | /// @return Stream URI token of the most recently completed request. |
158 | 6.27k | const std::string& RtspMockServer::last_stream_uri() const { return last_stream_uri_; } |
159 | | |
160 | | /// Clear RTSP's per-request parse state before the next drive. The shared base |
161 | | /// has already reset the transport, reply cursor, transcript, upload capture, |
162 | | /// and body mode. |
163 | 6.17k | void RtspMockServer::ResetProtocolState() { |
164 | 6.17k | in_request_ = false; |
165 | 6.17k | in_headers_ = false; |
166 | 6.17k | have_client_cseq_ = false; |
167 | 6.17k | have_content_length_ = false; |
168 | 6.17k | client_cseq_ = 0; |
169 | 6.17k | last_client_cseq_ = 0; |
170 | 6.17k | content_length_ = 0; |
171 | 6.17k | request_count_ = 0; |
172 | 6.17k | response_count_ = 0; |
173 | 6.17k | last_request_line_.clear(); |
174 | 6.17k | last_method_.clear(); |
175 | 6.17k | last_stream_uri_.clear(); |
176 | 6.17k | } |
177 | | |
178 | | /// Start a new request after the previous one completed. The request line is |
179 | | /// parsed for observability, while the reply itself stays scripted. |
180 | | /// @param line Complete request start line, including its line ending. |
181 | 16.7k | void RtspMockServer::BeginRequest(std::string_view line) { |
182 | 16.7k | const std::string_view trimmed = TrimLineEnding(line); |
183 | 16.7k | last_request_line_.assign(trimmed.data(), trimmed.size()); |
184 | 16.7k | const std::string_view method = FirstToken(trimmed); |
185 | 16.7k | const std::string_view stream_uri = SecondToken(trimmed); |
186 | 16.7k | last_method_.assign(method.data(), method.size()); |
187 | 16.7k | last_stream_uri_.assign(stream_uri.data(), stream_uri.size()); |
188 | | |
189 | 16.7k | in_request_ = true; |
190 | 16.7k | in_headers_ = true; |
191 | 16.7k | have_client_cseq_ = false; |
192 | 16.7k | have_content_length_ = false; |
193 | 16.7k | client_cseq_ = 0; |
194 | 16.7k | content_length_ = 0; |
195 | 16.7k | } |
196 | | |
197 | | /// Parse the two headers that change how the peer stays synchronized. Other |
198 | | /// headers remain part of the transcript and are otherwise opaque to the peer. |
199 | | /// @param line Complete header line, including its line ending. |
200 | 38.0k | void RtspMockServer::ParseHeader(std::string_view line) { |
201 | 38.0k | std::string_view value; |
202 | 38.0k | if (HeaderValue(line, "CSeq", &value)) { |
203 | 10.3k | std::uint64_t parsed = 0; |
204 | 10.3k | if (ParseDecimal(value, &parsed)) { |
205 | 8.37k | have_client_cseq_ = true; |
206 | 8.37k | client_cseq_ = |
207 | 8.37k | static_cast<std::uint32_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::uint32_t>::max())); |
208 | 8.37k | } |
209 | 10.3k | return; |
210 | 10.3k | } |
211 | | |
212 | 27.7k | if (HeaderValue(line, "Content-Length", &value)) { |
213 | 3.00k | std::uint64_t parsed = 0; |
214 | 3.00k | if (ParseDecimal(value, &parsed)) { |
215 | 2.27k | have_content_length_ = true; |
216 | 2.27k | content_length_ = |
217 | 2.27k | static_cast<std::size_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::size_t>::max())); |
218 | 2.27k | } |
219 | 3.00k | } |
220 | 27.7k | } |
221 | | |
222 | | /// Complete one request: advance the scripted reply cursor, record the parsed |
223 | | /// exchange, and return to start-line mode. An empty scripted reply still |
224 | | /// advances the cursor because it is a meaningful truncation mutation. |
225 | 16.2k | void RtspMockServer::FinishRequest() { |
226 | 16.2k | ++request_count_; |
227 | 16.2k | last_client_cseq_ = have_client_cseq_ ? client_cseq_ : 0; |
228 | 16.2k | if (WriteNextReply()) { |
229 | 3.78k | ++response_count_; |
230 | 3.78k | } |
231 | | |
232 | 16.2k | in_request_ = false; |
233 | 16.2k | in_headers_ = false; |
234 | 16.2k | have_client_cseq_ = false; |
235 | 16.2k | have_content_length_ = false; |
236 | 16.2k | client_cseq_ = 0; |
237 | 16.2k | content_length_ = 0; |
238 | 16.2k | } |
239 | | |
240 | | /// Consume one complete request line or header line. The blank line that ends |
241 | | /// the header block is the reply trigger: an announced body first switches the |
242 | | /// shared parser into exact-length mode, whose completion calls |
243 | | /// HandleBodyComplete(). |
244 | | /// @param line Complete line sent by curl. |
245 | 71.2k | void RtspMockServer::HandleLine(std::string_view line) { |
246 | 71.2k | if (!in_request_) { |
247 | 16.7k | BeginRequest(line); |
248 | 16.7k | return; |
249 | 16.7k | } |
250 | | |
251 | 54.4k | if (IsBlankLine(line)) { |
252 | 16.4k | in_headers_ = false; |
253 | 16.4k | if (have_content_length_ && content_length_ != 0) { |
254 | 1.53k | BeginExactBody(content_length_); |
255 | 1.53k | return; |
256 | 1.53k | } |
257 | 14.9k | FinishRequest(); |
258 | 14.9k | return; |
259 | 16.4k | } |
260 | | |
261 | 38.0k | if (in_headers_) { |
262 | 38.0k | ParseHeader(line); |
263 | 38.0k | } |
264 | 38.0k | } |
265 | | |
266 | | /// Release the reply after an announced request body was drained exactly. |
267 | 1.27k | void RtspMockServer::HandleBodyComplete() { FinishRequest(); } |
268 | | |
269 | | } // namespace proto_fuzzer |