Skip to content

Commit 2172190

Browse files
etrclaude
andcommitted
TASK-064: structured cookie value type for v2 http_response/http_request
Replaces the v1 string-blob `http_response::with_cookie(name, value)` surface with a structured `httpserver::cookie` value type carrying `name`, `value`, `domain`, `path`, `expires`, `max_age`, `secure`, `http_only`, and `same_site` attributes. Each cookie renders to a single RFC 6265 §4.1-compliant `Set-Cookie` header on the wire via `cookie::to_set_cookie_header()`. A paired RFC 6265 §5.4 parser exposes `cookie::parse_cookie_header` so request `Cookie:` headers round-trip through the same code path. Why now: - The follow-up was explicitly deferred at the v1 `with_cookie(string, string)` site (`src/httpserver/http_response.hpp`). - The v1 path renders the value verbatim into `Set-Cookie`, so a `;` in the value silently injects attributes (attribute-injection / CWE-113). v2 enforces the RFC ABNF via setter validation and a single render source. - PRD-RSP-REQ-004 (fluent return) and PRD §2 API minimalism call for typed values over string blobs on the response surface. What's new (public surface): - `src/httpserver/cookie.hpp` (installed, umbrella-included): value type with fluent `&` / `&&` setter pairs mirroring `http_response`'s style; `enum class same_site_mode { unset, strict, lax, none }`; `to_set_cookie_header()` renderer; static `parse_cookie_header()` parser. Setters reject CR/LF/NUL / `;` / `=` / whitespace per RFC 6265 §4.1.1 ABNF. - `http_response::with_cookie(cookie)` lvalue + rvalue overloads; `http_response::get_cookies_parsed()` returning `const std::vector<cookie>&`. - `http_request::get_cookies_parsed()` returning `const std::vector<cookie>&`; lazily built from the request's `Cookie:` header via `parse_cookie_header`, cached on the per-request impl following the TASK-016/017 arena pattern. Wire rendering (the headline correctness fix): - `decorate_mhd_response` now reads from `resp.get_cookies_parsed()` and emits one `Set-Cookie` header per entry via `cookie::to_set_cookie_header()`. Attributes (Domain, Path, Expires, Max-Age, Secure, HttpOnly, SameSite) propagate to the wire. - The legacy `cookies_` name->value mirror map survives only to back the deprecated `get_cookie(name)` / `get_cookies()` accessors; it is no longer a render source. Deprecation / migration (transitional, one release): - `http_response::with_cookie(string, string)` and the legacy `get_cookies()` / `get_cookie(string_view)` accessors are `[[deprecated]]`. The legacy `with_cookie` forwards through the structured path so wire output is unchanged for unmodified callers. - v2.1 removes the deprecated string-blob path entirely. The deprecation message points callers at the structured overload. Tests (TDD, all passing in isolation): - `cookie_header_sentinel_test.cpp`: class shape, default state, enum, copy/move-constructibility. - `cookie_render_test.cpp`: 43 tests / 82 checks covering fluent-setter ref-qualifier shape, setter validation (CWE-113), full RFC 6265 §4.1 rendering (attribute ordering, Max-Age zero/negative, IMF-fixdate using the canonical §4.1 example 784111777, SameSite=None auto-Secure coercion, byte-transparency, empty-name throw), §5.4 parsing (DQUOTE stripping, malformed-skip, case-preserving, no percent-decode), and the AC round-trip pin. - `http_response_cookie_wire_test.cpp`: `with_cookie(cookie)` integration on http_response, get_cookies_parsed shape + lifetime, structured cookies survive move ctor, legacy/structured interop, no double-emit when mixing both paths. - `http_request_cookies_parsed_test.cpp`: shape, lifetime, cache stability across calls and across unrelated getters. - `cookie_deprecation_sentinel_test.cpp`: legacy path still compiles (under `#pragma GCC diagnostic ignored "-Wdeprecated-declarations"`), legacy return types unchanged. - Existing `http_response_test.cpp`, `http_response_sbo_test.cpp`, `http_response_move_sanitizer_test.cpp` wrap their legacy `with_cookie(string, string)` calls in a deprecation-warning suppression pragma; runtime behaviour unchanged. Architecture docs updated under `specs/architecture/04-components/` (http-response.md, http-request.md). RELEASE_NOTES.md updated with the new public surface, the deprecation timeline, and the wire-render shift. Acceptance: - `http_response::string("...").with_cookie(cookie{}.with_name("sid") .with_secure(true).with_same_site(same_site_mode::strict))` compiles and renders to `sid=; Secure; SameSite=Strict` on the wire. - `http_request::get_cookies_parsed()` returns `const std::vector<cookie>&`; second call O(1) and zero-allocating. - RFC 6265 §4.1 canonical example round-trips byte-exact. - Deprecated string-blob path still compiles with a `[[deprecated]]` warning; `request_with_cookie` integ test still passes end-to-end. Verification: - All 5 new cookie test programs PASS (sentinel + render + wire + parsed + deprecation): 73 tests / 138 checks total. - Unit-test suite for http_response/http_request all PASS in isolation. - Clean build (no compile warnings). - `make check` integ-test FAILures on this host are pre-existing CURL error-7 (couldn't-connect) flake from port exhaustion on rapid sequential daemon-start tests — unrelated to TASK-064; the cookie integration test `request_with_cookie` in basic.cpp passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5e0abd5 commit 2172190

