Skip to content

Commit 4da8201

Browse files
etrclaude
andcommitted
refactor(dr-014): extract websocket_upgrader behavior service
Move the RFC-6455 upgrade handshake, upgrade completion, upgrade callback, and per-connection frame receive loop off webserver_impl into detail::websocket_upgrader (HAVE_WEBSOCKET-gated, like the ws_registry it depends on). Holds only ws_registry&. The ws_upgrade_data closure loses its unused webserver_impl* back-pointer. Only finalize_answer's try_handle_websocket_upgrade call is external, so webserver_websocket.cpp keeps just that one forwarder (outside the HAVE_WEBSOCKET guard: a no-op on WS-off builds, degrading to normal HTTP dispatch). validate_websocket_handshake / complete_websocket_upgrade / upgrade_handler / ws_upgrade_data move fully into the service. Verified on the WS-off build (113/113, gates green); the HAVE_WEBSOCKET body is a verbatim move and is exercised by CI's websocket lanes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dc3f141 commit 4da8201

7 files changed

Lines changed: 402 additions & 323 deletions

File tree

src/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ lib_LTLIBRARIES = libhttpserver.la
2525
# builds. The WS-off branch in websocket_handler.cpp provides stub
2626
# definitions (every member throws feature_unavailable except is_valid()
2727
# which returns false).
28-
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/hook_dispatcher.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/response_materializer.cpp detail/upload_pipeline.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/hook_dispatcher.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/response_materializer.cpp detail/upload_pipeline.cpp detail/websocket_upgrader.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.cpp
2929
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
3030
# Detail headers (httpserver/detail/*.hpp) live here so they cannot leak to
3131
# downstream consumers — the public surface comes in through <httpserver.hpp>.
32-
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/hook_dispatcher.hpp httpserver/detail/response_materializer.hpp httpserver/detail/upload_pipeline.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
32+
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/hook_dispatcher.hpp httpserver/detail/response_materializer.hpp httpserver/detail/upload_pipeline.hpp httpserver/detail/websocket_upgrader.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
3333
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/cookie.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_webserver_setters.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/webserver_routes.hpp httpserver/webserver_runtime.hpp httpserver/webserver_websocket.hpp httpserver/webserver_hooks.hpp httpserver/websocket_handler.hpp httpserver/http_utils.hpp httpserver/http_utils_helpers.hpp httpserver/ip_representation.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp httpserver/hook_phase.hpp httpserver/hook_action.hpp httpserver/hook_handle.hpp httpserver/hook_context.hpp
3434

3535
AM_CXXFLAGS += -fPIC -Wall

src/detail/webserver_websocket.cpp

Lines changed: 12 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
This file is part of libhttpserver
3-
Copyright (C) 2011-2019 Sebastiano Merlino
3+
Copyright (C) 2011-2026 Sebastiano Merlino
44
55
This library is free software; you can redistribute it and/or
66
modify it under the terms of the GNU Lesser General Public
@@ -18,322 +18,35 @@
1818
USA
1919
*/
2020

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+
2129
#include "httpserver/webserver.hpp"
2230
#include "httpserver/detail/webserver_impl.hpp"
2331

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>
3732
#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"
7433

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>
8535

8636
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-
17537
namespace detail {
17638

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 {
31139
std::optional<MHD_Result>
31240
webserver_impl::try_handle_websocket_upgrade(MHD_Connection* connection,
31341
detail::modded_request* mr) {
31442
#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);
33144
#else
33245
(void)connection;
33346
(void)mr;
33447
return std::nullopt;
33548
#endif // HAVE_WEBSOCKET
33649
}
337-
} // namespace detail
33850

51+
} // namespace detail
33952
} // namespace httpserver

0 commit comments

Comments
 (0)