Skip to content

Commit 014ad36

Browse files
committed
Merge TASK-036 minors (13/43 marked; 30 still need review)
2 parents 76d130e + 1cc7883 commit 014ad36

16 files changed

Lines changed: 141 additions & 49 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*.sw*
22
*.*~
3+
*~
34
*.in
45
*.php
56
*.pm

specs/architecture/04-components/http-resource.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
**Responsibility:** Stateful handler base for cases where state is shared across HTTP methods of one resource (counter, cache, DB handle, auth context).
44

5-
**Implementation:** Public abstract base. Subclasses override one of `render_get / render_post / render_put / render_delete / render_patch / render_options / render_head` (renamed from v1's `render_GET` etc., to comply with PRD-NAM-REQ-001 snake_case). The default `render(...)` falls back when the method-specific override is not provided.
5+
**Implementation:** Public abstract base. Subclasses override one of `render_get / render_post / render_put / render_delete / render_patch / render_options / render_head` (renamed from v1's `render_GET` etc., to comply with PRD-NAM-REQ-001 snake_case). Each `render_*` override returns `http_response` **by value** (DR-004 / PRD-RSP-REQ-007); the webserver moves the returned value into the per-connection `modded_request::response` anchor. The default `render(...)` falls back when the method-specific override is not provided; it returns a default-constructed `http_response` whose `status_code_ == -1` sentinel routes through `internal_error_page`.
66

77
The allow-mask (formerly `std::map<std::string, bool> method_state`) becomes `method_set methods_allowed_;` — a `uint32_t` bitmask wrapper (DR-6). `is_allowed(http_method)` and `get_allowed_methods()` are `const` and return without allocation.
88

99
**Lifetime:** owned by the `webserver` via `unique_ptr` or `shared_ptr` (PRD-HDL-REQ-003). Raw-pointer registration is gone (PRD-HDL-REQ-005).
1010

11-
**Related requirements:** PRD-HDL-REQ-003, PRD-HDL-REQ-005, PRD-REQ-REQ-002, PRD-REQ-REQ-003.
11+
**Related requirements:** DR-004, PRD-HDL-REQ-003, PRD-HDL-REQ-005, PRD-REQ-REQ-002, PRD-REQ-REQ-003, PRD-RSP-REQ-007.
1212

1313
---

specs/unworked_review_issues/2026-05-18_142351_task-036.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,20 @@ then use entry (or std::move(entry) for the regex push_back). This removes ~6 du
7373

7474
## Minor
7575

76-
13. [ ] **architecture-alignment-checker** | `src/httpserver/create_webserver.hpp:67` | adr-violation
76+
13. [x] **architecture-alignment-checker** | `src/httpserver/create_webserver.hpp:67` | adr-violation
7777
auth_handler_ptr is still typed as std::function<std::shared_ptr<http_response>(const http_request&)> — a v1 shared_ptr return pattern — while DR-004 mandates http_response by value for all handler signatures. The dispatch site in webserver.cpp (line 2227-2229) bridges the gap with a manual std::move(*auth_response) into mr->response_, with a comment explicitly marking this as out of scope for TASK-036.
7878
*Recommendation:* Track in a follow-up task: change auth_handler_ptr to std::function<std::optional<http_response>(const http_request&)> (optional because null means 'pass through') and remove the bridging code. This would complete the DR-004 rollout to the auth hook.
79+
*Status:* already addressed — TODO comment added on auth_handler_ptr in create_webserver.hpp (commit a11e5ee). The deferred migration is tracked.
7980

80-
14. [ ] **architecture-alignment-checker** | `src/httpserver/detail/modded_request.hpp:51` | pattern-violation
81+
14. [x] **architecture-alignment-checker** | `src/httpserver/detail/modded_request.hpp:51` | pattern-violation
8182
The callback pointer-to-member (http_response (http_resource::*callback)(...)) has no default member initializer. If an unrecognized HTTP method is received and a resource is found, mr->callback is indeterminate. The code comment at webserver.cpp:2382 acknowledges this as a pre-existing latent bug from TASK-027, and the 405 guard (is_allowed(count_) == false) prevents the indeterminate pointer from being dereferenced in practice. However, TASK-036 introduced the member with its current return-by-value signature, making it the right time to add a null initializer or a static_assert.
8283
*Recommendation:* Add a default member initializer: http_response (httpserver::http_resource::*callback)(const httpserver::http_request&) = nullptr; and guard the dispatch call site with an assert or explicit null check to eliminate the latent UB.
84+
*Status:* already addressed — `= nullptr` default initializer added to `callback` field (commit 22d8f07). Comment in resolve_method_callback documents the nullptr-for-unrecognized-method invariant.
8385

84-
15. [ ] **architecture-alignment-checker** | `src/webserver.cpp:2307` | interface-contract
86+
15. [x] **architecture-alignment-checker** | `src/webserver.cpp:2307` | interface-contract
8587
For a deferred (streaming) response, finalize_answer calls MHD_destroy_response(raw_response) immediately after MHD_queue_response. The deferred_body trampoline passes this (deferred_body*) as the cls pointer to MHD_create_response_from_callback. The body is safe because MHD's own reference counting keeps the MHD_Response alive until streaming completes, and request_completed (which destroys the modded_request and its embedded response_) only fires after MHD is done. However the code comment at the call site does not document this MHD ref-counting assumption, leaving a subtle lifetime dependency undocumented.
8688
*Recommendation:* Add a comment near MHD_destroy_response in finalize_answer explaining that for MHD_create_response_from_callback responses, MHD increments its own refcount during MHD_queue_response, so the immediate MHD_destroy_response only releases the caller's reference — the trampoline's cls pointer (deferred_body*) remains valid until request_completed fires.
89+
*Status:* fixed in this batch (commit 72614c0 — added MHD refcounting comment near MHD_destroy_response in materialize_and_queue_response in webserver_request.cpp).
8790

8891
16. [ ] **code-quality-reviewer** | `src/Makefile.am:25` | code-elegance
8992
Removal of http_resource.cpp from sources is correct and clean. No issue, noted for completeness that the build system correctly reflects the header-only migration.

src/detail/webserver_aliases.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ void webserver::install_default_alias_hooks_() {
178178
// webserver_impl::apply_auth_short_circuit inline path: it respects
179179
// auth_skip_paths, calls the user-supplied auth_handler, and
180180
// short-circuits with the returned response when auth fails.
181+
//
182+
// Design note (security-reviewer-iter1-7 / CWE-200): auth_handler
183+
// is registered as a before_handler hook, which fires only for
184+
// matched routes (found==true). Requests to unregistered paths
185+
// receive a 404 without consulting auth_handler. This means 401 vs
186+
// 404 distinguishes registered from unregistered routes (auth oracle).
187+
// Callers who need uniform authentication on all requests — including
188+
// 404s — should add a catch-all fallback route or register a
189+
// not_found_handler that applies equivalent auth logic.
181190
// ----------------------------------------------------------------
182191
if (auth_handler != nullptr) {
183192
// Capture both the webserver* (for auth_handler callable) and the

src/detail/webserver_callbacks.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ void webserver_impl::error_log(void* cls, const char* fmt, va_list ap) {
309309
webserver* dws = static_cast<webserver*>(cls);
310310

311311
std::string msg;
312-
msg.resize(80); // Asssume one line will be enought most of the time.
312+
msg.resize(80); // Assume one line will be enough most of the time.
313313

314314
va_list va;
315315
va_copy(va, ap); // Stash a copy in case we need to try again.

src/detail/webserver_error_pages.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ void webserver_impl::log_dispatch_error(std::string_view msg) const {
9898
if (parent->log_error == nullptr) {
9999
return;
100100
}
101+
// Framework contract (CWE-532): msg may contain e.what() text from
102+
// a handler exception, which could include sensitive data (DB connection
103+
// strings, file paths, user-supplied input that triggered the exception).
104+
// Application code should sanitize or wrap exceptions that might expose
105+
// sensitive information before re-throwing them.
106+
//
101107
// A misbehaving user logger must not poison the catch from inside
102108
// the catch. Swallow any exception it throws; we have no recovery
103109
// beyond dropping the log line.

src/detail/webserver_request.cpp

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,41 +194,44 @@ struct MHD_Response* webserver_impl::get_raw_response_with_fallback(detail::modd
194194
// emplace(std::move(...)); the optional owns the value and the
195195
// deferred-body trampoline keeps a pointer into it for the lifetime
196196
// of the modded_request.
197-
auto materialize_current = [&]() -> struct MHD_Response* {
197+
auto try_materialize = [&]() -> struct MHD_Response* {
198198
return materialize_response(mr->response ? &*mr->response : nullptr);
199199
};
200+
// Helper: emplace a new response and immediately materialize it.
201+
// Each catch arm sets mr->response then returns the raw MHD handle.
202+
auto emplace_and_materialize = [&](http_response r) -> struct MHD_Response* {
203+
mr->response.emplace(std::move(r));
204+
return try_materialize();
205+
};
200206
try {
201-
struct MHD_Response* raw = materialize_current();
207+
struct MHD_Response* raw = try_materialize();
202208
if (raw == nullptr) {
203209
// TASK-031: no exception was thrown, but the body materializer
204210
// returned null. Route through the safe internal-error path so
205211
// a misbehaving user handler can't escape.
206-
mr->response.emplace(run_internal_error_handler_safely(
212+
return emplace_and_materialize(run_internal_error_handler_safely(
207213
mr, "materialize_response returned null"));
208-
raw = materialize_current();
209214
}
210215
return raw;
211216
} catch(const std::invalid_argument&) {
212217
try {
213-
mr->response.emplace(not_found_page(mr));
214-
return materialize_current();
218+
return emplace_and_materialize(not_found_page(mr));
215219
} catch(...) {
216220
return nullptr;
217221
}
218222
} catch(const std::exception& e) {
219223
log_dispatch_error(std::string("materialize threw: ") + e.what());
220224
try {
221-
mr->response.emplace(run_internal_error_handler_safely(mr, e.what()));
222-
return materialize_current();
225+
return emplace_and_materialize(
226+
run_internal_error_handler_safely(mr, e.what()));
223227
} catch(...) {
224228
return nullptr;
225229
}
226230
} catch(...) {
227231
log_dispatch_error("materialize threw unknown exception");
228232
try {
229-
mr->response.emplace(run_internal_error_handler_safely(mr,
230-
"unknown exception"));
231-
return materialize_current();
233+
return emplace_and_materialize(run_internal_error_handler_safely(
234+
mr, "unknown exception"));
232235
} catch(...) {
233236
return nullptr;
234237
}
@@ -260,6 +263,14 @@ MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connec
260263
// queued bytes -- only the http_response in mr->response matters
261264
// for the hook ctx pointer.
262265
fire_response_sent_gated(mr);
266+
// MHD reference-counting note: for MHD_create_response_from_callback
267+
// responses (deferred/streaming), MHD increments its own internal
268+
// refcount during MHD_queue_response, so this MHD_destroy_response only
269+
// releases the *caller's* reference. MHD keeps the streaming callback
270+
// (deferred_body::trampoline) alive — and therefore the cls pointer into
271+
// mr->response — until request_completed fires and MHD releases its own
272+
// reference. The modded_request (and mr->response) are destroyed only in
273+
// the request_completed callback, which is after MHD is done streaming.
263274
MHD_destroy_response(raw_response);
264275
return (MHD_Result) to_ret;
265276
}

src/detail/webserver_routes.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ void webserver_impl::commit_handlers_to_shim(detail::lambda_resource& shim,
170170
method_set methods,
171171
const std::function<::httpserver::http_response(
172172
const ::httpserver::http_request&)>& handler) {
173-
// The shared std::function copies cheaply (type-erased callable),
174-
// so each slot owns its own copy.
173+
// Each slot gets its own copy of the std::function. For small captures
174+
// (within SBO, ~16-32 bytes on libstdc++/libc++) the copies are cheap.
175+
// For large-capture handlers registered across many methods, each copy
176+
// allocates once. Registration-time only; not a hot-path concern.
177+
// (performance-reviewer-iter1-3: document the per-slot copy behaviour.)
175178
for_each_requested_method(methods, [&](http_method m) {
176179
shim.set_slot(m, handler);
177180
});

src/detail/webserver_websocket.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,15 @@ webserver_impl::complete_websocket_upgrade(MHD_Connection* connection,
212212
std::shared_ptr<websocket_handler> handler_sp = ws_it->second;
213213
lock.unlock();
214214

215-
auto* data = new ws_upgrade_data{this, std::move(handler_sp)};
215+
// CWE-401: RAII guard so data is freed if MHD_create_response_for_upgrade
216+
// returns null. Ownership is transferred to MHD (via release()) only after
217+
// the queue call — upgrade_handler receives data_guard.get() as cls and
218+
// wraps it in unique_ptr for cleanup (see upgrade_handler below).
219+
std::unique_ptr<ws_upgrade_data> data_guard(
220+
new ws_upgrade_data{this, std::move(handler_sp)});
216221
struct MHD_Response* response = MHD_create_response_for_upgrade(
217-
&webserver_impl::upgrade_handler, data);
222+
&webserver_impl::upgrade_handler, data_guard.get());
218223
if (response == nullptr) {
219-
delete data;
220224
return std::nullopt;
221225
}
222226
MHD_add_response_header(response, MHD_HTTP_HEADER_UPGRADE, "websocket");
@@ -231,6 +235,7 @@ webserver_impl::complete_websocket_upgrade(MHD_Connection* connection,
231235
MHD_HTTP_SWITCHING_PROTOCOLS,
232236
response);
233237
MHD_destroy_response(response);
238+
data_guard.release(); // MHD now owns; upgrade_handler will delete.
234239
return to_ret;
235240
}
236241

src/httpserver/detail/lambda_resource.hpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ namespace detail {
6464

6565
// Tiny adapter that wraps a single lambda_handler as an http_resource
6666
// virtual override. We keep one slot per method enum and dispatch in
67-
// each render_* override.
67+
// each render_* override. Each slot holds a
68+
// std::function<http_response(const http_request&)>
69+
// (see route_entry.hpp::lambda_handler). Slots are installed via
70+
// set_slot() and invoked by the render_* overrides below.
6871
class lambda_resource final : public ::httpserver::http_resource {
6972
public:
7073
lambda_resource() {
@@ -90,6 +93,9 @@ class lambda_resource final : public ::httpserver::http_resource {
9093
// Each differs only by the http_method enum constant forwarded.
9194
// They are required by the http_resource base-class interface and
9295
// cannot be collapsed further without changing that interface.
96+
// render_connect and render_trace intentionally fall back to the
97+
// base-class default (no slot wired, no set_allowing call); they
98+
// return the -1 sentinel and route through internal_error_page.
9399
::httpserver::http_response
94100
render_get(const ::httpserver::http_request& r) override {
95101
return invoke_(http_method::get, r);
@@ -120,6 +126,9 @@ class lambda_resource final : public ::httpserver::http_resource {
120126
}
121127

122128
private:
129+
// Dispatch to the registered slot for method `m`, returning the
130+
// handler's http_response by value. Callers must ensure is_allowed(m)
131+
// is true before calling (the 405 guard in finalize_answer enforces this).
123132
::httpserver::http_response
124133
invoke_(http_method m, const ::httpserver::http_request& r) {
125134
auto& slot = slots_[static_cast<std::size_t>(m)];

0 commit comments

Comments
 (0)