24 files changed

Lines changed: 1828 additions & 15 deletions

RELEASE_NOTES.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ and see the v2 replacement.
231231
is no honoring path planned — `Content-Length` synthesis would lie
232232
when the pipe yields a different byte count, and libmicrohttpd's
233233
`MHD_create_response_from_pipe` takes no size.
234+
- **Structured cookie type (TASK-064).** The string-blob cookie API
235+
on `http_response` is now `[[deprecated]]` in favour of a typed
236+
`httpserver::cookie` value (new public header `<httpserver/cookie.hpp>`).
237+
Construct with fluent setters — `cookie{}.with_name(...).with_value(...)
238+
.with_domain(...).with_path(...).with_expires(epoch_seconds).with_max_age(s)
239+
.with_secure(true).with_http_only(true).with_same_site(same_site_mode::strict)`
240+
— then hand to `http_response::with_cookie(cookie)`. The dispatch path
241+
emits one RFC 6265 §4.1 well-formed `Set-Cookie` header per entry with
242+
a fixed attribute order (`name=value; Expires; Max-Age; Domain; Path;
243+
Secure; HttpOnly; SameSite`); `SameSite=None` auto-coerces `Secure` on
244+
the wire. The matching request-side accessor is `http_request::
245+
get_cookies_parsed()`, returning `const std::vector<httpserver::cookie>&`
246+
backed by a per-request lazy cache. Legacy `with_cookie(std::string,
247+
std::string)`, `get_cookie(...)`, and `get_cookies()` still compile but
248+
emit `[[deprecated]]`; they will be removed in v2.1. The new APIs reject
249+
CR/LF/NUL plus `;` in values (attribute-injection guard, CWE-113); the
250+
pre-TASK-064 wire footgun of `with_cookie("name", "v; Path=/admin")`
251+
silently emitting attributes is gone.
234252

