12#include <curl/websockets.h>
25namespace proto_fuzzer {
31constexpr std::size_t kMaxWsRecvIterations = 128;
32constexpr std::size_t kMaxWsSendIterations = 32;
45constexpr unsigned int kWsSendFlagMatrix[] = {
49 CURLWS_TEXT | CURLWS_OFFSET,
50 CURLWS_BINARY | CURLWS_OFFSET,
51 CURLWS_TEXT | CURLWS_CONT,
55 CURLWS_BINARY | CURLWS_CONT,
61 CURLWS_CLOSE | CURLWS_CONT,
62 CURLWS_PING | CURLWS_CONT,
63 CURLWS_PONG | CURLWS_CONT,
64 CURLWS_TEXT | CURLWS_BINARY,
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) {
76 pos +=
sizeof(kHeader) - 1;
77 while (pos < request.size() && (request[pos] ==
' ' || request[pos] ==
'\t')) {
80 std::size_t end = request.find(
"\r\n", pos);
81 if (end == std::string::npos) {
84 while (end > pos && (request[end - 1] ==
' ' || request[end - 1] ==
'\t')) {
87 return request.substr(pos, end - pos);
99std::vector<std::string> BuildFrameChunks(
const curl::fuzzer::proto::Connection& conn) {
100 std::vector<std::string> chunks;
103 for (std::size_t i = 0; i < raw_budget; ++i) {
104 chunks.emplace_back(conn.on_readable(i));
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) {
124size_t WebSocketWriteCallback(
void* ,
size_t size,
size_t nmemb,
void* 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);
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) {
154 if (rr != CURLE_OK && rr != CURLE_GOT_NOTHING) {
157 if (nread == 0 && meta ==
nullptr) {
160 total_received += nread;
162 return total_received;
177 bool manual_delivery =
false;
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) {
186 manual_delivery = value == 2;
189 return manual_delivery;
196 manual_delivery_(false),
197 handshake_sent_(false),
198 ws_probe_fired_(false),
199 easy_handle_(nullptr),
200 manual_received_bytes_(0) {}
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);
242 frames_ = std::move(frames);
262 return next_chunk_ >= frames_.size() ? 0 : frames_.size() - next_chunk_;
272 if (next_chunk_ < frames_.size()) {
291 const std::size_t size_before = ws_request_buffer_.size();
293 return ws_request_buffer_.size() - size_before;
306 if (!handshake_sent_ && data !=
nullptr && size != 0) {
307 ws_request_buffer_.append(
reinterpret_cast<const char*
>(data), size);
324 return CURL_SOCKET_BAD;
329 return CURL_SOCKET_BAD;
348 const bool written =
connection_->WriteAll(data, size);
360 if (!
connection_ || next_chunk_ >= frames_.size()) {
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())) {
370 if (next_chunk_ >= frames_.size()) {
383 if (made_progress !=
nullptr) {
384 *made_progress =
false;
387 return handshake_sent_;
390 *made_progress =
true;
392 if (ws_request_buffer_.find(
"\r\n\r\n") == std::string::npos) {
395 std::string key = ExtractWebSocketKey(ws_request_buffer_);
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: " +
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;
434 manual_received_bytes_ = 0;
437 SetFrames(BuildFrameChunks(scenario.connection()));
439 int still_running = 1;
440 int idle_iterations = 0;
441 int drive_iterations = 0;
442 bool pollset_probed =
false;
445 CURLMcode rc = CURLM_OK;
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) {
454 made_progress = still_running != running_before;
455 if (timed_drive && still_running && !pollset_probed) {
460 pollset_probed =
true;
464 bool handshake_progress =
false;
466 made_progress = handshake_progress || made_progress;
468 if (!still_running) {
490 if (rc != CURLM_OK) {
505 if (!chunk.empty()) {
506 PushRawBytes(
reinterpret_cast<const unsigned char*
>(chunk.data()), chunk.size());
509 manual_received_bytes_ += DrainWsRecv(easy);
512 manual_received_bytes_ += DrainWsRecv(easy);
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;
518 if (probes.flag_matrix()) {
522 std::size_t iteration = 0;
523 for (
unsigned int flags : kWsSendFlagMatrix) {
524 if (iteration++ >= kMaxWsSendIterations) {
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);
541 if (probes.unaligned_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);
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);
561 if (probes.raw_send()) {
566 std::size_t sent = 0;
567 (void)curl_ws_send(easy, kPayload, payload_len, &sent, 0, 0);
576#if defined(PROTO_FUZZER_OBSERVE_STATE)
579 (void)websocket_observation;
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().
MockConnection * connection()
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_.
In-process WebSocket peer.
std::size_t remaining_chunks() const
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.
CURL * easy_handle() const
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.
bool manual_delivery() const
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.
bool handshake_sent() const
bool has_more_chunks() const
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.
std::size_t manual_received_bytes() const
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.
Serialize a proto WebSocketFrame message into RFC 6455 wire bytes.