Skip to content

Commit e6bc48e

Browse files
etrclaude
andcommitted
refactor: fold webserver_callbacks_lifecycle.cpp back into webserver_callbacks.cpp
webserver_callbacks_lifecycle.cpp (connection_notify, policy_callback, and the make_peer_address / classify_decision / is_phase_armed anonymous-namespace helpers) was originally carved out of webserver_callbacks.cpp purely to keep that TU under the FILE_LOC_MAX gate. Two things since made the split unnecessary: DR-014 moved the hook-firing bodies into hook_dispatcher (shrinking the callbacks code), and the SLOC metric now excludes single-character lines. The two files recombine to 269 SLOC — comfortably under the 500 ceiling — so this reunites the per-connection MHD lifecycle trampolines with the rest of the MHD adapter callbacks in one TU, trimming the "one class, many files" surface by one file. Pure code move: no behavior change. webserver_request.cpp stays separate — it carries the dispatch helpers (normalize_path / resolve_method_callback / should_skip_auth), a genuine concern boundary rather than gate overflow. Local: library builds, 113/113 tests pass, cpplint + complexity + file-size gates green (merged file 269 SLOC). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tAodxYJMEY4VxCX4dk62e
1 parent 5603d4e commit e6bc48e

3 files changed

Lines changed: 195 additions & 254 deletions

File tree

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ 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/request_dispatcher.cpp detail/request_pipeline.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_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.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/request_dispatcher.cpp detail/request_pipeline.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_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.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>.

src/detail/webserver_callbacks.cpp

Lines changed: 194 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,22 @@
4949
#include <iostream>
5050
#include <memory>
5151
#include <mutex>
52+
#include <optional>
5253
#include <regex>
5354
#include <set>
5455
#include <shared_mutex>
5556
#include <stdexcept>
5657
#include <string>
5758
#include <string_view>
59+
#include <tuple>
5860
#include <utility>
5961
#include <vector>
6062

6163
#include "httpserver/constants.hpp"
6264
#include "httpserver/create_webserver.hpp"
6365
#include "httpserver/feature_unavailable.hpp"
66+
#include "httpserver/hook_context.hpp"
67+
#include "httpserver/hook_phase.hpp"
6468
#include "httpserver/websocket_handler.hpp"
6569
#include "httpserver/detail/http_endpoint.hpp"
6670
#include "httpserver/detail/lambda_resource.hpp"
@@ -72,6 +76,7 @@
7276
#include "httpserver/http_utils.hpp"
7377
#include "httpserver/string_utilities.hpp"
7478
#include "httpserver/detail/body.hpp"
79+
#include "httpserver/detail/connection_state.hpp"
7580

7681
#ifdef HAVE_GNUTLS
7782
#include <gnutls/gnutls.h>
@@ -90,6 +95,73 @@ using httpserver::http::http_utils;
9095
using httpserver::http::ip_representation;
9196
using httpserver::http::base_unescaper;
9297

