Skip to content

Commit 741faf4

Browse files
etrclaude
andcommitted
refactor(dr-014): extract response_materializer behavior service
Move the http_response -> MHD_Response wire construction, decoration, kind-dispatched queueing (plain + RFC-7616 digest challenge), the response_sent firing, and the belt-and-suspenders fallback chain off webserver_impl into detail::response_materializer. It holds error_pages& (error synthesis), hook_dispatcher& (response_sent), the digest opaque string, and const webserver_config& (logging); friend of http_response for body_ access. Only finalize_answer's materialize_and_queue_response call is external, so webserver_response_queue.cpp keeps just that one forwarder; the other four methods (get_raw_response_with_fallback, queue_response_dispatching_kind, materialize_response, decorate_mhd_response) had no external callers and move fully into the service, their declarations dropped from the fragment header. digest_opaque_ is made an unconditional webserver_impl member (empty on non-HAVE_DAUTH builds; the ctor-body generation and the queueing branch stay gated) so the service binds a plain const std::string& without HAVE_DAUTH in its signature. 113/113 tests pass; cpplint/complexity/file-size gates green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6c2eec3 commit 741faf4

7 files changed

Lines changed: 393 additions & 277 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/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/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/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/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
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// response_materializer behavior service (DR-014 §4.11). Logic moved
22+
// verbatim out of the former detail/webserver_response_queue.cpp; that TU
23+
// now holds only the thin webserver_impl::materialize_and_queue_response
24+
// forwarder. Rewiring vs the original: the error paths call errors_
25+
// (error_pages) instead of the in-class helpers; response_sent fires via
26+
// hook_dispatch_; log_dispatch_error is the free function over config_.
27+
28+
#include "httpserver/detail/response_materializer.hpp"
29+
30+
#include <microhttpd.h>
31+
32+
#include <optional>
33+
#include <stdexcept>
34+
#include <string>
35+
#include <utility>
36+
37+
#include "httpserver/create_webserver.hpp"
38+
#include "httpserver/http_response.hpp"
39+
#include "httpserver/http_utils.hpp"
40+
#include "httpserver/detail/body.hpp"
41+
#include "httpserver/detail/dispatch_util.hpp"
42+
#include "httpserver/detail/error_pages.hpp"
43+
#include "httpserver/detail/hook_dispatcher.hpp"
44+
#include "httpserver/detail/modded_request.hpp"
45+
46+
namespace httpserver {
47+
namespace detail {
48+
49+
// materialize_response: ask the body to produce a fresh MHD_Response with
50+
// no headers/footers/cookies attached. webserver_impl / response_materializer
51+
// are friends of http_response so body_ is reachable directly.
52+
MHD_Response* response_materializer::materialize_response(http_response* resp) {
53+
if (resp == nullptr || resp->body_ == nullptr) {
54+
return nullptr;
55+
}
56+
return resp->body_->materialize();
57+
}
58+
59+
// decorate_mhd_response: walk the response's header/footer/cookie maps and
60+
// attach each to the materialized MHD_Response.
61+
void response_materializer::decorate_mhd_response(MHD_Response* response,
62+
const http_response& resp) {
63+
for (const auto& [k, v] : resp.get_headers()) {
64+
MHD_add_response_header(response, k.c_str(), v.c_str());
65+
}
66+
for (const auto& [k, v] : resp.get_footers()) {
67+
MHD_add_response_footer(response, k.c_str(), v.c_str());
68+
}
69+
// Render from the structured cookie list (not the legacy cookies_ map)
70+
// via cookie::to_set_cookie_header() so attributes propagate to the wire
71+
// per RFC 6265 §4.1.
72+
for (const auto& c : resp.get_cookies_parsed()) {
73+
const std::string cookie_hdr = c.to_set_cookie_header();
74+
MHD_add_response_header(response, "Set-Cookie", cookie_hdr.c_str());
75+
}
76+
}
77+
78+
struct MHD_Response* response_materializer::get_raw_response_with_fallback(
79+
detail::modded_request* mr) {
80+
// Every assignment into mr->response uses emplace(std::move(...)); the
81+
// optional owns the value and the deferred-body trampoline keeps a
82+
// pointer into it for the lifetime of the modded_request.
83+
auto try_materialize = [&]() -> struct MHD_Response* {
84+
return materialize_response(mr->response ? &*mr->response : nullptr);
85+
};
86+
auto emplace_and_materialize = [&](http_response r) -> struct MHD_Response* {
87+
mr->response.emplace(std::move(r));
88+
return try_materialize();
89+
};
90+
try {
91+
struct MHD_Response* raw = try_materialize();
92+
if (raw == nullptr) {
93+
// No exception, but the body materializer returned null. Route
94+
// through the safe internal-error path.
95+
return emplace_and_materialize(
96+
errors_.run_internal_error_handler_safely(
97+
mr, "materialize_response returned null"));
98+
}
99+
return raw;
100+
} catch(const std::invalid_argument&) {
101+
try {
102+
return emplace_and_materialize(errors_.not_found_page(mr));
103+
} catch(...) {
104+
return nullptr;
105+
}
106+
} catch(const std::exception& e) {
107+
log_dispatch_error(config_, std::string("materialize threw: ") + e.what());
108+
try {
109+
return emplace_and_materialize(
110+
errors_.run_internal_error_handler_safely(mr, e.what()));
111+
} catch(...) {
112+
return nullptr;
113+
}
114+
} catch(...) {
115+
log_dispatch_error(config_, "materialize threw unknown exception");
116+
try {
117+
return emplace_and_materialize(
118+
errors_.run_internal_error_handler_safely(mr,
119+
"unknown exception"));
120+
} catch(...) {
121+
return nullptr;
122+
}
123+
}
124+
}
125+
126+
// Kind-dispatched queueing. For body_kind::digest_challenge, delegate to
127+
// MHD_queue_auth_required_response3 so libmicrohttpd writes the RFC-7616
128+
// WWW-Authenticate header with its HMAC-keyed nonce, our opaque, and the
129+
// requested algorithm/qop/charset/userhash bits. Every other body kind goes
130+
// through the standard MHD_queue_response path. The digest mapping is
131+
// factored into the map_to_mhd_digest_args_ anonymous-namespace helper so
132+
// the dispatcher stays under the CCN ceiling.
133+
#ifdef HAVE_DAUTH
134+
namespace {
135+
struct mhd_digest_args {
136+
MHD_DigestAuthMultiAlgo3 algo;
137+
MHD_DigestAuthMultiQOP qop;
138+
const char* opaque_cstr;
139+
const char* domain_cstr;
140+
};
141+
142+
mhd_digest_args map_to_mhd_digest_args_(
143+
const detail::digest_challenge_body::params& p,
144+
const std::string& server_opaque) {
145+
MHD_DigestAuthMultiAlgo3 algo;
146+
switch (p.algorithm) {
147+
case http::http_utils::digest_algorithm::SHA256:
148+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA256;
149+
break;
150+
case http::http_utils::digest_algorithm::SHA512_256:
151+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA512_256;
152+
break;
153+
case http::http_utils::digest_algorithm::MD5:
154+
default:
155+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_MD5;
156+
break;
157+
}
158+
// qop="auth" is the only v2.0-supported variant; auth-int is parked.
159+
// qop_auth == false -> RFC-2069 no-qop.
160+
MHD_DigestAuthMultiQOP qop = p.qop_auth
161+
? MHD_DIGEST_AUTH_MULT_QOP_AUTH
162+
: MHD_DIGEST_AUTH_MULT_QOP_NONE;
163+
// Empty user opaque -> substitute the per-webserver opaque.
164+
const char* opaque_cstr =
165+
p.opaque.empty() ? server_opaque.c_str() : p.opaque.c_str();
166+
const char* domain_cstr =
167+
p.domain.empty() ? nullptr : p.domain.c_str();
168+
return {algo, qop, opaque_cstr, domain_cstr};
169+
}
170+
} // namespace
171+
#endif // HAVE_DAUTH
172+
173+
int response_materializer::queue_response_dispatching_kind(
174+
MHD_Connection* connection,
175+
detail::modded_request* mr,
176+
MHD_Response* raw_response) {
177+
#ifdef HAVE_DAUTH
178+
if (mr->response->kind() == body_kind::digest_challenge) {
179+
auto* dch = static_cast<detail::digest_challenge_body*>(
180+
mr->response->body_);
181+
if (dch == nullptr) {
182+
// Defensive guard (CWE-476): kind() reported digest_challenge but
183+
// body_ is null. Fall back to the plain queue path.
184+
return static_cast<int>(MHD_queue_response(
185+
connection, mr->response->get_status(), raw_response));
186+
}
187+
const auto& p = dch->get_params();
188+
auto args = map_to_mhd_digest_args_(p, digest_opaque_);
189+
return static_cast<int>(MHD_queue_auth_required_response3(
190+
connection,
191+
p.realm.c_str(),
192+
args.opaque_cstr,
193+
args.domain_cstr,
194+
raw_response,
195+
p.signal_stale ? MHD_YES : MHD_NO,
196+
args.qop,
197+
args.algo,
198+
p.userhash_support ? MHD_YES : MHD_NO,
199+
p.prefer_utf8 ? MHD_YES : MHD_NO));
200+
}
201+
#endif // HAVE_DAUTH
202+
return static_cast<int>(MHD_queue_response(
203+
connection, mr->response->get_status(), raw_response));
204+
}
205+
206+
MHD_Result response_materializer::materialize_and_queue_response(
207+
MHD_Connection* connection,
208+
detail::modded_request* mr,
209+
http_resource* resource) {
210+
struct MHD_Response* raw_response = get_raw_response_with_fallback(mr);
211+
if (raw_response == nullptr) {
212+
// Belt-and-suspenders: even get_raw_response_with_fallback's own
213+
// try/catch couldn't produce a response. Force the empty-body 500 so
214+
// MHD always has something to queue. Contract: log before the 500.
215+
log_dispatch_error(config_,
216+
"materialize_and_queue_response: "
217+
"get_raw_response_with_fallback returned null; "
218+
"forcing hardcoded empty-body 500");
219+
mr->response.emplace(
220+
errors_.internal_error_page(mr, "", /*force_our=*/true));
221+
raw_response = materialize_response(&*mr->response);
222+
if (raw_response == nullptr) {
223+
// Last-resort guard: internal_error_page's materialization also
224+
// returned null (e.g. under extreme memory pressure). Cannot call
225+
// decorate_mhd_response(nullptr, ...) -- that would pass a null
226+
// MHD_Response to MHD_add_response_header (CWE-476). Return MHD_NO
227+
// so the connection terminates gracefully rather than crashing.
228+
return (MHD_Result) MHD_NO;
229+
}
230+
}
231+
decorate_mhd_response(raw_response, *mr->response);
232+
int to_ret = queue_response_dispatching_kind(connection, mr, raw_response);
233+
// Fire response_sent AFTER MHD_queue_response (status/bytes reflect what
234+
// was queued) and BEFORE MHD_destroy_response (ctx.response backed by live
235+
// storage). MHD copies the response data during queue, so destroying the
236+
// MHD_Response below does not affect the queued bytes.
237+
hook_dispatch_.fire_response_sent_gated(mr, resource);
238+
// MHD reference-counting: for callback (deferred/streaming) responses MHD
239+
// increments its own refcount during queue, so this destroy only releases
240+
// the caller's reference. MHD keeps the streaming callback (and the cls
241+
// pointer into mr->response) alive until request_completed fires. The
242+
// modded_request (and mr->response) are destroyed only in the
243+
// request_completed callback, after MHD is done streaming.
244+
MHD_destroy_response(raw_response);
245+
return (MHD_Result) to_ret;
246+
}
247+
248+
} // namespace detail
249+
} // namespace httpserver

0 commit comments

Comments
 (0)