Skip to content

Commit b278f8a

Browse files
etrclaude
andcommitted
TASK-050: housekeeping — mark Done, persist review notes (0 critical, 0 major, 48 minor)
Apply selected post-review minor fixes: - CWE-117: sanitize control chars in log_access alias path/method (and in examples/clf_access_log.cpp) to prevent log-line injection. - Perf: skip steady_clock::now() in fire_response_sent_gated when only the log_access alias slot fires (alias does not read elapsed). - Doxygen block on create_webserver::log_access() pointing users to add_hook(response_sent, ...) for structured ctx. - Architecture doc: update hooks.md firing-site references to the new webserver_request.cpp / webserver_callbacks.cpp split. - Test wiring: add -lmicrohttpd to hooks_log_access_alias_slot LDADD. Persist full reviewer findings under specs/unworked_review_issues/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bed6ad3 commit b278f8a

10 files changed

Lines changed: 342 additions & 15 deletions

File tree

examples/clf_access_log.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include <functional>
5353
#include <memory>
5454
#include <string>
55+
#include <string_view>
5556

5657
#include <httpserver.hpp>
5758

@@ -66,6 +67,19 @@ class hello_resource : public hs::http_resource {
6667
}
6768
};
6869

70+
// SECURITY (CWE-117): sanitize a string_view for CLF output.
71+
// Replace ASCII control characters (< 0x20 or == 0x7F) with '-' to
72+
// prevent a client from injecting additional log lines by embedding
73+
// newlines or carriage-returns in the request path or method.
74+
std::string sanitize_clf(std::string_view sv) {
75+
std::string out;
76+
out.reserve(sv.size());
77+
for (unsigned char c : sv) {
78+
out += (c < 0x20 || c == 0x7f) ? '-' : static_cast<char>(c);
79+
}
80+
return out;
81+
}
82+
6983
void emit_clf_line(const hs::response_sent_ctx& ctx) {
7084
std::time_t now = std::time(nullptr);
7185
char ts[32];
@@ -81,8 +95,9 @@ void emit_clf_line(const hs::response_sent_ctx& ctx) {
8195
ctx.elapsed).count();
8296
std::string method, path;
8397
if (ctx.request != nullptr) {
84-
method = std::string(ctx.request->get_method());
85-
path = std::string(ctx.request->get_path());
98+
// Sanitize before embedding in the CLF line to prevent log injection.
99+
method = sanitize_clf(ctx.request->get_method());
100+
path = sanitize_clf(ctx.request->get_path());
86101
}
87102
std::printf("- - - [%s] \"%s %s HTTP/1.1\" %d %zu %lld\n",
88103
ts,

specs/architecture/04-components/hooks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
| `before_handler` | `webserver.cpp:dispatch_resource_handler` — start | **yes** | yes |
1515
| `handler_exception` | `webserver.cpp:dispatch_resource_handler` — each catch arm | **yes** (maps exception to response) | yes |
1616
| `after_handler` | between handler return and `materialize_and_queue_response` | **yes** (replaces response) | yes |
17-
| `response_sent` | `webserver.cpp:materialize_and_queue_response` — post `MHD_queue_response` | no | yes |
18-
| `request_completed` | `webserver.cpp:request_completed` — NOTIFY_COMPLETED | no | yes |
17+
| `response_sent` | `webserver_request.cpp:materialize_and_queue_response` — post `MHD_queue_response` | no | yes |
18+
| `request_completed` | `webserver_callbacks.cpp:request_completed` — NOTIFY_COMPLETED | no | yes |
1919
| `connection_closed` | `webserver.cpp:connection_notify` — NOTIFY_CLOSED | no | no |
2020

2121
**Implementation.** Each phase has its own `std::vector<std::function<...>>` in `webserver_impl`, guarded by a single `std::shared_mutex hook_table_mutex_`. A per-phase `std::atomic<bool> any_hooks_[hook_phase::count_]` flag short-circuits the dispatch site to a relaxed atomic load and a compare-with-zero when no subscribers exist — the only hook-related cost on the hot request path for a server with zero hooks registered.

specs/tasks/M5-routing-lifecycle/TASK-050.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
Wire the three tail-end phases. `after_handler` is the post-handler short-circuit (it can REPLACE the in-flight response). `response_sent` is the observation point that finally has the response, status, byte-count, and timing — the data #281 and #69 have been asking for. `request_completed` is the unconditional final hook. Convert `log_access` into the documented alias.
99

1010
**Action Items:**
11-
- [ ] Fire `after_handler` between `dispatch_resource_handler` and `materialize_and_queue_response` in `webserver_impl::finalize_answer` (`webserver.cpp:2444``webserver.cpp:2449`). Context: `after_handler_ctx { const http_request& req; http_response& resp; }` — mutable response reference so a hook can `resp.with_header(...)` in place. Short-circuit semantics: a hook returning `hook_action::respond_with(r2)` REPLACES `mr->response_` with `r2` and skips remaining hooks at the phase. Subsequent hooks at later phases (`response_sent`, `request_completed`) still fire on the replaced response.
12-
- [ ] Fire `response_sent` immediately after the `MHD_queue_response` call in `webserver_impl::materialize_and_queue_response` (`webserver.cpp:2421`), BEFORE `MHD_destroy_response`. Context: `response_sent_ctx { const http_request& req; const http_response& resp; int status; std::size_t bytes_queued; std::chrono::nanoseconds elapsed; }`. `elapsed` is measured from `answer_to_connection`'s first invocation for this `mr` (timestamp captured in `modded_request` at TASK-045 time).
13-
- [ ] Fire `request_completed` from `webserver_impl::request_completed` (`webserver.cpp:1253`), BEFORE the `delete static_cast<detail::modded_request*>(*con_cls)` step (otherwise the context fields would dangle). Context: `request_completed_ctx { const http_request& req; const http_response* resp; bool succeeded; }`. `resp` is nullable — on early failures (`accept_decision` rejection, etc.) there may be no response object.
14-
- [ ] **Alias: `log_access(fn)`.** Internally register a `response_sent` hook that formats the request line as today and invokes `fn(line)`. Re-registration replaces. Doxygen notes it as an alias and recommends `add_hook(hook_phase::response_sent, ...)` for users who want the structured context (status, bytes, timing — what #281 and #69 actually asked for).
15-
- [ ] `examples/clf_access_log.cpp`: a Common-Log-Format access logger as a `response_sent` hook, demonstrating that the long-requested CLF / `time-taken` format is now writable in user code without a library change. Closes issues #281 and #69 from the user-facing-doc perspective.
11+
- [x] Fire `after_handler` between `dispatch_resource_handler` and `materialize_and_queue_response` in `webserver_impl::finalize_answer` (implemented in `src/detail/webserver_finalize.cpp:fire_after_handler_gated`, called from `src/detail/webserver_request.cpp:finalize_answer`). Context: `after_handler_ctx { const http_request& req; http_response& resp; }` — mutable response reference so a hook can `resp.with_header(...)` in place. Short-circuit semantics: a hook returning `hook_action::respond_with(r2)` REPLACES `mr->response_` with `r2` and skips remaining hooks at the phase. Subsequent hooks at later phases (`response_sent`, `request_completed`) still fire on the replaced response.
12+
- [x] Fire `response_sent` immediately after the `MHD_queue_response` call in `webserver_impl::materialize_and_queue_response` (`src/detail/webserver_request.cpp:materialize_and_queue_response`), BEFORE `MHD_destroy_response`. Context: `response_sent_ctx { const http_request& req; const http_response& resp; int status; std::size_t bytes_queued; std::chrono::nanoseconds elapsed; }`. `elapsed` is measured from `answer_to_connection`'s first invocation for this `mr` (timestamp captured in `modded_request` at TASK-045 time).
13+
- [x] Fire `request_completed` from `webserver_impl::request_completed` (`src/detail/webserver_callbacks.cpp`), BEFORE the `delete static_cast<detail::modded_request*>(*con_cls)` step (otherwise the context fields would dangle). Context: `request_completed_ctx { const http_request& req; const http_response* resp; bool succeeded; }`. `resp` is nullable — on early failures (`accept_decision` rejection, etc.) there may be no response object.
14+
- [x] **Alias: `log_access(fn)`.** Internally registers a `response_sent` hook via dedicated alias slot (`webserver_impl::log_access_alias_`) that formats the request line and invokes `fn(line)`. Re-registration replaces. Control characters are sanitized (CWE-117). Doxygen block added to `create_webserver::log_access()` noting it as an alias and recommending `add_hook(hook_phase::response_sent, ...)` for structured context (status, bytes, timing — what #281 and #69 actually asked for).
15+
- [x] `examples/clf_access_log.cpp`: a Common-Log-Format access logger as a `response_sent` hook, demonstrating that the long-requested CLF / `time-taken` format is now writable in user code without a library change. Closes issues #281 and #69 from the user-facing-doc perspective.
1616

1717
**Dependencies:**
1818
- Blocked by: TASK-045
@@ -32,4 +32,4 @@ Wire the three tail-end phases. `after_handler` is the post-handler short-circui
3232
**Related Decisions:** DR-012, §4.10
3333
**Resolves:** Issues #281, #69
3434

35-
**Status:** Not Started
35+
**Status:** Done

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
132132
| TASK-047 | Fire `request_received` and `body_chunk` (pre-handler short-circuit) | M5 | Done | TASK-045 |
133133
| TASK-048 | Fire `route_resolved` and `before_handler`; wire 404/405/auth aliases | M5 | Done | TASK-045, TASK-027, TASK-031 |
134134
| TASK-049 | Fire `handler_exception`; wire `internal_error_handler` alias | M5 | Done | TASK-045, TASK-031 |
135-
| TASK-050 | Fire `after_handler` (post-handler short-circuit), `response_sent`, `request_completed`; wire `log_access` alias | M5 | Not Started | TASK-045 |
135+
| TASK-050 | Fire `after_handler` (post-handler short-circuit), `response_sent`, `request_completed`; wire `log_access` alias | M5 | Done | TASK-045 |
136136
| TASK-051 | Per-route hooks (`http_resource::add_hook`) | M5 | Not Started | TASK-045, TASK-048, TASK-049, TASK-050 |
137137
| TASK-052 | Hook bus documentation, examples, benchmark, stress-test extension (touches back into TASK-040/041/042/043) | M5 | Not Started | TASK-045, TASK-046, TASK-047, TASK-048, TASK-049, TASK-050, TASK-051 |
138138
| TASK-037 | CI test for build-flag invariance | M6 | Done | TASK-034 |

0 commit comments

Comments
 (0)