Skip to content

Commit e530ae3

Browse files
etrclaude
andcommitted
Merge TASK-064: structured cookie API on v2 http_response/http_request
Replaces the string-blob cookie surface with a structured `http::cookie` type per RFC 6265, including render-time guards on name/value, parser hardening, and accompanying tests. Marks TASK-064 Completed and records remaining review findings under specs/unworked_review_issues. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2 parents 5e0abd5 + cd2e2de commit e530ae3

26 files changed

Lines changed: 2397 additions & 20 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/product_specs.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,47 @@ The response hierarchy has eight subclasses (`string_response`, `file_response`,
184184

185185
---
186186

187+
### 3.5.1 Structured Cookie Type (API-CKY)
188+
189+
**Problem / outcome**
190+
The v1 `with_cookie(string, string)` / `get_cookie(string)` API stored cookies as opaque string blobs, making RFC 6265 attribute fields (Secure, HttpOnly, SameSite, Path, Domain, Expires, Max-Age) inaccessible to callers and allowing header-injection via unguarded semicolons in values. After TASK-064 the library exposes `httpserver::cookie`, a structured value type, alongside `http_response::with_cookie(cookie)` for responses and `http_request::get_cookies_parsed()` for requests. The string-blob path is retained as a `[[deprecated]]` shim for one transitional v2.0 release.
191+
192+
**In scope**
193+
- `httpserver::cookie` 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.
194+
- `enum class same_site_mode { unset, strict, lax, none }`.
195+
- `cookie::to_set_cookie_header()` renders a fully RFC 6265 §4.1 conformant `Set-Cookie` value.
196+
- `cookie::parse_cookie_header(string_view)` parses a `Cookie:` request header into a `std::vector<cookie>`, byte-transparent and lenient.
197+
- `http_response::with_cookie(cookie)` structured overload; legacy `with_cookie(string, string)` marked `[[deprecated]]`.
198+
- `http_request::get_cookies_parsed()` returns `const std::vector<cookie>&`, parsed once and cached.
199+
- Injection guard: CR, LF, NUL, and `;` are rejected in name, value, domain, and path at setter time (CWE-113).
200+
- SameSite=None auto-coerces `Secure` at render time (browser requirement per draft-west-cookie-incrementalism).
201+
- Transitional policy: legacy `get_cookie(string)`, `get_cookies()`, and `with_cookie(string, string)` are `[[deprecated]]` and compile with a diagnostic in v2.0.
202+
203+
**Out of scope**
204+
- Removing the deprecated string-blob path before v2.1.
205+
- Domain-format hostname validation beyond semicolon rejection.
206+
207+
**EARS Requirements**
208+
- `PRD-CKY-REQ-001` When a user constructs a `httpserver::cookie` then the system shall provide fluent `with_*` setters that return `cookie&` on lvalue and `cookie&&` on rvalue, mirroring the `http_response` fluent style.
209+
- `PRD-CKY-REQ-002` When a user calls `with_name`, `with_value`, `with_domain`, or `with_path` with a value containing CR, LF, NUL, or `;` then the system shall throw `std::invalid_argument`.
210+
- `PRD-CKY-REQ-003` When a user calls `with_name` with a value containing `=` or ASCII whitespace then the system shall throw `std::invalid_argument`.
211+
- `PRD-CKY-REQ-004` When a user calls `cookie::to_set_cookie_header()` then the system shall produce a string conforming to RFC 6265 §4.1 with attributes in the canonical order: Expires, Max-Age, Domain, Path, Secure, HttpOnly, SameSite.
212+
- `PRD-CKY-REQ-005` When a user calls `cookie::to_set_cookie_header()` with `same_site_mode::none` set and `with_secure(false)` then the system shall include `Secure` in the output (browser requirement).
213+
- `PRD-CKY-REQ-006` When a user calls `cookie::parse_cookie_header(s)` then the system shall return a `std::vector<cookie>` parsed from the `Cookie:` wire format, stripping outer DQUOTE pairs from values and tolerating arbitrary ASCII whitespace around tokens.
214+
- `PRD-CKY-REQ-007` When a user calls `http_response::with_cookie(cookie)` then the system shall append the structured cookie to the response's cookie list; `get_cookies_parsed()` shall reflect it without copying the cookie object.
215+
- `PRD-CKY-REQ-008` When a user calls `http_request::get_cookies_parsed()` then the system shall return a `const std::vector<cookie>&` backed by a parse-once cached representation; subsequent calls shall not allocate.
216+
- `PRD-CKY-REQ-009` When v2.0 ships then `http_response::with_cookie(string, string)`, `http_response::get_cookie(string_view)`, and `http_response::get_cookies()` shall be `[[deprecated]]` and shall not be removed until v2.1.
217+
- `PRD-CKY-REQ-010` When a user calls the legacy `with_cookie(string, string)` shim with a name or value that violates RFC 6265 cookie-name or cookie-value rules then the system shall throw `std::invalid_argument` before mutating any internal state.
218+
219+
**Acceptance criteria**
220+
- `http_response::string("body").with_cookie(cookie{}.with_name("sid").with_secure(true).with_same_site(same_site_mode::strict))` compiles and produces a single well-formed `Set-Cookie` value.
221+
- `http_request::get_cookies_parsed()` returns `const std::vector<cookie>&`; pointer identity is stable across unrelated mutations.
222+
- RFC 6265 round-trip examples pass (`cookie_render` test suite, cycles 2–6).
223+
- `cookie{}.with_path("/; Secure")` throws `std::invalid_argument`.
224+
- Deprecated string-blob path compiles with `-Wdeprecated-declarations` diagnostic.
225+
226+
---
227+
187228
### 3.6 Request Type Ergonomics (API-REQ)
188229

189230
**Problem / outcome**

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
Replace the string-blob cookie surface on `http_response` with a structured `httpserver::cookie` value type carrying `name`, `value`, `domain`, `path`, `expires`, `max_age`, `secure`, `http_only`, `same_site`. The follow-up was explicitly deferred at `src/httpserver/http_response.hpp:304-313`.
99

1010
**Action Items:**
11-
- [ ] Design the `httpserver::cookie` value type in a new public header `src/httpserver/cookie.hpp`. Default-construct empty; provide fluent `with_*` setters mirroring `http_response`'s style. Include enum `same_site_mode { unset, strict, lax, none }`.
12-
- [ ] Add `http_response::with_cookie(cookie)` and `http_response::with_cookie(std::string name, std::string value)` overloads. Internally render to the `Set-Cookie` header per RFC 6265 §4.1.
13-
- [ ] Provide `http_request::get_cookies()` returning a structured view (parsed once, cached on the request impl per TASK-016 arena pattern).
14-
- [ ] Document migration: legacy string-blob path remains as a `[[deprecated]]` thin shim for one transitional release.
15-
- [ ] Add a unit test pinning round-trip parsing/rendering against RFC 6265 examples.
11+
- [x] Design the `httpserver::cookie` value type in a new public header `src/httpserver/cookie.hpp`. Default-construct empty; provide fluent `with_*` setters mirroring `http_response`'s style. Include enum `same_site_mode { unset, strict, lax, none }`.
12+
- [x] Add `http_response::with_cookie(cookie)` and `http_response::with_cookie(std::string name, std::string value)` overloads. Internally render to the `Set-Cookie` header per RFC 6265 §4.1.
13+
- [x] Provide `http_request::get_cookies()` returning a structured view (parsed once, cached on the request impl per TASK-016 arena pattern).
14+
- [x] Document migration: legacy string-blob path remains as a `[[deprecated]]` thin shim for one transitional release.
15+
- [x] Add a unit test pinning round-trip parsing/rendering against RFC 6265 examples.
1616

1717
**Dependencies:**
1818
- Blocked by: TASK-016 (arena), TASK-009 (http_response value type)
@@ -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

0 commit comments

Comments
 (0)