Skip to content

Commit d97a421

Browse files
committed
Merge TASK-062: RFC-7616-compliant Digest auth response factory
Brings in the new `unauthorized(digest_challenge{})` overload that routes through `MHD_queue_auth_required_response3`, drives the real MHD nonce/opaque state machine, and lets `curl --digest` complete the full RFC-7616 handshake. Also: wire-format integ tests (algorithm token, opaque, domain), HA1 resource migration, factory unit tests pinned to body_kind discrimination only (no internal coupling).
2 parents e448c35 + 73c6e13 commit d97a421

18 files changed

Lines changed: 1357 additions & 84 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The body subclasses (`detail::string_body`, `file_body`, `iovec_body`, `pipe_bod
1919

2020
**Interfaces:**
2121
- Exposes (from PRD §3.5):
22-
- Factories: `http_response::string(...)`, `::file(...)`, `::iovec(std::span<const httpserver::iovec_entry>)`, `::pipe(...)`, `::empty(...)`, `::deferred(...)`, `::unauthorized(scheme, realm, ...)` — all return `http_response` by value. **For the `"Digest"` scheme**, `unauthorized()` produces only a static `WWW-Authenticate: Digest realm="<realm>"` challenge; it does NOT set `nonce`, `opaque`, `algorithm`, or `qop`, which RFC 7616 §3.3 requires. The response provides no replay protection and will be rejected by strict RFC 7616 parsers. This is a known limitation of the value-typed response model (DR-013). Callers needing fully RFC-compliant Digest auth must call MHD APIs directly; use `"Basic"` for a compliant challenge.
22+
- Factories: `http_response::string(...)`, `::file(...)`, `::iovec(std::span<const httpserver::iovec_entry>)`, `::pipe(...)`, `::empty(...)`, `::deferred(...)`, `::unauthorized(scheme, realm, ...)`, `::unauthorized(digest_challenge)` — all return `http_response` by value. The `unauthorized(digest_challenge)` overload (TASK-062, declared behind `HAVE_DAUTH`) produces a fully RFC 7616 §3.3-compliant `WWW-Authenticate: Digest ` challenge with `nonce`, `opaque`, `algorithm`, and `qop` parameters; the dispatch path detects `body_kind::digest_challenge` and routes through libmicrohttpd's `MHD_queue_auth_required_response3`, which drives the per-connection nonce state machine. The legacy `unauthorized("Digest", realm, body)` string overload remains for source compatibility and emits only `WWW-Authenticate: Digest realm="<realm>"`; for new code prefer the `digest_challenge` overload. See DR-013 (Superseded by TASK-062) for the supersession note.
2323
- **`httpserver::iovec_entry`** is a library-defined POD declared in `<httpserver/iovec_entry.hpp>` (a dedicated public header installed alongside `http_response.hpp` and included by it): `struct iovec_entry { const void* base; std::size_t len; };`. It mirrors POSIX `struct iovec` exactly in layout but does not require `<sys/uio.h>` in any installed header. The internal dispatch path uses the user-supplied span to build a `struct iovec` array inside `iovec_body`. The implementation file `src/detail/body.cpp` carries `static_assert`s pinning the layout assumption: `static_assert(sizeof(iovec_entry) == sizeof(struct iovec))`, `static_assert(offsetof(iovec_entry, base) == offsetof(struct iovec, iov_base))`, `static_assert(offsetof(iovec_entry, len) == offsetof(struct iovec, iov_len))`. When the asserts hold, conversion is a `reinterpret_cast`; when they fail (a hypothetical platform with divergent layout), the build fails loudly at compile time and we fall back to memcpy. This keeps the public header free of system headers and makes the API uniformly available on platforms where `<sys/uio.h>` is not standard (e.g., MSVC builds).
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).

specs/architecture/11-decisions/DR-013.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### DR-013: Digest auth simplified to static WWW-Authenticate challenge
22

3-
**Status:** Accepted
3+
**Status:** Superseded by TASK-062 (2026-06-04)
44
**Date:** 2026-05-04
55
**Context:** v1's `basic_auth_fail_response` and `digest_auth_fail_response` each produced their 401 challenges via dedicated response subclasses; `digest_auth_fail_response` in particular delegated to `MHD_queue_auth_required_response3`, which sets the full RFC 7616 nonce, opaque, algorithm, and qop fields and wires the challenge into libmicrohttpd's per-connection Digest state machine. v2.0 removes all `*_response` subclasses and the `get_raw_response` / `decorate_response` / `enqueue_response` virtuals in favour of the unified `http_response::unauthorized(scheme, realm, body)` factory. The factory returns an `http_response` by value with no MHD connection handle available, so it cannot call `MHD_queue_auth_required_response3` — that API requires a live connection pointer, which is incompatible with the value-typed response model.
66

@@ -22,4 +22,21 @@
2222

2323
**Related:** PRD-RSP-REQ-005, TASK-013 §2/§10, http-response.md §4.3.
2424

25+
**Supersession note (TASK-062, 2026-06-04):**
26+
The "value-type response model cannot reach `MHD_queue_auth_required_response3`"
27+
constraint was lifted by adding a kind-dispatched queueing helper in
28+
`webserver_impl`. The dispatch path now branches on
29+
`body_kind::digest_challenge` and routes through
30+
`MHD_queue_auth_required_response3` instead of `MHD_queue_response`, while the
31+
response value itself remains a movable value-typed `http_response` (DR-005
32+
preserved). The new factory `http_response::unauthorized(digest_challenge)`
33+
(declared in `src/httpserver/http_response.hpp` behind `HAVE_DAUTH`) emits a
34+
fully RFC-7616 §3.3-compliant challenge with libmicrohttpd-owned nonce,
35+
server-owned opaque, and user-selectable algorithm (MD5/SHA-256/SHA-512-256)
36+
and qop (auth) parameters. The legacy `unauthorized("Digest", realm, body)`
37+
overload remains for source-compatibility but now its Doxygen points callers
38+
at the new overload as the RFC-compliant path. See TASK-062 plan §1, §2.3,
39+
§2.4 for the architecture; the dispatch-time switch lives in
40+
`src/detail/webserver_request.cpp::queue_response_dispatching_kind`.
41+
2542
---

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
Make `http_response::unauthorized(...)` produce an RFC-7616-compliant `WWW-Authenticate: Digest …` challenge with `nonce`, `opaque`, `algorithm`, and `qop` parameters, and drive the matching nonce/opaque server-side state machine so strict clients negotiate a real Digest session. The current implementation (`src/httpserver/http_response.hpp:184-196`) is documented as a non-RFC-compliant stub that strict parsers reject.
99

1010
**Action Items:**
11-
- [ ] Audit current `http_response::unauthorized(...)` overloads and document the gap against RFC 7616 §3 (challenge format) and §3.4 (`Authorization` validation) in a short header comment.
12-
- [ ] Add a nonce/opaque generator to `webserver_impl` (CSPRNG-backed, with replay/expiry tracking). Reuse the existing `dauth` plumbing where possible.
13-
- [ ] Extend `http_response::unauthorized(...)` to accept (or auto-derive) `nonce`, `opaque`, `algorithm` (default `MD5`, support `SHA-256` and `SHA-256-sess`), `qop` (default `auth`).
14-
- [ ] Wire the dispatch path to validate incoming `Authorization: Digest …` against the issued nonce/opaque pair and route to `dauth` handlers.
15-
- [ ] Convert the six v2 digest placeholder integ tests (`test/integ/authentication.cpp:42-60, 245-613`) to drive the nonce/opaque state machine end-to-end. Coordinated with TASK-079 (test work).
16-
- [ ] Update Doxygen on `unauthorized(...)` to remove the "non-RFC-compliant stub" disclaimer and add RFC references.
11+
- [x] Audit current `http_response::unauthorized(...)` overloads and document the gap against RFC 7616 §3 (challenge format) and §3.4 (`Authorization` validation) in a short header comment.
12+
- [x] Add a nonce/opaque generator to `webserver_impl` (CSPRNG-backed, with replay/expiry tracking). Reuse the existing `dauth` plumbing where possible.*delegated to libmicrohttpd via `MHD_queue_auth_required_response3` (nonce HMAC keyed by `MHD_OPTION_DIGEST_AUTH_RANDOM`, replay window by `MHD_OPTION_NONCE_NC_SIZE`); opaque generated once at `webserver_impl` construction from `std::random_device`.*
13+
- [x] Extend `http_response::unauthorized(...)` to accept (or auto-derive) `nonce`, `opaque`, `algorithm` (default `MD5`, support `SHA-256` and `SHA-256-sess`), `qop` (default `auth`).*new `unauthorized(digest_challenge)` overload supports MD5 (default), SHA-256, SHA-512-256, qop="auth". SHA-256-sess deferred (out of v2 scope, see plan §7).*
14+
- [x] Wire the dispatch path to validate incoming `Authorization: Digest …` against the issued nonce/opaque pair and route to `dauth` handlers.*dispatch branches on `body_kind::digest_challenge` in `webserver_impl::queue_response_dispatching_kind`; existing `req.check_digest_auth(...)` validates the returning header.*
15+
- [x] Convert the six v2 digest placeholder integ tests (`test/integ/authentication.cpp:42-60, 245-613`) to drive the nonce/opaque state machine end-to-end. Coordinated with TASK-079 (test work).*`digest_resource` updated to emit the new `digest_challenge` body; all six tests now exercise the real handshake path end-to-end (the wrong-password tests remain 401/FAIL but now go through `MHD_digest_auth_check3` validation rather than failing because no nonce was issued).*
16+
- [x] Update Doxygen on `unauthorized(...)` to remove the "non-RFC-compliant stub" disclaimer and add RFC references.
1717

1818
**Dependencies:**
1919
- Blocked by: None
@@ -30,4 +30,4 @@ Make `http_response::unauthorized(...)` produce an RFC-7616-compliant `WWW-Authe
3030
**Related Requirements:** PRD-RSP-REQ-005 (`unauthorized` factory completeness)
3131
**Related Decisions:** None new (RFC 7616)
3232

33-
**Status:** Backlog
33+
**Status:** Done

specs/tasks/M7-v2-cleanup/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ TASK-093).
2626
|---|---|---|---|---|
2727
| TASK-060 | Scope or remove file-scoped `-Warray-bounds` suppressions | HIGH | S | Done |
2828
| TASK-061 | Mechanical cleanup sweep — unfinished prose, orphan comments, stale doc refs | HIGH | S | Done |
29-
| TASK-062 | RFC-7616-compliant Digest auth response factory | HIGH | L | Backlog |
29+
| TASK-062 | RFC-7616-compliant Digest auth response factory | HIGH | L | Done |
3030
| TASK-063 | Honor or remove `http_response::pipe` `size_hint` parameter | HIGH | S | Backlog |
3131
| TASK-064 | Structured cookie type | HIGH | M | Backlog |
3232
| TASK-065 | RFC 5952 IPv6 zero-compression in `peer_address` | HIGH | S | Backlog |

0 commit comments

Comments
 (0)