/src/curl_fuzzer/proto_fuzzer/websocket_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 WebSocketMockServer. |
9 | | |
10 | | #include "proto_fuzzer/websocket_mock_server.h" |
11 | | |
12 | | #include <curl/websockets.h> |
13 | | |
14 | | #include <algorithm> |
15 | | #include <cstddef> |
16 | | #include <cstdint> |
17 | | #include <string> |
18 | | #include <utility> |
19 | | |
20 | | #include "proto_fuzzer/option_apply.h" |
21 | | #include "proto_fuzzer/scenario_limits.h" |
22 | | #include "proto_fuzzer/ws_accept_key.h" |
23 | | #include "proto_fuzzer/ws_frame.h" |
24 | | |
25 | | namespace proto_fuzzer { |
26 | | |
27 | | namespace { |
28 | | |
29 | | // Iteration caps for the post-handshake manual WS drive. Kept small because |
30 | | // every iteration makes a curl_ws_recv / curl_ws_send call. |
31 | | constexpr std::size_t kMaxWsRecvIterations = 128; |
32 | | constexpr std::size_t kMaxWsSendIterations = 32; |
33 | | |
34 | | /// Ordered list of flag combinations fed to curl_ws_send / curl_ws_start_frame |
35 | | /// during the manual-drive tail. Sequencing matters: the TEXT|CONT entry puts |
36 | | /// the encoder into `contfragment=true`, so subsequent CONT / TEXT|CONT / |
37 | | /// BINARY|CONT entries then take the contfragment-aware branches in |
38 | | /// ws_frame_flags2firstbyte that would otherwise be unreachable from the |
39 | | /// fuzzer. A lone CURLWS_CONT runs first (contfragment=false → "No ongoing |
40 | | /// fragmented message" failf), and a bare `0` directly after CURLWS_CONT |
41 | | /// drives the "no flags given; interpreting as continuation" compatibility |
42 | | /// path. (TEXT|BINARY), (CLOSE|CONT), (PING|CONT), (PONG|CONT) cover the |
43 | | /// invalid-combination failf() branches; the trailing lone `0` reaches the |
44 | | /// ordinary "no flags given" rejection with contfragment=false. |
45 | | constexpr unsigned int kWsSendFlagMatrix[] = { |
46 | | CURLWS_CONT, |
47 | | CURLWS_TEXT, |
48 | | CURLWS_BINARY, |
49 | | CURLWS_TEXT | CURLWS_OFFSET, |
50 | | CURLWS_BINARY | CURLWS_OFFSET, |
51 | | CURLWS_TEXT | CURLWS_CONT, |
52 | | CURLWS_CONT, |
53 | | 0, |
54 | | CURLWS_TEXT, |
55 | | CURLWS_BINARY | CURLWS_CONT, |
56 | | CURLWS_BINARY, |
57 | | CURLWS_PING, |
58 | | CURLWS_PONG, |
59 | | CURLWS_CLOSE, |
60 | | 0, |
61 | | CURLWS_CLOSE | CURLWS_CONT, |
62 | | CURLWS_PING | CURLWS_CONT, |
63 | | CURLWS_PONG | CURLWS_CONT, |
64 | | CURLWS_TEXT | CURLWS_BINARY, |
65 | | }; |
66 | | |
67 | | /// Find "Sec-WebSocket-Key:" in the request and return the trimmed value. |
68 | | /// @param request The raw HTTP request bytes buffered from the client. |
69 | | /// @return The header value, or an empty string if not found. |
70 | 1.55k | std::string ExtractWebSocketKey(const std::string& request) { |
71 | 1.55k | static const char kHeader[] = "Sec-WebSocket-Key:"; |
72 | 1.55k | std::size_t pos = request.find(kHeader); |
73 | 1.55k | if (pos == std::string::npos) { |
74 | 2 | return {}; |
75 | 2 | } |
76 | 1.55k | pos += sizeof(kHeader) - 1; |
77 | 4.14k | while (pos < request.size() && (request[pos] == ' ' || request[pos] == '\t')) { |
78 | 2.59k | ++pos; |
79 | 2.59k | } |
80 | 1.55k | std::size_t end = request.find("\r\n", pos); |
81 | 1.55k | if (end == std::string::npos) { |
82 | 5 | return {}; |
83 | 5 | } |
84 | 2.17k | while (end > pos && (request[end - 1] == ' ' || request[end - 1] == '\t')) { |
85 | 628 | --end; |
86 | 628 | } |
87 | 1.54k | return request.substr(pos, end - pos); |
88 | 1.55k | } |
89 | | |
90 | | /// SHA1(key + WS magic guid), base64-encoded — RFC 6455 §4.2.2. |
91 | | /// Delegates to the standalone implementation in ws_accept_key.h so we don't |
92 | | /// need OpenSSL. |
93 | 1.54k | std::string ComputeWebSocketAccept(const std::string& key) { return proto_fuzzer::ComputeWebSocketAcceptKey(key); } |
94 | | |
95 | | /// Build the ordered list of chunks to deliver once the 101 handshake has |
96 | | /// completed. Mixes raw `on_readable` bytes (fuzzer-controlled) with serialised |
97 | | /// `server_frames` (structured RFC 6455 frames from the proto) under a shared |
98 | | /// shared response-chunk budget. |
99 | 2.21k | std::vector<std::string> BuildFrameChunks(const curl::fuzzer::proto::Connection& conn) { |
100 | 2.21k | std::vector<std::string> chunks; |
101 | 2.21k | chunks.reserve(scenario_limits::kMaxResponseChunks); |
102 | 2.21k | const std::size_t raw_budget = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, conn.on_readable_size()); |
103 | 4.21k | for (std::size_t i = 0; i < raw_budget; ++i) { |
104 | 1.99k | chunks.emplace_back(conn.on_readable(i)); |
105 | 1.99k | } |
106 | 2.21k | const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - chunks.size(); |
107 | 2.21k | const std::size_t frame_count = std::min<std::size_t>(frame_budget, conn.server_frames_size()); |
108 | 4.59k | for (std::size_t i = 0; i < frame_count; ++i) { |
109 | 2.37k | chunks.emplace_back(SerializeWebSocketFrame(conn.server_frames(static_cast<int>(i)))); |
110 | 2.37k | } |
111 | 2.21k | return chunks; |
112 | 2.21k | } |
113 | | |
114 | | /// WRITEFUNCTION / HEADERFUNCTION installed on the easy handle by |
115 | | /// WebSocketMockServer::Install. Pokes curl_ws_meta on every invocation (so |
116 | | /// the Curl_is_in_callback-guarded branch stays covered), and fires a |
117 | | /// one-shot curl_ws_send probe to reach ws_send_raw_blocking — that target |
118 | | /// is unreachable unless CURLWS_RAW_MODE is set AND the caller is inside a |
119 | | /// callback. The probe is sized larger than typical backpressure recv |
120 | | /// buffers so the partial-write / SOCKET_WRITABLE loop engages under a |
121 | | /// tightened SO_RCVBUF. The one-shot gate keeps the per-scenario cost |
122 | | /// bounded when SOCKET_WRITABLE times out. WRITEDATA is the owning |
123 | | /// WebSocketMockServer so callback state lives on the server, not globals. |
124 | 12.7k | size_t WebSocketWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* userdata) { |
125 | 12.7k | auto* server = static_cast<WebSocketMockServer*>(userdata); |
126 | 12.7k | if (server != nullptr) { |
127 | 4.99k | CURL* easy = server->easy_handle(); |
128 | 4.99k | if (easy != nullptr) { |
129 | 4.99k | (void)curl_ws_meta(easy); |
130 | 4.99k | if (!server->ws_probe_fired()) { |
131 | 348 | server->MarkWsProbeFired(); |
132 | 348 | static unsigned char kProbe[16384]; |
133 | 348 | std::fill(kProbe, kProbe + sizeof(kProbe), 'P'); |
134 | 348 | std::size_t sent = 0; |
135 | 348 | (void)curl_ws_send(easy, kProbe, sizeof(kProbe), &sent, 0, 0); |
136 | 348 | } |
137 | 4.99k | } |
138 | 4.99k | } |
139 | 12.7k | return size * nmemb; |
140 | 12.7k | } |
141 | | |
142 | | /// Drain curl_ws_recv in a tight loop until it returns CURLE_AGAIN / nothing |
143 | | /// pending. Bounded; not expected to do anything on well-formed scenarios. |
144 | 2.53k | void DrainWsRecv(CURL* easy) { |
145 | 9.52k | for (std::size_t i = 0; i < kMaxWsRecvIterations; ++i) { |
146 | 9.50k | unsigned char buffer[4096]; |
147 | 9.50k | std::size_t nread = 0; |
148 | 9.50k | const struct curl_ws_frame* meta = nullptr; |
149 | 9.50k | CURLcode rr = curl_ws_recv(easy, buffer, sizeof(buffer), &nread, &meta); |
150 | 9.50k | if (rr == CURLE_AGAIN) { |
151 | 1.60k | break; |
152 | 1.60k | } |
153 | 7.89k | if (rr != CURLE_OK && rr != CURLE_GOT_NOTHING) { |
154 | 903 | break; |
155 | 903 | } |
156 | 6.99k | if (nread == 0 && meta == nullptr) { |
157 | 0 | break; |
158 | 0 | } |
159 | 6.99k | } |
160 | 2.53k | } |
161 | | |
162 | | } // namespace |
163 | | |
164 | | /// Mirror the sequential CURLOPT_CONNECT_ONLY setopts applied by |
165 | | /// ApplyScenarioOptions. Values above 2 are rejected by libcurl and therefore |
166 | | /// cannot replace the last accepted setting. Use the same descriptor-aware |
167 | | /// scalar decoder as ApplySetOption so compatibility inputs and cross-family |
168 | | /// mutations cannot make curl and its mock disagree about manual delivery. |
169 | | /// Restricting the scan to RuntimeOptionCount also prevents a compatibility- |
170 | | /// only suffix from changing mock delivery after curl stops observing options. |
171 | 2.21k | bool ScenarioRequestsManualWsDrive(const curl::fuzzer::proto::Scenario& scenario) { |
172 | 2.21k | bool manual_delivery = false; |
173 | 2.21k | const std::size_t option_count = RuntimeOptionCount(scenario); |
174 | 8.43k | for (std::size_t index = 0; index < option_count; ++index) { |
175 | 6.21k | const auto& option = scenario.options(static_cast<int>(index)); |
176 | 6.21k | if (option.option_id() != curl::fuzzer::proto::CURLOPT_CONNECT_ONLY) { |
177 | 5.18k | continue; |
178 | 5.18k | } |
179 | 1.02k | const std::uint64_t value = DecodeIntegralOptionValue(option); |
180 | 1.02k | if (value <= 2) { |
181 | 861 | manual_delivery = value == 2; |
182 | 861 | } |
183 | 1.02k | } |
184 | 2.21k | return manual_delivery; |
185 | 2.21k | } |
186 | | |
187 | | /// Construct an idle WebSocketMockServer with no queued frames. Install() |
188 | | /// on the base class and DriveScenario() configure it from a Scenario proto. |
189 | | WebSocketMockServer::WebSocketMockServer() |
190 | 2.21k | : next_chunk_(0), manual_delivery_(false), handshake_sent_(false), ws_probe_fired_(false), easy_handle_(nullptr) {} |
191 | | |
192 | | /// Default destructor; the owned MockConnection (if any) cleans up its socketpair. |
193 | 2.21k | WebSocketMockServer::~WebSocketMockServer() = default; |
194 | | |
195 | | /// Install the common socket callbacks via the base, then overwrite |
196 | | /// WRITEFUNCTION / HEADERFUNCTION with a ws-aware variant and wire |
197 | | /// WRITEDATA to this server instance so the callback can consult per- |
198 | | /// scenario state (easy handle, one-shot probe flag). |
199 | 2.21k | void WebSocketMockServer::Install(CURL* easy) { |
200 | 2.21k | MockServerBase::Install(easy); |
201 | 2.21k | easy_handle_ = easy; |
202 | 2.21k | ws_probe_fired_ = false; |
203 | 2.21k | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &WebSocketWriteCallback); |
204 | 2.21k | curl_easy_setopt(easy, CURLOPT_WRITEDATA, this); |
205 | 2.21k | curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &WebSocketWriteCallback); |
206 | 2.21k | } |
207 | | |
208 | | /// @return true once the one-shot WS probe has fired for this scenario. |
209 | 4.99k | bool WebSocketMockServer::ws_probe_fired() const { return ws_probe_fired_; } |
210 | | |
211 | | /// Flip the one-shot gate closed. Called from the write callback before it |
212 | | /// invokes curl_ws_send, so subsequent callback entries skip the probe. |
213 | 348 | void WebSocketMockServer::MarkWsProbeFired() { ws_probe_fired_ = true; } |
214 | | |
215 | | /// @return the curl easy handle cached by Install() for the write callback. |
216 | 4.99k | CURL* WebSocketMockServer::easy_handle() const { return easy_handle_; } |
217 | | |
218 | | /// Queue RFC 6455 wire-byte chunks to emit once the handshake has completed. |
219 | | /// Resets the next-chunk cursor. |
220 | | /// @param frames Ordered list of chunk byte strings. |
221 | 2.21k | void WebSocketMockServer::SetFrames(std::vector<std::string> frames) { |
222 | 2.21k | frames_ = std::move(frames); |
223 | 2.21k | next_chunk_ = 0; |
224 | 2.21k | } |
225 | | |
226 | | /// Toggle streaming (false, default) vs manual (true) chunk delivery. |
227 | | /// @param manual Whether to suppress automatic chunk pushing by the |
228 | | /// drive loop. |
229 | 2.21k | void WebSocketMockServer::SetManualDelivery(bool manual) { manual_delivery_ = manual; } |
230 | | |
231 | | /// @return true if chunks are caller-driven rather than drive-loop-driven. |
232 | 19.9k | bool WebSocketMockServer::manual_delivery() const { return manual_delivery_; } |
233 | | |
234 | | /// @return true once a 101 Switching Protocols response has been written. |
235 | 52.2k | bool WebSocketMockServer::handshake_sent() const { return handshake_sent_; } |
236 | | |
237 | | /// @return true if at least one frame chunk has not yet been sent. |
238 | 13.0k | bool WebSocketMockServer::has_more_chunks() const { return next_chunk_ < frames_.size(); } |
239 | | |
240 | | /// @return the number of queued chunks not yet consumed. |
241 | 2.53k | std::size_t WebSocketMockServer::remaining_chunks() const { |
242 | 2.53k | return next_chunk_ >= frames_.size() ? 0 : frames_.size() - next_chunk_; |
243 | 2.53k | } |
244 | | |
245 | | /// Access a pending chunk without consuming it. |
246 | | /// @param index Offset from the next-pending cursor. |
247 | | /// @return reference to the chunk byte string. |
248 | 2.08k | const std::string& WebSocketMockServer::PeekChunk(std::size_t index) const { return frames_[next_chunk_ + index]; } |
249 | | |
250 | | /// Advance the pending-chunk cursor by one. No-op when no chunks remain. |
251 | 2.08k | void WebSocketMockServer::ConsumeChunk() { |
252 | 2.08k | if (next_chunk_ < frames_.size()) { |
253 | 2.08k | ++next_chunk_; |
254 | 2.08k | } |
255 | 2.08k | } |
256 | | |
257 | | /// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the |
258 | | /// MockConnection but does NOT write anything — the handshake is driven later |
259 | | /// by TryAdvanceHandshake(). |
260 | | /// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on |
261 | | /// failure. |
262 | 1.93k | curl_socket_t WebSocketMockServer::HandleOpenSocket() { |
263 | 1.93k | if (connection_) { |
264 | 0 | return CURL_SOCKET_BAD; |
265 | 0 | } |
266 | 1.93k | connection_ = std::make_unique<MockConnection>(); |
267 | 1.93k | if (!connection_->ok()) { |
268 | 0 | connection_.reset(); |
269 | 0 | return CURL_SOCKET_BAD; |
270 | 0 | } |
271 | 1.93k | ApplyPendingBackpressure(); |
272 | | // Wait for curl's Upgrade request before we write anything — the drive |
273 | | // loop calls TryAdvanceHandshake() to drive that exchange. |
274 | 1.93k | return connection_->take_client_fd(); |
275 | 1.93k | } |
276 | | |
277 | | /// Push raw bytes onto the server fd. Used by the manual-drive path to feed |
278 | | /// frame bytes directly into curl without any mock-side framing. |
279 | | /// @param data Buffer to send. |
280 | | /// @param size Number of bytes in 'data'. |
281 | | /// @return false on short or failed write. |
282 | 1.97k | bool WebSocketMockServer::PushRawBytes(const unsigned char* data, std::size_t size) { |
283 | 1.97k | if (!connection_) { |
284 | 0 | return false; |
285 | 0 | } |
286 | 1.97k | connection_->DrainIncoming(); |
287 | 1.97k | return connection_->WriteAll(data, size); |
288 | 1.97k | } |
289 | | |
290 | | /// Push the next queued frame when curl is ready. Used in streaming mode; |
291 | | /// the drive loop calls this after the handshake has been sent. Shuts |
292 | | /// the write side once the last chunk is delivered. |
293 | | /// @return true when a chunk was consumed from the script. |
294 | 1.97k | bool WebSocketMockServer::DeliverNextChunk() { |
295 | 1.97k | if (!connection_ || next_chunk_ >= frames_.size()) { |
296 | 0 | return false; |
297 | 0 | } |
298 | 1.97k | connection_->DrainIncoming(); |
299 | 1.97k | const std::string& chunk = frames_[next_chunk_++]; |
300 | 1.97k | if (!chunk.empty()) { |
301 | 1.72k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
302 | 1.72k | } |
303 | 1.97k | if (next_chunk_ >= frames_.size()) { |
304 | 518 | connection_->ShutdownWrite(); |
305 | 518 | } |
306 | 1.97k | return true; |
307 | 1.97k | } |
308 | | |
309 | | /// Drive the WebSocket opening handshake: read whatever curl has written so |
310 | | /// far, and once we've seen the end of the request headers, reply with a |
311 | | /// valid 101 Switching Protocols. |
312 | | /// @return true once the 101 response has been written (idempotent afterwards). |
313 | 5.81k | bool WebSocketMockServer::TryAdvanceHandshake() { |
314 | 5.81k | if (handshake_sent_ || !connection_) { |
315 | 284 | return handshake_sent_; |
316 | 284 | } |
317 | 5.53k | connection_->ReadAvailable(&ws_request_buffer_); |
318 | 5.53k | if (ws_request_buffer_.find("\r\n\r\n") == std::string::npos) { |
319 | 3.97k | return false; |
320 | 3.97k | } |
321 | 1.55k | std::string key = ExtractWebSocketKey(ws_request_buffer_); |
322 | | // Even if parsing failed, reply with *something* so curl doesn't wedge. |
323 | | // A bad Accept exercises curl's handshake-error path. |
324 | 1.55k | std::string accept = key.empty() ? std::string("AAAAAAAAAAAAAAAAAAAAAAAAAAA=") : ComputeWebSocketAccept(key); |
325 | 1.55k | std::string response = |
326 | 1.55k | "HTTP/1.1 101 Switching Protocols\r\n" |
327 | 1.55k | "Upgrade: websocket\r\n" |
328 | 1.55k | "Connection: Upgrade\r\n" |
329 | 1.55k | "Sec-WebSocket-Accept: " + |
330 | 1.55k | accept + "\r\n\r\n"; |
331 | 1.55k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(response.data()), response.size()); |
332 | 1.55k | handshake_sent_ = true; |
333 | 1.55k | return true; |
334 | 5.53k | } |
335 | | |
336 | | /// Seed the mock from the scenario, then run the perform loop. Drives the 101 |
337 | | /// handshake on every iteration; in streaming mode also pushes queued frame |
338 | | /// chunks as curl becomes readable. In manual mode (CURLOPT_CONNECT_ONLY=2) |
339 | | /// chunks are left alone during the loop — once the loop returns, this method |
340 | | /// pushes them as raw bytes and exercises curl_ws_recv / curl_ws_send against |
341 | | /// a small flag matrix. |
342 | | /// @param multi caller-owned multi; 'easy' is already added. |
343 | | /// @param easy the curl easy handle attached to this mock. |
344 | | /// @param scenario source of the frame chunks and CONNECT_ONLY setting. |
345 | 2.21k | void WebSocketMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
346 | 2.21k | SetManualDelivery(ScenarioRequestsManualWsDrive(scenario)); |
347 | | // initial_response is unused in WS mode; we synthesise the 101 dynamically |
348 | | // from curl's Upgrade request. |
349 | 2.21k | SetFrames(BuildFrameChunks(scenario.connection())); |
350 | | |
351 | 2.21k | int still_running = 1; |
352 | 2.21k | int idle_iterations = 0; |
353 | 2.21k | int drive_iterations = 0; |
354 | 2.21k | bool pollset_probed = false; |
355 | 2.21k | const bool timed_drive = UsesTimedDrive(scenario); |
356 | 2.21k | const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations; |
357 | 2.21k | CURLMcode rc = CURLM_OK; |
358 | | |
359 | 19.9k | while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) { |
360 | 19.0k | bool made_progress = false; |
361 | 19.0k | const int running_before = still_running; |
362 | 19.0k | rc = curl_multi_perform(multi, &still_running); |
363 | 19.0k | if (rc != CURLM_OK) { |
364 | 0 | break; |
365 | 0 | } |
366 | 19.0k | made_progress = still_running != running_before; |
367 | 19.0k | if (timed_drive && still_running && !pollset_probed) { |
368 | | // At this point curl has created the Upgrade connection but the mock has |
369 | | // not necessarily replied, which gives curl_multi_poll a meaningful WS |
370 | | // filter state without adding a timeout to fixed fast-lane inputs. |
371 | 180 | ProbeMultiPollset(multi); |
372 | 180 | pollset_probed = true; |
373 | 180 | } |
374 | | // Drive the 101 handshake on every iteration; no-op once sent. |
375 | 19.0k | if (!handshake_sent()) { |
376 | 5.81k | if (TryAdvanceHandshake()) { |
377 | 1.55k | made_progress = true; |
378 | 1.55k | } |
379 | 5.81k | } |
380 | 19.0k | if (!still_running) { |
381 | 1.35k | break; |
382 | 1.35k | } |
383 | | |
384 | | // Only push frame chunks in streaming mode — in manual mode the caller |
385 | | // will push them below after the handshake. |
386 | 17.6k | if (!manual_delivery() && handshake_sent() && has_more_chunks()) { |
387 | 1.97k | made_progress = DeliverNextChunk() || made_progress; |
388 | 15.7k | } else if (handshake_sent() && connection() != nullptr) { |
389 | | // Uploading WS scenarios can have no server frames at all. Drain their |
390 | | // encoded client data here so they make deterministic progress and do |
391 | | // not have to wait for CURLOPT_TIMEOUT_MS just to exercise cr_ws_read. |
392 | 11.7k | made_progress = connection()->DrainIncoming() != 0 || made_progress; |
393 | 11.7k | } |
394 | | |
395 | 17.6k | if (made_progress) { |
396 | 3.73k | idle_iterations = 0; |
397 | 3.73k | continue; |
398 | 3.73k | } |
399 | | |
400 | 13.9k | if (timed_drive) { |
401 | 7.09k | (void)WaitOnMultiFdset(multi, &rc); |
402 | 7.09k | if (rc != CURLM_OK) { |
403 | 0 | break; |
404 | 0 | } |
405 | 7.09k | } |
406 | 13.9k | ++idle_iterations; |
407 | 13.9k | } |
408 | | |
409 | 2.21k | if (!manual_delivery() || !handshake_sent()) { |
410 | 1.76k | return; |
411 | 1.76k | } |
412 | | |
413 | | // Manual-drive tail: feed every remaining scripted chunk straight onto the |
414 | | // server fd as raw frame bytes, draining curl_ws_recv between each push. |
415 | 2.53k | while (remaining_chunks() > 0) { |
416 | 2.08k | const std::string& chunk = PeekChunk(0); |
417 | 2.08k | if (!chunk.empty()) { |
418 | 1.97k | PushRawBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
419 | 1.97k | } |
420 | 2.08k | ConsumeChunk(); |
421 | 2.08k | DrainWsRecv(easy); |
422 | 2.08k | } |
423 | | // Final drain in case frame parsing produced more work after the last push. |
424 | 450 | DrainWsRecv(easy); |
425 | | |
426 | 450 | const auto& probes = scenario.connection().manual_probes(); |
427 | 450 | static const unsigned char kPayload[] = "hello-from-proto-fuzzer"; |
428 | 450 | const std::size_t payload_len = sizeof(kPayload) - 1; |
429 | | |
430 | 450 | if (probes.flag_matrix()) { |
431 | | // Exercise curl_ws_send with a fixed matrix of flags. We don't care whether |
432 | | // the send actually lands on the wire — the point is to reach the encode |
433 | | // paths in ws_enc_add_frame / ws_enc_write_head. |
434 | 86 | std::size_t iteration = 0; |
435 | 1.63k | for (unsigned int flags : kWsSendFlagMatrix) { |
436 | 1.63k | if (iteration++ >= kMaxWsSendIterations) { |
437 | 0 | break; |
438 | 0 | } |
439 | | // Announce the frame via the public curl_ws_start_frame entrypoint before |
440 | | // the actual send. In non-raw mode this writes the frame head into the |
441 | | // sendbuf; the follow-up curl_ws_send with the same flags finishes the |
442 | | // exchange or fails cleanly on invalid flag combos. |
443 | 1.63k | (void)curl_ws_start_frame(easy, flags, static_cast<curl_off_t>(payload_len)); |
444 | 1.63k | std::size_t sent = 0; |
445 | 1.63k | curl_off_t fragsize = (flags & CURLWS_OFFSET) ? static_cast<curl_off_t>(payload_len) : 0; |
446 | 1.63k | (void)curl_ws_send(easy, kPayload, payload_len, &sent, fragsize, flags); |
447 | 1.63k | if (connection() != nullptr) { |
448 | 1.63k | connection()->DrainIncoming(); |
449 | 1.63k | } |
450 | 1.63k | } |
451 | 86 | } |
452 | | |
453 | 450 | if (probes.unaligned_send()) { |
454 | | // Multi-call mis-sized send probe: declare a fragsize=200 frame, send 23 |
455 | | // bytes, then call curl_ws_send again with a buflen much bigger than the |
456 | | // remaining payload. Hits the "unaligned frame size" failf in ws_enc_send. |
457 | 55 | constexpr curl_off_t kBigFrag = 200; |
458 | 55 | (void)curl_ws_start_frame(easy, CURLWS_TEXT | CURLWS_OFFSET, kBigFrag); |
459 | 55 | std::size_t sent = 0; |
460 | 55 | (void)curl_ws_send(easy, kPayload, payload_len, &sent, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET); |
461 | | // Now enc.payload_remain ≈ kBigFrag - payload_len. A follow-up send with |
462 | | // buflen > remaining trips the guard at ws_enc_send:~1050. |
463 | 55 | std::size_t sent2 = 0; |
464 | 55 | constexpr std::size_t kOverrun = 500; |
465 | 55 | unsigned char overrun[kOverrun]; |
466 | 55 | std::fill(overrun, overrun + kOverrun, 'X'); |
467 | 55 | (void)curl_ws_send(easy, overrun, kOverrun, &sent2, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET); |
468 | 55 | if (connection() != nullptr) { |
469 | 55 | connection()->DrainIncoming(); |
470 | 55 | } |
471 | 55 | } |
472 | | |
473 | 450 | if (probes.raw_send()) { |
474 | | // Raw-mode send path: reachable only when CURLOPT_WS_OPTIONS has |
475 | | // CURLWS_RAW_MODE set. curl_ws_send with flags=0, fragsize=0 takes the |
476 | | // data->set.ws_raw_mode branch → ws_send_raw. No-op for non-raw scenarios |
477 | | // (hits the "no flags given" failure path instead). |
478 | 43 | std::size_t sent = 0; |
479 | 43 | (void)curl_ws_send(easy, kPayload, payload_len, &sent, 0, 0); |
480 | 43 | if (connection() != nullptr) { |
481 | 43 | connection()->DrainIncoming(); |
482 | 43 | } |
483 | 43 | } |
484 | 450 | } |
485 | | |
486 | | } // namespace proto_fuzzer |