98+
namespace {
99+
100+
// Convert MHD's POSIX sockaddr (AF_INET / AF_INET6) into the
101+
// libhttpserver-defined peer_address POD that the hook public API
102+
// exposes. Keeps every <sys/socket.h> reference inside this TU so the
103+
// public hook surface stays MHD-clean.
104+
//
105+
// On AF_INET, the four address bytes go into bytes[0..3] (the rest stay
106+
// zero); on AF_INET6, all sixteen bytes are copied. `port` is stored in
107+
// host byte order (ntohs). A null sockaddr (defensive — MHD does not
108+
// document this case but the lookup_v2 / connection_info paths can
109+
// return nullptr in edge cases) yields a zeroed peer_address with
110+
// family::unspec so the hook still observes the event with a
111+
// meaningful "unknown peer" signal.
112+
::httpserver::peer_address make_peer_address(const struct sockaddr* addr) {
113+
::httpserver::peer_address out{};
114+
if (addr == nullptr) return out;
115+
if (addr->sa_family == AF_INET) {
116+
const auto* sin = reinterpret_cast<const struct sockaddr_in*>(addr);
117+
out.fam = ::httpserver::peer_address::family::ipv4;
118+
std::memcpy(out.bytes.data(), &sin->sin_addr, 4);
119+
out.port = ntohs(sin->sin_port);
120+
} else if (addr->sa_family == AF_INET6) {
121+
const auto* sin6 = reinterpret_cast<const struct sockaddr_in6*>(addr);
122+
out.fam = ::httpserver::peer_address::family::ipv6;
123+
std::memcpy(out.bytes.data(), &sin6->sin6_addr, 16);
124+
out.port = ntohs(sin6->sin6_port);
125+
}
126+
return out;
127+
}
128+
129+
// Reduce a {default_policy, is_denied, is_allowed} triple to the
130+
// (accepted, reason) decision exposed via accept_ctx. Extracted out of
131+
// policy_callback to keep that function under the CCN gate.
132+
//
133+
// Truth table:
134+
// default=ACCEPT, denied, !allowed -> reject "denied"
135+
// default=REJECT, denied -> reject "denied"
136+
// default=REJECT, !denied, !allowed-> reject "not-on-allow-list"
137+
// anything else -> accept
138+
// An allow-list entry always overrides a deny-list entry (allow wins).
139+
std::pair<bool, std::optional<std::string_view>>
140+
classify_decision(int default_policy, bool is_denied, bool is_allowed) {
141+
if (default_policy == ::httpserver::http::http_utils::ACCEPT
142+
&& is_denied && !is_allowed) {
143+
return {false, std::string_view{"denied"}};
144+
}
145+
if (default_policy == ::httpserver::http::http_utils::REJECT) {
146+
if (is_denied) return {false, std::string_view{"denied"}};
147+
if (!is_allowed) return {false, std::string_view{"not-on-allow-list"}};
148+
}
149+
return {true, std::nullopt};
150+
}
151+
152+
// Short-circuit gate for a single phase: returns true when at least one
153+
// hook is registered. Shared by connection_notify (via the hooks_armed
154+
// lambda which delegates to this) and policy_callback (which calls it
155+
// directly since the hooks_armed lambda is local to connection_notify).
156+
// Uses relaxed memory order: a false-negative on a very early concurrent
157+
// add is acceptable; the hook simply fires on the next event.
158+
inline bool is_phase_armed(const detail::webserver_impl* impl,
159+
::httpserver::hook_phase p) noexcept {
160+
return impl != nullptr && impl->has_hooks_for(p);
161+
}
162+
163+
} // namespace
164+
93165

94166
namespace detail {
95167

@@ -158,11 +230,128 @@ void webserver_impl::request_completed(void *cls, struct MHD_Connection *connect
158230
}
159231
}
160232

