|
1 | 1 | /* |
2 | 2 | This file is part of libhttpserver |
3 | | - Copyright (C) 2011-2019 Sebastiano Merlino |
| 3 | + Copyright (C) 2011-2026 Sebastiano Merlino |
4 | 4 |
|
5 | 5 | This library is free software; you can redistribute it and/or |
6 | 6 | modify it under the terms of the GNU Lesser General Public |
|
18 | 18 | USA |
19 | 19 | */ |
20 | 20 |
|
| 21 | +// Thin webserver_impl forwarder for the websocket upgrade probe into the |
| 22 | +// websocket_upgrader behavior service (DR-014 §4.11). The handshake / |
| 23 | +// completion / frame-loop logic moved to detail/websocket_upgrader.cpp. |
| 24 | +// This forwarder lives outside the HAVE_WEBSOCKET guard because |
| 25 | +// finalize_answer (webserver_request.cpp) calls it unconditionally; on |
| 26 | +// HAVE_WEBSOCKET-off builds it is a no-op that degrades to normal HTTP |
| 27 | +// dispatch. Removed in the final slim step. |
| 28 | + |
21 | 29 | #include "httpserver/webserver.hpp" |
22 | 30 | #include "httpserver/detail/webserver_impl.hpp" |
23 | 31 |
|
24 | | -#if defined(_WIN32) && !defined(__CYGWIN__) |
25 | | -#include <winsock2.h> |
26 | | -#include <ws2tcpip.h> |
27 | | -#define _WINDOWS |
28 | | -#else |
29 | | -#if defined(__CYGWIN__) |
30 | | -#include <sys/select.h> |
31 | | -#endif |
32 | | -#include <netinet/in.h> |
33 | | -#include <netinet/tcp.h> |
34 | | -#endif |
35 | | - |
36 | | -#include <errno.h> |
37 | 32 | #include <microhttpd.h> |
38 | | -#ifdef HAVE_WEBSOCKET |
39 | | -#include <microhttpd_ws.h> |
40 | | -#endif // HAVE_WEBSOCKET |
41 | | -#include <signal.h> |
42 | | -#include <stdint.h> |
43 | | -#include <stdio.h> |
44 | | -#include <stdlib.h> |
45 | | -#include <strings.h> |
46 | | -#include <algorithm> |
47 | | -#include <cstring> |
48 | | -#include <iosfwd> |
49 | | -#include <iostream> |
50 | | -#include <memory> |
51 | | -#include <mutex> |
52 | | -#include <regex> |
53 | | -#include <set> |
54 | | -#include <shared_mutex> |
55 | | -#include <stdexcept> |
56 | | -#include <string> |
57 | | -#include <string_view> |
58 | | -#include <utility> |
59 | | -#include <vector> |
60 | | - |
61 | | -#include "httpserver/constants.hpp" |
62 | | -#include "httpserver/create_webserver.hpp" |
63 | | -#include "httpserver/feature_unavailable.hpp" |
64 | | -#include "httpserver/websocket_handler.hpp" |
65 | | -#include "httpserver/detail/http_endpoint.hpp" |
66 | | -#include "httpserver/detail/lambda_resource.hpp" |
67 | | -#include "httpserver/detail/modded_request.hpp" |
68 | | -#include "httpserver/http_request.hpp" |
69 | | -#include "httpserver/http_resource.hpp" |
70 | | -#include "httpserver/http_response.hpp" |
71 | | -#include "httpserver/http_utils.hpp" |
72 | | -#include "httpserver/string_utilities.hpp" |
73 | | -#include "httpserver/detail/body.hpp" |
74 | 33 |
|
75 | | -#ifdef HAVE_GNUTLS |
76 | | -#include <gnutls/gnutls.h> |
77 | | -#include <gnutls/x509.h> |
78 | | -#endif // HAVE_GNUTLS |
79 | | - |
80 | | -using std::string; |
81 | | -using std::pair; |
82 | | -using std::vector; |
83 | | -using std::map; |
84 | | -using std::set; |
| 34 | +#include <optional> |
85 | 35 |
|
86 | 36 | namespace httpserver { |
87 | | - |
88 | | -using httpserver::http::http_utils; |
89 | | -using httpserver::http::ip_representation; |
90 | | -using httpserver::http::base_unescaper; |
91 | | - |
92 | | - |
93 | | -#ifdef HAVE_WEBSOCKET |
94 | | -namespace { |
95 | | - |
96 | | -// RFC 6455 §5.5.1: a CLOSE frame's payload starts with a 2-byte |
97 | | -// status code (default 1000 "normal closure") followed by an optional |
98 | | -// UTF-8 reason. Pulled out of dispatch_websocket_frame so the switch |
99 | | -// stays under the CCN bar. |
100 | | -void handle_close_frame(websocket_handler* handler, websocket_session& session, |
101 | | - const char* frame_data, size_t frame_len) { |
102 | | - uint16_t close_code = 1000; |
103 | | - std::string close_reason; |
104 | | - if (frame_len >= 2) { |
105 | | - close_code = static_cast<uint16_t>( |
106 | | - (static_cast<unsigned char>(frame_data[0]) << 8) | |
107 | | - static_cast<unsigned char>(frame_data[1])); |
108 | | - if (frame_len > 2) { |
109 | | - close_reason.assign(frame_data + 2, frame_len - 2); |
110 | | - } |
111 | | - } |
112 | | - handler->on_close(session, close_code, close_reason); |
113 | | - // Echo the close back and end the loop. |
114 | | - session.close(close_code, close_reason); |
115 | | -} |
116 | | - |
117 | | -void dispatch_websocket_frame(int status, struct MHD_WebSocketStream* ws_stream, |
118 | | - websocket_handler* handler, |
119 | | - websocket_session& session, |
120 | | - char* frame_data, size_t frame_len) { |
121 | | - switch (status) { |
122 | | - case MHD_WEBSOCKET_STATUS_TEXT_FRAME: |
123 | | - handler->on_message(session, std::string_view(frame_data, frame_len)); |
124 | | - MHD_websocket_free(ws_stream, frame_data); |
125 | | - break; |
126 | | - case MHD_WEBSOCKET_STATUS_BINARY_FRAME: |
127 | | - handler->on_binary(session, frame_data, frame_len); |
128 | | - MHD_websocket_free(ws_stream, frame_data); |
129 | | - break; |
130 | | - case MHD_WEBSOCKET_STATUS_PING_FRAME: |
131 | | - handler->on_ping(session, std::string_view(frame_data, frame_len)); |
132 | | - MHD_websocket_free(ws_stream, frame_data); |
133 | | - break; |
134 | | - case MHD_WEBSOCKET_STATUS_CLOSE_FRAME: |
135 | | - handle_close_frame(handler, session, frame_data, frame_len); |
136 | | - MHD_websocket_free(ws_stream, frame_data); |
137 | | - break; |
138 | | - case MHD_WEBSOCKET_STATUS_OK: |
139 | | - // Need more data - go back to recv. |
140 | | - if (frame_data != nullptr) MHD_websocket_free(ws_stream, frame_data); |
141 | | - break; |
142 | | - default: |
143 | | - // Protocol error or unknown frame. |
144 | | - if (frame_data != nullptr) MHD_websocket_free(ws_stream, frame_data); |
145 | | - session.close(1002, "Protocol error"); |
146 | | - break; |
147 | | - } |
148 | | -} |
149 | | - |
150 | | -} // namespace |
151 | | - |
152 | | -static void decode_websocket_buffer(struct MHD_WebSocketStream* ws_stream, |
153 | | - websocket_handler* handler, |
154 | | - websocket_session& session, |
155 | | - const char* buf, size_t buf_len) { |
156 | | - size_t offset = 0; |
157 | | - while (offset < buf_len && session.is_valid()) { |
158 | | - char* frame_data = nullptr; |
159 | | - size_t frame_len = 0; |
160 | | - size_t step = 0; |
161 | | - int status = MHD_websocket_decode(ws_stream, |
162 | | - buf + offset, |
163 | | - buf_len - offset, |
164 | | - &step, |
165 | | - &frame_data, |
166 | | - &frame_len); |
167 | | - offset += step; |
168 | | - dispatch_websocket_frame(status, ws_stream, handler, session, |
169 | | - frame_data, frame_len); |
170 | | - // If decode consumed no bytes, we need more data. |
171 | | - if (step == 0) break; |
172 | | - } |
173 | | -} |
174 | | - |
175 | 37 | namespace detail { |
176 | 38 |
|
177 | | -// validate_websocket_handshake / complete_websocket_upgrade live here |
178 | | -// rather than in webserver_request.cpp to keep that TU under |
179 | | -// FILE_LOC_MAX. |
180 | | -std::optional<const char*> |
181 | | -webserver_impl::validate_websocket_handshake(MHD_Connection* connection) { |
182 | | - const char* connection_header = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, |
183 | | - MHD_HTTP_HEADER_CONNECTION); |
184 | | - const char* ws_version = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, |
185 | | - "Sec-WebSocket-Version"); |
186 | | - const char* ws_key = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, |
187 | | - "Sec-WebSocket-Key"); |
188 | | - if (connection_header == nullptr || strcasestr(connection_header, "Upgrade") == nullptr) { |
189 | | - return std::nullopt; |
190 | | - } |
191 | | - if (ws_version == nullptr || strcmp(ws_version, "13") != 0) { |
192 | | - return std::nullopt; |
193 | | - } |
194 | | - if (ws_key == nullptr || ws_key[0] == '\0') { |
195 | | - return std::nullopt; |
196 | | - } |
197 | | - return ws_key; |
198 | | -} |
199 | | - |
200 | | -// Return contract: an engaged optional carries the MHD_queue_response |
201 | | -// result for the 101 handshake. std::nullopt is returned for several |
202 | | -// DISTINCT situations — no websocket handler registered at the request |
203 | | -// path, MHD_create_response_for_upgrade failure, and Sec-WebSocket- |
204 | | -// Accept computation failure. The empty optional propagates through |
205 | | -// try_handle_websocket_upgrade to the dispatch path in |
206 | | -// webserver_request.cpp, which deliberately degrades ALL of these |
207 | | -// cases to normal HTTP dispatch rather than failing the request. |
208 | | -std::optional<MHD_Result> |
209 | | -webserver_impl::complete_websocket_upgrade(MHD_Connection* connection, |
210 | | - detail::modded_request* mr, |
211 | | - const char* ws_key) { |
212 | | - // find() returns a shared_ptr copy taken under the registry's read lock, |
213 | | - // so the handler is kept alive across the MHD upgrade callback even if |
214 | | - // unregister_ws_resource erases the slot mid-upgrade. |
215 | | - std::shared_ptr<websocket_handler> handler_sp = ws_.find(mr->standardized_url); |
216 | | - if (!handler_sp) { |
217 | | - return std::nullopt; |
218 | | - } |
219 | | - |
220 | | - // CWE-401: RAII guard so data is freed if MHD_create_response_for_upgrade |
221 | | - // returns null. Ownership is transferred to MHD (via release()) only after |
222 | | - // the queue call — upgrade_handler receives data_guard.get() as cls and |
223 | | - // wraps it in unique_ptr for cleanup (see upgrade_handler below). |
224 | | - std::unique_ptr<ws_upgrade_data> data_guard( |
225 | | - new ws_upgrade_data{this, std::move(handler_sp)}); |
226 | | - struct MHD_Response* response = MHD_create_response_for_upgrade( |
227 | | - &webserver_impl::upgrade_handler, data_guard.get()); |
228 | | - if (response == nullptr) { |
229 | | - return std::nullopt; |
230 | | - } |
231 | | - MHD_add_response_header(response, MHD_HTTP_HEADER_UPGRADE, "websocket"); |
232 | | - |
233 | | - // Compute Sec-WebSocket-Accept from client's key (RFC 6455 §4.2.2). |
234 | | - // Base64 of SHA-1 = 28 chars + null. |
235 | | - // RFC 6455 §4.2.2: the Sec-WebSocket-Accept header is required; if the |
236 | | - // library call fails treat it as a fatal handshake error and abort. |
237 | | - char accept_header[29]; |
238 | | - if (MHD_websocket_create_accept_header(ws_key, accept_header) != MHD_WEBSOCKET_STATUS_OK) { |
239 | | - MHD_destroy_response(response); |
240 | | - return std::nullopt; |
241 | | - } |
242 | | - MHD_add_response_header(response, "Sec-WebSocket-Accept", accept_header); |
243 | | - MHD_Result to_ret = (MHD_Result) MHD_queue_response(connection, |
244 | | - MHD_HTTP_SWITCHING_PROTOCOLS, |
245 | | - response); |
246 | | - MHD_destroy_response(response); |
247 | | - if (to_ret == MHD_YES) { |
248 | | - // Transfer ownership to MHD: upgrade_handler receives data_guard.get() |
249 | | - // as cls and wraps it in unique_ptr for cleanup. Only release after a |
250 | | - // confirmed successful queue; if MHD_queue_response returns MHD_NO the |
251 | | - // upgrade callback will never fire, so data_guard's destructor frees the |
252 | | - // allocation instead. |
253 | | - data_guard.release(); |
254 | | - } |
255 | | - return to_ret; |
256 | | -} |
257 | | - |
258 | | -void webserver_impl::upgrade_handler(void *cls, struct MHD_Connection* connection, |
259 | | - void *req_cls, const char *extra_in, |
260 | | - size_t extra_in_size, MHD_socket sock, |
261 | | - struct MHD_UpgradeResponseHandle *urh) { |
262 | | - std::ignore = connection; |
263 | | - std::ignore = req_cls; |
264 | | - |
265 | | - // Own ws_upgrade_data via unique_ptr for the duration of |
266 | | - // the session. The shared_ptr<websocket_handler> inside `data` |
267 | | - // keeps the handler alive across this upgrade callback even if a |
268 | | - // concurrent unregister_ws_resource drops the registration in the |
269 | | - // owning webserver. |
270 | | - std::unique_ptr<ws_upgrade_data> data(static_cast<ws_upgrade_data*>(cls)); |
271 | | - websocket_handler* handler = data->handler.get(); |
272 | | - |
273 | | - // Create a WebSocket stream for this connection |
274 | | - struct MHD_WebSocketStream* ws_stream = nullptr; |
275 | | - int ws_result = MHD_websocket_stream_init(&ws_stream, |
276 | | - MHD_WEBSOCKET_FLAG_SERVER | MHD_WEBSOCKET_FLAG_NO_FRAGMENTS, |
277 | | - 0); |
278 | | - if (ws_result != MHD_WEBSOCKET_STATUS_OK || ws_stream == nullptr) { |
279 | | - MHD_upgrade_action(urh, MHD_UPGRADE_ACTION_CLOSE); |
280 | | - return; |
281 | | - } |
282 | | - |
283 | | - websocket_session session(static_cast<std::uintptr_t>(sock), urh, ws_stream); |
284 | | - handler->on_open(session); |
285 | | - |
286 | | - // Process any initial data that MHD may have buffered |
287 | | - if (extra_in != nullptr && extra_in_size > 0) { |
288 | | - decode_websocket_buffer(ws_stream, handler, session, extra_in, extra_in_size); |
289 | | - } |
290 | | - |
291 | | - // Receive loop |
292 | | - char buf[4096]; |
293 | | - while (session.is_valid()) { |
294 | | - ssize_t got = recv(sock, buf, sizeof(buf), 0); |
295 | | - if (got <= 0) break; |
296 | | - |
297 | | - decode_websocket_buffer(ws_stream, handler, session, |
298 | | - buf, static_cast<size_t>(got)); |
299 | | - } |
300 | | - |
301 | | - // Session destructor will free ws_stream and close urh. |
302 | | - // `data` (and its shared_ptr handler reference) goes out of scope here. |
303 | | -} |
304 | | - |
305 | | -} // namespace detail |
306 | | -#endif // HAVE_WEBSOCKET |
307 | | - |
308 | | -// try_handle_websocket_upgrade lives outside the HAVE_WEBSOCKET |
309 | | -// guard because it has a no-op definition on the WS-off branch. |
310 | | -namespace detail { |
311 | 39 | std::optional<MHD_Result> |
312 | 40 | webserver_impl::try_handle_websocket_upgrade(MHD_Connection* connection, |
313 | 41 | detail::modded_request* mr) { |
314 | 42 | #ifdef HAVE_WEBSOCKET |
315 | | - const char* upgrade_header = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, |
316 | | - MHD_HTTP_HEADER_UPGRADE); |
317 | | - if (upgrade_header == nullptr || strcasecmp(upgrade_header, "websocket") != 0) { |
318 | | - return std::nullopt; |
319 | | - } |
320 | | - auto ws_key = validate_websocket_handshake(connection); |
321 | | - if (!ws_key) { |
322 | | - // RFC 6455 §4.2.1: required handshake header missing or malformed. |
323 | | - struct MHD_Response* bad_response = MHD_create_response_from_buffer(0, nullptr, |
324 | | - MHD_RESPMEM_PERSISTENT); |
325 | | - MHD_Result ret = (MHD_Result) MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, |
326 | | - bad_response); |
327 | | - MHD_destroy_response(bad_response); |
328 | | - return ret; |
329 | | - } |
330 | | - return complete_websocket_upgrade(connection, mr, *ws_key); |
| 43 | + return ws_upgrader_.try_handle(connection, mr); |
331 | 44 | #else |
332 | 45 | (void)connection; |
333 | 46 | (void)mr; |
334 | 47 | return std::nullopt; |
335 | 48 | #endif // HAVE_WEBSOCKET |
336 | 49 | } |
337 | | -} // namespace detail |
338 | 50 |
|
| 51 | +} // namespace detail |
339 | 52 | } // namespace httpserver |
0 commit comments