curl-fuzzer C++ reference
Loading...
Searching...
No Matches
websocket_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 <curl/websockets.h>
13
14#include <algorithm>
15#include <cstddef>
16#include <cstdint>
17#include <string>
18#include <utility>
19
24
25namespace proto_fuzzer {
26
27namespace {
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.
31constexpr std::size_t kMaxWsRecvIterations = 128;
32constexpr std::size_t kMaxWsSendIterations = 32;
33
45constexpr 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
70std::string ExtractWebSocketKey(const std::string& request) {
71 static const char kHeader[] = "Sec-WebSocket-Key:";
72 std::size_t pos = request.find(kHeader);
73 if (pos == std::string::npos) {
74 return {};
75 }
76 pos += sizeof(kHeader) - 1;
77 while (pos < request.size() && (request[pos] == ' ' || request[pos] == '\t')) {
78 ++pos;
79 }
80 std::size_t end = request.find("\r\n", pos);
81 if (end == std::string::npos) {
82 return {};
83 }
84 while (end > pos && (request[end - 1] == ' ' || request[end - 1] == '\t')) {
85 --end;
86 }
87 return request.substr(pos, end - pos);
88}
89
93std::string ComputeWebSocketAccept(const std::string& key) { return proto_fuzzer::ComputeWebSocketAcceptKey(key); }
94
99std::vector<std::string> BuildFrameChunks(const curl::fuzzer::proto::Connection& conn) {
100 std::vector<std::string> chunks;
102 const std::size_t raw_budget = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, conn.on_readable_size());
103 for (std::size_t i = 0; i < raw_budget; ++i) {
104 chunks.emplace_back(conn.on_readable(i));
105 }
106 const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - chunks.size();
107 const std::size_t frame_count = std::min<std::size_t>(frame_budget, conn.server_frames_size());
108 for (std::size_t i = 0; i < frame_count; ++i) {
109 chunks.emplace_back(SerializeWebSocketFrame(conn.server_frames(static_cast<int>(i))));
110 }
111 return chunks;
112}
113
124size_t WebSocketWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* userdata) {
125 auto* server = static_cast<WebSocketMockServer*>(userdata);
126 if (server != nullptr) {
127 CURL* easy = server->easy_handle();
128 if (easy != nullptr) {
129 (void)curl_ws_meta(easy);
130 if (!server->ws_probe_fired()) {
131 server->MarkWsProbeFired();
132 static unsigned char kProbe[16384];
133 std::fill(kProbe, kProbe + sizeof(kProbe), 'P');
134 std::size_t sent = 0;
135 (void)curl_ws_send(easy, kProbe, sizeof(kProbe), &sent, 0, 0);
136 }
137 }
138 }
139 return size * nmemb;
140}
141
144std::size_t DrainWsRecv(CURL* easy) {
145 std::size_t total_received = 0;
146 for (std::size_t i = 0; i < kMaxWsRecvIterations; ++i) {
147 unsigned char buffer[4096];
148 std::size_t nread = 0;
149 const struct curl_ws_frame* meta = nullptr;
150 CURLcode rr = curl_ws_recv(easy, buffer, sizeof(buffer), &nread, &meta);
151 if (rr == CURLE_AGAIN) {
152 break;
153 }
154 if (rr != CURLE_OK && rr != CURLE_GOT_NOTHING) {
155 break;
156 }
157 if (nread == 0 && meta == nullptr) {
158 break;
159 }
160 total_received += nread;
161 }
162 return total_received;
163}
164
165} // namespace
166
176bool ScenarioRequestsManualWsDrive(const curl::fuzzer::proto::Scenario& scenario) {
177 bool manual_delivery = false;
178 const std::size_t option_count = RuntimeOptionCount(scenario);
179 for (std::size_t index = 0; index < option_count; ++index) {
180 const auto& option = scenario.options(static_cast<int>(index));
181 if (option.option_id() != curl::fuzzer::proto::CURLOPT_CONNECT_ONLY) {
182 continue;
183 }
184 const std::uint64_t value = DecodeIntegralOptionValue(option);
185 if (value <= 2) {
186 manual_delivery = value == 2;
187 }
188 }
189 return manual_delivery;
190}
191
195 : next_chunk_(0),
196 manual_delivery_(false),
197 handshake_sent_(false),
198 ws_probe_fired_(false),
199 easy_handle_(nullptr),
200 manual_received_bytes_(0) {}
201
204
213 easy_handle_ = easy;
214 ws_probe_fired_ = false;
215 curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &WebSocketWriteCallback);
216 curl_easy_setopt(easy, CURLOPT_WRITEDATA, this);
217 curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &WebSocketWriteCallback);
218}
219
225bool WebSocketMockServer::ws_probe_fired() const { return ws_probe_fired_; }
226
229void WebSocketMockServer::MarkWsProbeFired() { ws_probe_fired_ = true; }
230
232CURL* WebSocketMockServer::easy_handle() const { return easy_handle_; }
233
236std::size_t WebSocketMockServer::manual_received_bytes() const { return manual_received_bytes_; }
237
241void WebSocketMockServer::SetFrames(std::vector<std::string> frames) {
242 frames_ = std::move(frames);
243 next_chunk_ = 0;
244}
245
249void WebSocketMockServer::SetManualDelivery(bool manual) { manual_delivery_ = manual; }
250
252bool WebSocketMockServer::manual_delivery() const { return manual_delivery_; }
253
255bool WebSocketMockServer::handshake_sent() const { return handshake_sent_; }
256
258bool WebSocketMockServer::has_more_chunks() const { return next_chunk_ < frames_.size(); }
259
262 return next_chunk_ >= frames_.size() ? 0 : frames_.size() - next_chunk_;
263}
264
268const std::string& WebSocketMockServer::PeekChunk(std::size_t index) const { return frames_[next_chunk_ + index]; }
269
272 if (next_chunk_ < frames_.size()) {
273 ++next_chunk_;
274 }
275}
276
281std::unique_ptr<MockConnection> WebSocketMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
282
288 if (!connection_) {
289 return 0;
290 }
291 const std::size_t size_before = ws_request_buffer_.size();
292 connection_->ReadAvailable(&ws_request_buffer_);
293 return ws_request_buffer_.size() - size_before;
294}
295
299
305void WebSocketMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) {
306 if (!handshake_sent_ && data != nullptr && size != 0) {
307 ws_request_buffer_.append(reinterpret_cast<const char*>(data), size);
308 }
309}
310
320curl_socket_t WebSocketMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
321 (void)purpose;
322 (void)address;
323 if (connection_) {
324 return CURL_SOCKET_BAD;
325 }
327 if (!connection_ || !connection_->ok()) {
328 connection_.reset();
329 return CURL_SOCKET_BAD;
330 }
331 connection_->SetIncomingDataObserver(this);
333 // Wait for curl's Upgrade request before we write anything — the drive
334 // loop calls TryAdvanceHandshake() to drive that exchange.
335 return connection_->take_client_fd();
336}
337
343bool WebSocketMockServer::PushRawBytes(const unsigned char* data, std::size_t size) {
344 if (!connection_) {
345 return false;
346 }
347 connection_->DrainIncoming();
348 const bool written = connection_->WriteAll(data, size);
349 if (written) {
351 }
352 return written;
353}
354
360 if (!connection_ || next_chunk_ >= frames_.size()) {
361 return false;
362 }
363 connection_->DrainIncoming();
364 const std::string& chunk = frames_[next_chunk_++];
365 if (!chunk.empty()) {
366 if (connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size())) {
368 }
369 }
370 if (next_chunk_ >= frames_.size()) {
371 connection_->ShutdownWrite();
372 }
373 return true;
374}
375
383 if (made_progress != nullptr) {
384 *made_progress = false;
385 }
386 if (handshake_sent_ || !connection_) {
387 return handshake_sent_;
388 }
389 if (DrainHandshakeData() != 0 && made_progress != nullptr) {
390 *made_progress = true;
391 }
392 if (ws_request_buffer_.find("\r\n\r\n") == std::string::npos) {
393 return false;
394 }
395 std::string key = ExtractWebSocketKey(ws_request_buffer_);
396 // Even if parsing failed, reply with *something* so curl doesn't wedge.
397 // A bad Accept exercises curl's handshake-error path.
398 std::string accept = key.empty() ? std::string("AAAAAAAAAAAAAAAAAAAAAAAAAAA=") : ComputeWebSocketAccept(key);
399 std::string response =
400 "HTTP/1.1 101 Switching Protocols\r\n"
401 "Upgrade: websocket\r\n"
402 "Connection: Upgrade\r\n"
403 "Sec-WebSocket-Accept: " +
404 accept + "\r\n\r\n";
405 // CONNECT_ONLY may leave already-read bytes after the 101 headers. Put one
406 // bounded frame in the same peer write so curl has to preserve those bytes
407 // for the later curl_ws_recv call instead of discarding them when the
408 // opening transfer finishes. Remaining chunks still exercise the explicit
409 // manual-delivery tail below.
410 if (manual_delivery() && has_more_chunks()) {
411 response.append(PeekChunk(0));
412 ConsumeChunk();
413 }
414 connection_->WriteAll(reinterpret_cast<const unsigned char*>(response.data()), response.size());
416 handshake_sent_ = true;
417 if (made_progress != nullptr) {
418 *made_progress = true;
419 }
420 return true;
421}
422
432void WebSocketMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
434 manual_received_bytes_ = 0;
435 // initial_response is unused in WS mode; we synthesise the 101 dynamically
436 // from curl's Upgrade request.
437 SetFrames(BuildFrameChunks(scenario.connection()));
438
439 int still_running = 1;
440 int idle_iterations = 0;
441 int drive_iterations = 0;
442 bool pollset_probed = false;
443 const bool timed_drive = UsesTimedDrive(scenario);
444 const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations;
445 CURLMcode rc = CURLM_OK;
446
447 while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) {
448 bool made_progress = false;
449 const int running_before = still_running;
450 rc = curl_multi_perform(multi, &still_running);
451 if (rc != CURLM_OK) {
452 break;
453 }
454 made_progress = still_running != running_before;
455 if (timed_drive && still_running && !pollset_probed) {
456 // At this point curl has created the Upgrade connection but the mock has
457 // not necessarily replied, which gives curl_multi_poll a meaningful WS
458 // filter state without adding a timeout to fixed fast-lane inputs.
459 ProbeMultiPollset(multi);
460 pollset_probed = true;
461 }
462 // Drive the 101 handshake on every iteration; no-op once sent.
463 if (!handshake_sent()) {
464 bool handshake_progress = false;
465 (void)TryAdvanceHandshake(&handshake_progress);
466 made_progress = handshake_progress || made_progress;
467 }
468 if (!still_running) {
469 break;
470 }
471
472 // Only push frame chunks in streaming mode — in manual mode the caller
473 // will push them below after the handshake.
475 made_progress = DeliverNextChunk() || made_progress;
476 } else if (handshake_sent() && connection() != nullptr) {
477 // Uploading WS scenarios can have no server frames at all. Drain their
478 // encoded client data here so they make deterministic progress and do
479 // not have to wait for CURLOPT_TIMEOUT_MS just to exercise cr_ws_read.
480 made_progress = connection()->DrainIncoming() != 0 || made_progress;
481 }
482
483 if (made_progress) {
484 idle_iterations = 0;
485 continue;
486 }
487
488 if (timed_drive) {
489 (void)WaitOnMultiFdset(multi, &rc);
490 if (rc != CURLM_OK) {
491 break;
492 }
493 }
494 ++idle_iterations;
495 }
496
497 if (!manual_delivery() || !handshake_sent()) {
498 return;
499 }
500
501 // Manual-drive tail: feed every remaining scripted chunk straight onto the
502 // server fd as raw frame bytes, draining curl_ws_recv between each push.
503 while (remaining_chunks() > 0) {
504 const std::string& chunk = PeekChunk(0);
505 if (!chunk.empty()) {
506 PushRawBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
507 }
508 ConsumeChunk();
509 manual_received_bytes_ += DrainWsRecv(easy);
510 }
511 // Final drain in case frame parsing produced more work after the last push.
512 manual_received_bytes_ += DrainWsRecv(easy);
513
514 const auto& probes = scenario.connection().manual_probes();
515 static const unsigned char kPayload[] = "hello-from-proto-fuzzer";
516 const std::size_t payload_len = sizeof(kPayload) - 1;
517
518 if (probes.flag_matrix()) {
519 // Exercise curl_ws_send with a fixed matrix of flags. We don't care whether
520 // the send actually lands on the wire — the point is to reach the encode
521 // paths in ws_enc_add_frame / ws_enc_write_head.
522 std::size_t iteration = 0;
523 for (unsigned int flags : kWsSendFlagMatrix) {
524 if (iteration++ >= kMaxWsSendIterations) {
525 break;
526 }
527 // Announce the frame via the public curl_ws_start_frame entrypoint before
528 // the actual send. In non-raw mode this writes the frame head into the
529 // sendbuf; the follow-up curl_ws_send with the same flags finishes the
530 // exchange or fails cleanly on invalid flag combos.
531 (void)curl_ws_start_frame(easy, flags, static_cast<curl_off_t>(payload_len));
532 std::size_t sent = 0;
533 curl_off_t fragsize = (flags & CURLWS_OFFSET) ? static_cast<curl_off_t>(payload_len) : 0;
534 (void)curl_ws_send(easy, kPayload, payload_len, &sent, fragsize, flags);
535 if (connection() != nullptr) {
537 }
538 }
539 }
540
541 if (probes.unaligned_send()) {
542 // Multi-call mis-sized send probe: declare a fragsize=200 frame, send 23
543 // bytes, then call curl_ws_send again with a buflen much bigger than the
544 // remaining payload. Hits the "unaligned frame size" failf in ws_enc_send.
545 constexpr curl_off_t kBigFrag = 200;
546 (void)curl_ws_start_frame(easy, CURLWS_TEXT | CURLWS_OFFSET, kBigFrag);
547 std::size_t sent = 0;
548 (void)curl_ws_send(easy, kPayload, payload_len, &sent, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET);
549 // Now enc.payload_remain ≈ kBigFrag - payload_len. A follow-up send with
550 // buflen > remaining trips the guard at ws_enc_send:~1050.
551 std::size_t sent2 = 0;
552 constexpr std::size_t kOverrun = 500;
553 unsigned char overrun[kOverrun];
554 std::fill(overrun, overrun + kOverrun, 'X');
555 (void)curl_ws_send(easy, overrun, kOverrun, &sent2, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET);
556 if (connection() != nullptr) {
558 }
559 }
560
561 if (probes.raw_send()) {
562 // Raw-mode send path: reachable only when CURLOPT_WS_OPTIONS has
563 // CURLWS_RAW_MODE set. curl_ws_send with flags=0, fragsize=0 takes the
564 // data->set.ws_raw_mode branch → ws_send_raw. No-op for non-raw scenarios
565 // (hits the "no flags given" failure path instead).
566 std::size_t sent = 0;
567 (void)curl_ws_send(easy, kPayload, payload_len, &sent, 0, 0);
568 if (connection() != nullptr) {
570 }
571 }
572}
573
576#if defined(PROTO_FUZZER_OBSERVE_STATE)
578 const volatile std::size_t websocket_observation = manual_received_bytes();
579 (void)websocket_observation;
580#endif
581}
582
583} // namespace proto_fuzzer
virtual std::size_t DrainIncoming()
Advance incoming transport work according to the configured per-call limit.
static bool UsesTimedDrive(const curl::fuzzer::proto::Scenario &scenario)
Treat a non-default BackpressureConfig as an explicit request for the slower, timed drive policy.
virtual void Install(CURL *easy)
Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION callbacks on 'easy'.
std::unique_ptr< MockConnection > connection_
The per-scenario MockConnection, lazily created by HandleOpenSocket().
static int WaitOnMultiFdset(CURLM *multi, CURLMcode *rc)
Wait on curl's fdset with a short timeout.
static constexpr int kMaxTimedIdleIterations
Explicit backpressure scenarios get a larger idle budget and may use the short timed wait above.
static void ProbeMultiPollset(CURLM *multi)
Ask curl to construct and inspect its connection-filter pollset without waiting.
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
void ApplyPendingBackpressure()
Apply the BackpressureConfig cached by DriveScenario to the new connection_.
bool TryAdvanceHandshake(bool *made_progress=nullptr)
Drive the WebSocket opening handshake: read whatever curl has written so far, and once we've seen the...
bool PushRawBytes(const unsigned char *data, std::size_t size)
Push raw bytes onto the server fd.
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Collect application bytes consumed by the active transport.
bool DeliverNextChunk()
Push the next queued frame when curl is ready.
WebSocketMockServer()
Construct an idle WebSocketMockServer with no queued frames.
void SetFrames(std::vector< std::string > frames)
Queue RFC 6455 wire-byte chunks to emit once the handshake has completed.
virtual void FlushTransport()
Flush application writes when the carrier queues them.
const std::string & PeekChunk(std::size_t index) const
Access a pending chunk without consuming it.
void ConsumeChunk()
Advance the pending-chunk cursor by one. No-op when no chunks remain.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Seed the mock from the scenario, then run the perform loop.
void SetManualDelivery(bool manual)
Toggle streaming (false, default) vs manual (true) chunk delivery.
void Install(CURL *easy) override
Install the shared socket trampolines via the base, then layer a WebSocket-aware WRITEFUNCTION / HEAD...
~WebSocketMockServer() override
Default destructor; the owned MockConnection (if any) cleans up its socketpair.
bool ws_probe_fired() const
Return the one-shot probe gate.
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Called by the OPENSOCKETFUNCTION trampoline in the base class.
virtual std::size_t DrainHandshakeData()
Consume the plaintext Upgrade request without applying DrainIncoming's backpressure limit.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
virtual std::unique_ptr< MockConnection > CreateConnection()
Construct the transport used for the WebSocket exchange.
void MarkWsProbeFired()
Flip the one-shot gate closed.
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario &scenario)
Return the runtime-visible option prefix length.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
std::uint64_t DecodeIntegralOptionValue(const OptionDescriptor &descriptor, const curl::fuzzer::proto::SetOption &option)
Decode using an already-resolved descriptor, avoiding another lookup when the runtime has selected th...
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxResponseChunks
Enough response boundaries to exercise incremental parsers while keeping per-perform delivery work fi...
bool ScenarioRequestsManualWsDrive(const curl::fuzzer::proto::Scenario &scenario)
Mirror the sequential CURLOPT_CONNECT_ONLY setopts applied by ApplyScenarioOptions.
WebSocketMockServer — an in-process WebSocket peer that drives a dynamic 101 handshake and then feeds...
Minimal standalone SHA-1 + base64 for the WebSocket Sec-WebSocket-Accept computation (RFC 6455 §4....
std::string ComputeWebSocketAcceptKey(const std::string &key)
Compute the RFC 6455 Sec-WebSocket-Accept value for a given client key.
std::string SerializeWebSocketFrame(const curl::fuzzer::proto::WebSocketFrame &frame)
Serialise a proto WebSocketFrame into RFC 6455 wire bytes.
Definition ws_frame.cc:59
Serialize a proto WebSocketFrame message into RFC 6455 wire bytes.