161-
// connection_notify (NOTIFY_STARTED / NOTIFY_CLOSED) and policy_callback
162-
// live in webserver_callbacks_lifecycle.cpp. Both
163-
// callbacks fire lifecycle hooks and reach into <sys/socket.h> via the
164-
// peer-address adapter; isolating them keeps this TU under the project
165-
// FILE_LOC_MAX gate.
233+
// connection_notify (NOTIFY_STARTED / NOTIFY_CLOSED) and policy_callback:
234+
// the per-connection lifecycle trampolines. Both fire lifecycle hooks and
235+
// reach into <sys/socket.h> via the peer-address adapter in the anonymous
236+
// namespace above. (Previously carved into webserver_callbacks_lifecycle.cpp
237+
// for the FILE_LOC_MAX gate; folded back once the SLOC metric stopped
238+
// counting single-character lines brought this TU comfortably under the
239+
// ceiling.)
240+
241+
void webserver_impl::connection_notify(void* cls, struct MHD_Connection* connection,
242+
void** socket_context,
243+
enum MHD_ConnectionNotificationCode toe) {
244+
// cls is the owning webserver* (set in webserver_lifecycle.cpp at
245+
// MHD_OPTION_NOTIFY_CONNECTION). It MAY be null in tests that
246+
// exercise the callback without an enclosing webserver; defensive
247+
// null-check gates every hook fire on a non-null impl.
248+
auto* ws = static_cast<webserver*>(cls);
249+
// `ws_impl` names the owning webserver's impl explicitly, distinguishing
250+
// it from the static trampoline's implicit `this` (which is the
251+
// webserver_impl on which connection_notify is declared, but the
252+
// trampoline pattern means `this` is never used here directly).
253+
webserver_impl* ws_impl = (ws != nullptr) ? ws->impl_.get() : nullptr;
254+
255+
// Resolve the peer address from MHD via the live MHD_Connection*.
256+
// The connection is valid in both the STARTED and CLOSED branches
257+
// (MHD tears it down only after NOTIFY_CLOSED returns).
258+
auto resolve_peer = [&]() -> ::httpserver::peer_address {
259+
if (connection == nullptr) return {};
260+
const MHD_ConnectionInfo* ci = MHD_get_connection_info(
261+
connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS);
262+
if (ci == nullptr) return {};
263+
return make_peer_address(ci->client_addr);
264+
};
265+
266+
// Short-circuit predicate: delegates to the file-scope is_phase_armed
267+
// free function so that both connection_notify and policy_callback use
268+
// the same idiom without duplicating the cast/load expression.
269+
auto hooks_armed = [&ws_impl](::httpserver::hook_phase p) -> bool {
270+
return is_phase_armed(ws_impl, p);
271+
};
272+
273+
switch (toe) {
274+
case MHD_CONNECTION_NOTIFY_STARTED: {
275+
// Allocate the per-connection state (and its embedded arena)
276+
// on connection start. The new is the only heap allocation
277+
// tied to a connection's lifetime; afterwards every request
278+
// on this connection draws its impl out of the arena.
279+
auto* cs = new detail::connection_state();
280+
// Copy the per-request args DoS limits from the owning
281+
// webserver so populate_args() can size the
282+
// arguments_accumulator from the socket_context. 0 means
283+
// "use the compile-time defaults" -- see connection_state.hpp.
284+
if (ws != nullptr) {
285+
cs->max_args_count = ws->config.max_args_count;
286+
cs->max_args_bytes = ws->config.max_args_bytes;
287+
}
288+
*socket_context = cs;
289+
// Fire connection_opened. Zero-cost when no hook is
290+
// registered: a single relaxed atomic load + branch.
291+
if (hooks_armed(::httpserver::hook_phase::connection_opened)) {
292+
::httpserver::connection_open_ctx ctx{resolve_peer()};
293+
ws_impl->hooks_dispatch_.fire_connection_opened(ctx);
294+
}
295+
break;
296+
}
297+
case MHD_CONNECTION_NOTIFY_CLOSED:
298+
// Fire connection_closed BEFORE the per-connection state is
299+
// deleted. The arena is not exposed through
300+
// connection_close_ctx today (only peer_address), but the
301+
// ordering choice is safe regardless and pins the contract.
302+
if (hooks_armed(::httpserver::hook_phase::connection_closed)) {
303+
::httpserver::connection_close_ctx ctx{resolve_peer()};
304+
ws_impl->hooks_dispatch_.fire_connection_closed(ctx);
305+
}
306+
// MHD ordering guarantee: NOTIFY_COMPLETED fires before
307+
// NOTIFY_CLOSED for the same connection. By the time we reach
308+
// this branch, request_completed has already called reset_arena()
309+
// and the modded_request has already been deleted -- so the
310+
// connection_state is no longer referenced by any live object.
311+
// (Documents the invariant that prevents the concurrent
312+
// request_completed + NOTIFY_CLOSED race described in CWE-362.)
313+
delete static_cast<detail::connection_state*>(*socket_context);
314+
*socket_context = nullptr;
315+
break;
316+
}
317+
}
318+
319+
MHD_Result webserver_impl::policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen) {
320+
// Parameter needed to respect MHD interface, but not needed here.
321+
std::ignore = addrlen;
322+
323+
const auto ws = static_cast<webserver*>(cls);
324+
auto* impl = ws->impl_.get();
325+
326+
// Compute the accept/reject decision (and its reason) up front,
327+
// then fire `accept_decision` strictly AFTER the decision is fixed
328+
// in `decision`. A throwing hook lands in fire_accept_decision's
329+
// catch and cannot change `decision` — this is a structural
330+
// guarantee, not a runtime check.
331+
bool accepted = true;
332+
std::optional<std::string_view> reason{};
333+
334+
if (ws->config.ip_access_control_enabled) {
335+
// Consistent deny/allow snapshot under the acl's own locks, released
336+
// before the user hook fires.
337+
const auto membership = impl->acl_.classify(ip_representation(addr));
338+
std::tie(accepted, reason) =
339+
classify_decision(ws->config.default_policy,
340+
membership.denied, membership.allowed);
341+
}
342+
343+
const MHD_Result decision = accepted ? MHD_YES : MHD_NO;
344+
345+
// Fire the hook strictly after `decision` is fixed. The relaxed
346+
// atomic gate keeps zero-cost-when-unused.
347+
if (is_phase_armed(impl, ::httpserver::hook_phase::accept_decision)) {
348+
::httpserver::accept_ctx ctx{
349+
make_peer_address(addr), accepted, reason};
350+
impl->hooks_dispatch_.fire_accept_decision(ctx);
351+
}
352+
353+
return decision;
354+
}
166355

167356
#ifdef HAVE_GNUTLS
168357
// MHD_PskServerCredentialsCallback signature:

0 commit comments

Comments
 (0)