235253
## Threading
236254

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- `get_path()`, `get_method()`, `get_version()`, `get_content()`, `get_querystring()` returning `string_view`
1010
- `get_headers()`, `get_footers()`, `get_cookies()`, `get_args()`, `get_path_pieces()`, `get_files()` returning `const ContainerType&`
1111
- `get_header(key)`, `get_cookie(key)`, `get_footer(key)`, `get_arg(key)`, `get_arg_flat(key)` returning `string_view` (empty on miss; never insert)
12+
- `get_cookies_parsed()` (TASK-064) returning `const std::vector<httpserver::cookie>&`: structured RFC 6265 §5.4 parse of the request's `Cookie:` header. Each entry carries `name` and `value` (request cookies have no attributes per the spec). Backed by a per-request lazy cache that follows the TASK-016/TASK-017 arena pattern: the first call parses and populates the vector; subsequent calls are O(1) and reuse the same buffer (`reference_stable_across_calls`, `second_call_does_not_reallocate` pinned by `http_request_cookies_parsed_test`).
1213
- `get_user()`, `get_pass()`, `get_digested_user()` returning `string_view` (empty when basic/digest auth disabled at build)
1314
- `has_tls_session()`, `has_client_certificate()`, `get_client_cert_dn()`, `get_client_cert_issuer_dn()`, `get_client_cert_cn()`, `get_client_cert_fingerprint_sha256()`, `is_client_cert_verified()`, `get_client_cert_not_before()`, `get_client_cert_not_after()` (all returning sentinels when GnuTLS disabled)
1415
- `check_digest_auth(...)` family

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ The body subclasses (`detail::string_body`, `file_body`, `iovec_body`, `pipe_bod
2424
- Fluent setters: `with_header`, `with_footer`, `with_cookie`, `with_status` — each has two ref-qualified overloads: `& → http_response&` (mutate-in-place on an lvalue) and `&& → http_response&&` (return the object by rvalue-reference for zero-copy rvalue factory chains, e.g. `http_response::string("body").with_header("X-Foo", "bar").with_status(201)`).
2525
- `const` accessors: `get_header`, `get_footer`, `get_cookie` returning `string_view` (empty on miss; do not insert).
2626
- `get_headers`, `get_footers`, `get_cookies` returning `const map&`.
27+
28+
**Cookie surface (TASK-064):** the v2.0 cookie API is structured. A new public header `<httpserver/cookie.hpp>` declares `httpserver::cookie` (a copyable + movable value type) with fluent `with_name`, `with_value`, `with_domain`, `with_path`, `with_expires`, `with_max_age`, `with_secure`, `with_http_only`, `with_same_site` setters, plus an `enum class same_site_mode { unset, strict, lax, none }`. `http_response::with_cookie(cookie)` appends to a `std::vector<cookie>` carried directly on the response (separate field from the legacy `cookies_` map). The dispatch path (`detail/webserver_request.cpp::decorate_mhd_response`) renders one `Set-Cookie` header per entry via `cookie::to_set_cookie_header()`, which produces an RFC 6265 §4.1 well-formed serialization with fixed attribute ordering (`name=value; Expires=...; Max-Age=...; Domain=...; Path=...; Secure; HttpOnly; SameSite=...`) and auto-coerces `Secure` when `SameSite=None` is set. `cookie::parse_cookie_header(string_view)` is the matching RFC 6265 §5.4 request-side parser (byte-transparent, skips entries without `=`, strips outer DQUOTE pairs). The legacy `with_cookie(string, string)`, `get_cookie(...)`, and `get_cookies()` accessors are `[[deprecated]]` and will be removed in v2.1; they keep working through a thin shim that forwards through the structured path and mirrors name/value into the legacy `cookies_` map for source-compatibility with v1 callers.
2729
- `kind()` returning `body_kind`.
2830
- The virtuals `get_raw_response`, `decorate_response`, `enqueue_response` are removed from the public API (PRD-HDR-REQ-005). The MHD response object is constructed inside the library's dispatch path from the `http_response` value's `body_->materialize()` (or equivalent internal API on `detail::body`).
2931

specs/tasks/M7-v2-cleanup/TASK-064.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ Replace the string-blob cookie surface on `http_response` with a structured `htt
2929
**Related Requirements:** PRD-RSP-REQ-004 (fluent return), PRD §2 API minimalism
3030
**Related Decisions:** None new (RFC 6265)
3131

32-
**Status:** Backlog
32+
**Status:** Completed

src/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ 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 http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/http_request_impl.cpp detail/http_request_impl_tls.cpp detail/webserver_setup.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_finalize.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.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/http_request_impl.cpp detail/http_request_impl_tls.cpp detail/webserver_setup.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_finalize.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>.
3232
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/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/radix_tree.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp gettext.h
33-
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/webserver_routes.hpp httpserver/webserver_websocket.hpp httpserver/webserver_hooks.hpp httpserver/websocket_handler.hpp httpserver/http_utils.hpp httpserver/ip_representation.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_request_auth.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
33+
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/cookie.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/webserver_routes.hpp httpserver/webserver_websocket.hpp httpserver/webserver_hooks.hpp httpserver/websocket_handler.hpp httpserver/http_utils.hpp httpserver/ip_representation.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_request_auth.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
3636

0 commit comments

Comments
 (0)