/src/curl_fuzzer/proto_fuzzer/mock_server.h
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 MockConnection and MockServer — the in-process peer that feeds |
9 | | /// canned response bytes to libcurl over a socketpair. |
10 | | |
11 | | #ifndef PROTO_FUZZER_MOCK_SERVER_H_ |
12 | | #define PROTO_FUZZER_MOCK_SERVER_H_ |
13 | | |
14 | | #include <curl/curl.h> |
15 | | |
16 | | #include <array> |
17 | | #include <cstddef> |
18 | | #include <memory> |
19 | | #include <string> |
20 | | #include <vector> |
21 | | |
22 | | #include "curl_fuzzer.pb.h" |
23 | | #include "proto_fuzzer/mock_server_base.h" |
24 | | #include "proto_fuzzer/scenario_limits.h" |
25 | | |
26 | | namespace proto_fuzzer { |
27 | | |
28 | | class MockConnection { |
29 | | public: |
30 | | MockConnection(); |
31 | | virtual ~MockConnection(); |
32 | | |
33 | | MockConnection(const MockConnection&) = delete; |
34 | | MockConnection& operator=(const MockConnection&) = delete; |
35 | | |
36 | | virtual bool ok() const; |
37 | | curl_socket_t take_client_fd(); |
38 | | int server_fd() const; |
39 | | |
40 | | /// Return the capacity curl's endpoint reports for queued client writes. |
41 | | /// Protocol mocks use this before transferring fd ownership when their |
42 | | /// synchronous send path cannot rely on the outer driver to make room. |
43 | | /// @return SO_SNDBUF in bytes, or zero when it cannot be queried. |
44 | | std::size_t client_send_buffer_size() const; |
45 | | |
46 | | /// Ensure curl's endpoint reports at least `minimum` bytes of send buffer, |
47 | | /// requesting a larger SO_SNDBUF when the platform default is smaller. |
48 | | /// @param minimum Smallest acceptable reported capacity in bytes. |
49 | | /// @return true when the queried postcondition holds. |
50 | | bool EnsureClientSendBufferSize(std::size_t minimum); |
51 | | |
52 | | virtual bool WriteAll(const unsigned char* data, std::size_t size); |
53 | | /// Advance incoming transport work according to the configured per-call |
54 | | /// limit. Plaintext connections report bytes consumed; layered transports |
55 | | /// may also report handshake state changes so the bounded outer loop does |
56 | | /// not mistake useful protocol progress for an idle connection. |
57 | | /// @return amount of transport progress made during this call. |
58 | | virtual std::size_t DrainIncoming(); |
59 | | void ReadAvailable(std::string* out); |
60 | | virtual void ShutdownWrite(); |
61 | | |
62 | | /// Apply deterministic backpressure knobs. Set SO_RCVBUF on the server |
63 | | /// side (if recv_buf_bytes > 0) to cap how much curl can write before it |
64 | | /// short-writes / EAGAINs, and cap DrainIncoming()'s per-call byte budget |
65 | | /// (0 = unlimited). Must be called before any traffic for the recv_buf |
66 | | /// setting to matter. |
67 | | /// @param recv_buf_bytes SO_RCVBUF size in bytes, or 0 to leave default. |
68 | | /// @param drain_limit Max bytes drained per DrainIncoming call, 0 for unlimited. |
69 | | void ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit); |
70 | | |
71 | | private: |
72 | | int server_fd_; |
73 | | int client_fd_; |
74 | | std::size_t drain_limit_; |
75 | | }; |
76 | | |
77 | | /// @class proto_fuzzer::MockServer |
78 | | /// @brief HTTP in-process mock peer. Assigns one bounded response script to |
79 | | /// each socket curl opens, allowing redirects and authentication |
80 | | /// retries to progress without permitting an unbounded connection |
81 | | /// graph. WebSocketMockServer keeps its separate single-socket model. |
82 | | class MockServer : public MockServerBase { |
83 | | public: |
84 | | MockServer(); |
85 | | ~MockServer() override; |
86 | | |
87 | | /// Borrow the primary and bounded follow-on HTTP scripts from `scenario`. |
88 | | /// The caller must keep the scenario alive and unmodified until the current |
89 | | /// synchronous drive has finished. ScenarioRunner already provides exactly |
90 | | /// that lifetime, so retaining pointers avoids copying response bytes before |
91 | | /// curl has even requested the corresponding socket or chunk. |
92 | | void SetScripts(const curl::fuzzer::proto::Scenario& scenario); |
93 | | |
94 | | /// Preload the bounded HTTP response, half-close the peer, and invoke |
95 | | /// curl_easy_perform. This avoids a helper thread while guaranteeing that |
96 | | /// curl never waits for the outer chunk-delivery loop. |
97 | | void DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override; |
98 | | |
99 | | /// Deliver one queued response chunk. |
100 | | /// @return true when a chunk was consumed from the script. |
101 | | bool DeliverNextChunk(); |
102 | | bool has_more_chunks() const; |
103 | | |
104 | | protected: |
105 | | /// Construct the transport used for one HTTP exchange. HTTPS overrides this |
106 | | /// factory with a TLS record layer while retaining the same bounded response |
107 | | /// scripting and redirect lifetimes as plaintext HTTP. |
108 | | /// @return a new connection, whose ok() result is checked before use. |
109 | | virtual std::unique_ptr<MockConnection> CreateConnection(); |
110 | | |
111 | | /// Release every current and retired connection before resetting transport- |
112 | | /// specific state. Derived servers call this from their destructors when |
113 | | /// connection objects borrow state owned by the derived class. |
114 | | void ResetConnections(); |
115 | | |
116 | | /// Observe curl while its connection filters are still attached. The |
117 | | /// default HTTP peer has no transport-specific state to inspect; layered |
118 | | /// transports override this instead of querying stale state after the |
119 | | /// multi handle has been dismantled. |
120 | | virtual void ObserveActiveTransfer(CURL* easy); |
121 | | |
122 | | curl_socket_t HandleOpenSocket(curlsocktype purpose = CURLSOCKTYPE_IPCXN, |
123 | | struct curl_sockaddr* address = nullptr) override; |
124 | | void RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override; |
125 | | |
126 | | private: |
127 | | /// One socket's borrowed response configuration plus its delivery cursor. |
128 | | /// The fixed script array is fully populated before curl runs, so both this |
129 | | /// object and its pointer into the caller-owned Scenario remain stable across |
130 | | /// callbacks. Counts record the exact runtime-visible prefix: raw chunks come |
131 | | /// first and structured frames consume only the remaining shared budget. |
132 | | struct ConnectionScript { |
133 | | const curl::fuzzer::proto::Connection* connection = nullptr; |
134 | | std::size_t raw_chunk_count = 0; |
135 | | std::size_t frame_chunk_count = 0; |
136 | | std::size_t next_chunk = 0; |
137 | | |
138 | | /// @return Number of raw and structured chunks visible to the runtime. |
139 | 237k | std::size_t chunk_count() const { return raw_chunk_count + frame_chunk_count; } |
140 | | }; |
141 | | |
142 | | /// Drain client request bytes from both the current socket and sockets curl |
143 | | /// has moved past. Keeping old peers responsive prevents a late write/close |
144 | | /// on a redirect source connection from stalling the new exchange. |
145 | | /// @return total bytes drained during this call. |
146 | | std::size_t DrainIncomingConnections(); |
147 | | |
148 | | /// Service one deterministic application event-loop turn by draining all |
149 | | /// request bytes currently available and releasing at most one response |
150 | | /// chunk. Both multi APIs use this ordering so selecting socket_action |
151 | | /// changes only how readiness reaches curl, not the mock protocol script. |
152 | | /// @return true when any request or response byte advanced. |
153 | | bool ServiceConnections(); |
154 | | |
155 | | /// Drive the HTTP exchange through curl_multi_socket_action using the |
156 | | /// callback state owned by MockServerBase::DriveScenario. |
157 | | /// @param multi Multi handle containing `easy`. |
158 | | /// @return after curl finishes or a deterministic idle/operation cap wins. |
159 | | void RunSocketActionLoop(CURLM* multi, CURL* easy); |
160 | | |
161 | | std::array<ConnectionScript, scenario_limits::kMaxConnections> scripts_; |
162 | | std::size_t script_count_; |
163 | | std::size_t next_script_; |
164 | | ConnectionScript* active_script_; |
165 | | |
166 | | /// True only while HandleOpenSocket is preparing a synchronous easy drive. |
167 | | /// Every response chunk must be queued before the callback returns because |
168 | | /// curl_easy_perform does not yield control to the mock. |
169 | | bool preload_all_chunks_; |
170 | | |
171 | | /// Old server halves must outlive their active role: libcurl owns the client |
172 | | /// fds and may close or briefly revisit them after opening the next socket. |
173 | | /// Destroying a MockConnection at that boundary would turn valid lifecycle |
174 | | /// traffic into harness-generated ECONNRESET/SIGPIPE behavior. |
175 | | std::vector<std::unique_ptr<MockConnection>> previous_connections_; |
176 | | }; |
177 | | |
178 | | } // namespace proto_fuzzer |
179 | | |
180 | | #endif // PROTO_FUZZER_MOCK_SERVER_H_ |