Skip to content

Commit aef6089

Browse files
committed
Merge TASK-197: Complete public HTTP/3 lifecycle, policy and WebSocket parity
Complete public HTTP/3 semantics, bounded streams, drain, TLS credential policy and RFC9220 WebSocket behavior. Include installed client witnesses, local gate evidence, examples and capability documentation. Nonlocal platform checks remain CI-owned.
2 parents 05e5e89 + d5e30ce commit aef6089

57 files changed

Lines changed: 17915 additions & 129 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,9 @@ __pycache__/
144144
!scripts/check-v3-public-tls.py
145145
!scripts/check-v3-public-http2.py
146146
!scripts/test-v3-public-http2.py
147+
!test/installed/v3_http3_credentials.py
148+
!test/installed/v3_http3_semantics.py
149+
!test/installed/v3_http3_lifecycle.py
150+
!test/installed/v3_http3_qpack.py
151+
152+
!test/installed/v3_http3_flow.py

‎Makefile.am‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,6 @@ EXTRA_DIST += scripts/test-v3-http3-full-gates.py scripts/check-v3-public-http3.
572572
test/installed/v3_http3_aioquic.py test/integ/http3-client-go/public.go
573573
EXTRA_DIST += specs/architecture/v3/TASK-196-design.md \
574574
specs/architecture/v3/TASK-196-implementation-evidence.md specs/architecture/v3/TASK-196-receipt.json
575+
576+
EXTRA_DIST += specs/architecture/v3/TASK-197-design.md \
577+
specs/architecture/v3/TASK-197-implementation-evidence.md specs/architecture/v3/TASK-197-receipt.json

‎README.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ There is no v2 source or ABI compatibility shim. Include native headers such as
2828
| Native capability | Current public availability |
2929
| --- | --- |
3030
| HTTP/1.0 and HTTP/1.1 plaintext | Managed and external listeners |
31-
| HTTP/1.1 WebSocket | Coroutine upgrade/echo; independent of legacy WS flags |
32-
| TLS / HTTP/2 / HTTP/3 listeners | Typed `not_supported` before binding |
33-
| Certificate rotation | Repository-internal demonstration only; no public installation API |
31+
| WebSocket over HTTP/1.1, HTTP/2 and HTTP/3 | Coroutine upgrade, bounded sessions, RFC8441/RFC9220 Extended CONNECT |
32+
| TLS / HTTP/2 / HTTP/3 listeners | Certificate and initial mTLS; TCP ALPN `h2`, explicit UDP QUICv1 ALPN `h3`; managed and external loops |
33+
| Certificate rotation | Public `tls_credentials::replace`; accepted sessions retain immutable selections |
34+
| TLS-off encrypted listeners | Same declarations; typed `not_supported` before binding |
3435

3536
Build native examples with `./bootstrap`, an out-of-tree configure using
3637
`CXX=clang++ CXXFLAGS='-std=c++20 -O0 -g'`, `--enable-examples` and
3738
`--disable-v3-tls`, then `make -j2`. For the optional native provider use
3839
`--enable-v3-tls` and genuine OpenSSL 3.5.x patch 9 or later as described in
39-
[the native TLS build guide](docs/native-tls-build.md). Provider availability
40-
and configuration validation do not imply public encrypted listener availability.
40+
[the native TLS build guide](docs/native-tls-build.md). Encrypted listeners require
41+
valid published credentials; HTTP/3 additionally requires explicit UDP configuration.
4142
The installed library is native-only: TLS-off needs C++20, threads and platform
4243
socket libraries; TLS-on adds the selected private OpenSSL provider. MHD and
4344
GnuTLS are no longer build or package prerequisites. Version **3.0.0** uses ELF

‎docs/v2-to-v3-migration.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ TCP/TLS/QUIC transport evidence; these are different kinds of proof.
2323
| Plaintext HTTP/1.0 and HTTP/1.1 | Managed and external listeners available | `v3_routes`, `v3_external_event_loop` |
2424
| HTTP/1.1 WebSocket Upgrade | Coroutine API available without legacy WS build flags | `v3_websocket_echo` |
2525
| Certificate TCP TLS HTTP/1 listeners | Public credential store, managed/external listeners in TLS-on builds | `test/installed/v3_tls_consumer.cpp` |
26-
| HTTP/2 and HTTP/3 listeners | `native_server::listen()` returns `not_supported` before binding any endpoint | `v3_protocol_options` |
26+
| HTTP/2 and HTTP/3 listeners | TLS-on: TCP ALPN `h2`; explicit UDP QUICv1/TLS1.3 ALPN `h3`, managed/external | `v3_protocol_options`, `v3_http3` |
27+
| HTTP/2 and HTTP/3 WebSocket | RFC8441/RFC9220 Extended CONNECT shares the bounded session API | Installed independent wire corpora |
2728
| Common HTTP/1.1, HTTP/2, HTTP/3 handlers | Semantic exchange contract exercised for each version | `test/unit/v3_examples_test.cpp` |
2829
| Certificate rotation | Public `tls_credentials::replace`, immutable accepted selections | `test/examples/v3_tls_rotation.cpp`, installed TLS runner |
2930

30-
`server_options::validate()` checks configuration semantics, not transport
31-
availability. A TLS-on build can validate certificate-profile H2/H3 options and
32-
still refuse `listen()`. `query_features()` distinguishes provider availability
33-
from public transports. Valid credential-backed mixed plaintext/TLS listeners
34-
serve HTTP/1; missing credentials reject atomically. Private H2/H3 and Extended
35-
CONNECT evidence remains distinct from public protocol availability.
31+
`server_options::validate()` checks configuration semantics. TLS-on encrypted
32+
listeners also require compatible published credentials. HTTP/3 requires a UDP
33+
listener entry; TCP and UDP may share a fixed numeric port through two explicit
34+
entries. Missing credentials and invalid mappings reject atomically. TLS-off keeps
35+
the same declarations and typed `not_supported` outcomes. Public installed-client
36+
evidence supplements the retained private transport and Extended CONNECT gates.
3637

3738
## Server construction and routing
3839

‎docs/v3-api.md‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ selection, budgets, timeouts, worker concurrency and loop ownership.
1313
Plaintext HTTP/1.0 and HTTP/1.1 listeners are available. TLS-on builds also serve
1414
public HTTP/1 certificate, initial mTLS and eligible external-PSK listeners.
1515
Certificate and initial mTLS listeners also serve HTTP/2 when enabled and negotiated
16-
with TLS ALPN `h2`. HTTP/3 listeners still return not_supported before binding.
16+
with TLS ALPN `h2`. Explicit UDP listeners serve QUICv1 HTTP/3 with ALPN `h3`.
1717
HTTP/2 has no cleartext upgrade or prior-knowledge listener.
1818
Provider availability from \ref httpserver::query_features is a separate fact.
1919

@@ -54,7 +54,10 @@ canonical attribute ordering, SameSite=None and bounded GMT Expires behavior.
5454
\ref httpserver::exchange::upgrade returns a typed rejection or owning
5555
\ref httpserver::websocket::session. Check try_send backpressure and await
5656
writable; keep the session alive through close notification. The public example
57-
uses HTTP/1.1 Upgrade; private H2/H3 evidence uses Extended CONNECT.
57+
uses HTTP/1.1 Upgrade. Native H2 uses RFC8441 Extended CONNECT and native H3
58+
uses RFC9220 Extended CONNECT through the same operation. The public
59+
examples/v3_http3.cpp example demonstrates a certificate-backed UDP listener and
60+
keeps the WebSocket session alive through Close/FIN.
5861

5962
\include v3_websocket_echo.cpp
6063

@@ -100,4 +103,28 @@ options.add_listener({"127.0.0.1", 0, true, credentials,
100103
101104
Publish a certificate host with `h3` ALPN through the ordinary public credential store and register the ordinary public routes. `get_bound_port(index)` reports that UDP entry's actual port. To serve TCP and UDP on the same fixed port, configure two entries with different transports. All endpoints bind before any becomes active; partial failure rolls back every port. H3 without explicit UDP and UDP without H3/TLS fail before bind. TLS-off builds retain these declarations and return typed `not_supported`. External PSK remains HTTP/1-only.
102105
103-
The native path supports authenticated QUICv1/TLS1.3 routes, incremental bodies, ordered trailers and independent concurrent request streams. Native RFC9220 and comprehensive HTTP/3 parity/rotation/staged drain are subsequent v3 tasks. `request_stop()` is immediate; `stop()` and destruction retain callback services until pending completions and protocol close expiry, including when an external host loop has stopped dispatching.
106+
The native path supports authenticated QUICv1/TLS1.3 routes, incremental bodies,
107+
ordered trailers, independent concurrent request streams and RFC9220 WebSocket
108+
Extended CONNECT. Routes use the same authentication, forms and immutable response
109+
services as TCP. Stream credit follows actual application consumption; a stalled
110+
body or writer leaves admitted siblings and critical protocol output usable.
111+
Credential replacement preserves established connections and isolates new
112+
handshakes by host/store/generation. Initial mTLS is available; external PSK remains
113+
HTTP/1-only and QUIC never selects ACME challenge credentials or accepts 0-RTT data.
114+
115+
One `begin_drain` ticket includes every TCP/UDP listener, accepted application work
116+
and reliable response retirement. H3 staged GOAWAY refuses new requests, accepted
117+
streams can finish, and WebSocket sessions receive Close 1001. Deadline expiry
118+
cancels remaining work. Protocol closing retention is separate from unfinished
119+
application work. `request_stop()` is immediate; `stop()` and destruction retain
120+
callback services through pending atomic sends and actual three-PTO close expiry,
121+
including when an external host loop has stopped dispatching.
122+
123+
The current native connection has bounded history for 64 transport stream records
124+
and 16 active requests. A terminal request releases heavy buffers and replenishes
125+
MAX_STREAMS only after reliable FIN/reset acknowledgment; retained IDs cannot be
126+
recreated. The fixed lifetime history remains a connection envelope, even after
127+
live application charges return to baseline.
128+
129+
See `examples/v3_http3.cpp` for one route registry serving TCP H1/H2 and UDP H3,
130+
with an RFC9220 echo route and either loop mode.

‎examples/Makefile.am‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ noinst_PROGRAMS += v3_external_event_loop
2525
v3_external_event_loop_SOURCES = v3_external_event_loop.cpp v3_host_poll.hpp
2626
v3_external_event_loop_LDADD = $(top_builddir)/src/libhttpserver.la
2727

28-
noinst_PROGRAMS += v3_routes v3_protocol_options v3_websocket_echo
28+
noinst_PROGRAMS += v3_routes v3_protocol_options v3_websocket_echo v3_http3
29+
v3_http3_SOURCES = v3_http3.cpp v3_host_poll.hpp
30+
v3_http3_LDADD = $(top_builddir)/src/libhttpserver.la
31+
v3_http3_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS)
2932
v3_routes_SOURCES = v3_routes.cpp v3_route_handlers.hpp v3_example_host.hpp
3033
v3_protocol_options_SOURCES = v3_protocol_options.cpp
3134
v3_websocket_echo_SOURCES = v3_websocket_echo.cpp v3_example_host.hpp

‎examples/README.md‎

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,18 @@ Run `./examples/v3_protocol_options` for the configuration report and
3838
The WebSocket example accepts `/echo` without an Origin policy for local smoke
3939
use; applications should configure allowed origins, subprotocols and limits.
4040

41-
The protocol example compiles in TLS-off and TLS-on builds. It validates each
42-
configuration and reports `not_supported` with port zero for public TLS/H2/H3
43-
listeners. It does not serve H2/H3. Shared-handler tests exercise their semantic
44-
contract; the [TASK-179 audit](../docs/task-179-behavior-parity.md) separately
45-
records private encrypted transport and Extended CONNECT evidence.
41+
The protocol example compiles in TLS-off and TLS-on builds. In TLS-on, pass a PEM
42+
certificate chain and matching private key: `v3_protocol_options CHAIN_PEM KEY_PEM`.
43+
It binds each supported listener, reports the actual port, then stops. TLS-off
44+
reports typed `not_supported` with port zero for TLS/H2/H3.
45+
46+
`v3_http3 CHAIN_PEM KEY_PEM [managed|external]` serves `/hello` through the same
47+
route on TCP H1/H2 and UDP H3, and `/ws/echo` through RFC9220 Extended CONNECT.
48+
Its sample SNI is `a.example` and required WebSocket subprotocol is `harness.v1`;
49+
adapt both to your certificates/application. The first output line is the UDP
50+
port; the second reports `TCP_PORT`. Enter `stop` or close stdin to shut down.
51+
The installed runner compiles this example in shared/static cells and exercises
52+
both loops with independent clients. The retained private gates remain separate.
4653

4754
The native examples also compile as installed consumers. With libhttpserver.pc and prerequisite .pc metadata
4855
available via PKG_CONFIG_PATH, run from the repository root:
@@ -54,21 +61,20 @@ clang++ -std=c++20 examples/v3_protocol_options.cpp -o /tmp/v3_protocol_options
5461
clang++ -std=c++20 examples/v3_external_event_loop.cpp -o /tmp/v3_external_event_loop $(pkg-config --cflags --libs libhttpserver)
5562
```
5663

57-
Private TLS rotation demonstration
58-
----------------------------------
59-
60-
[test/examples/v3_tls_rotation.cpp](../test/examples/v3_tls_rotation.cpp) is
61-
repository-internal, requires `HTTPSERVER_COMPILATION`, and cannot use installed
62-
public headers. Build with the genuine provider described in
63-
[native-tls-build.md](../docs/native-tls-build.md): configure a fresh build with
64-
`--enable-v3-tls --enable-examples` and explicit `V3_TLS_CFLAGS`/`V3_TLS_LIBS`, run
65-
`make -j2`, then `make -C test v3_tls_rotation`. Run `./test/v3_tls_rotation` from
66-
the build root. It loads committed fixtures and demonstrates retained old
67-
selection, new active selection, and failed replacement preserving that selection.
68-
`make -C test check TESTS='tls_credentials tls_credentials_rotation v3_tls_rotation'
69-
check_PROGRAMS='tls_credentials tls_credentials_rotation v3_tls_rotation'` runs
70-
the private demonstration and independent established-session handshake tests.
71-
There is no public native credential installation/rotation API.
64+
Public TLS rotation example
65+
---------------------------
66+
67+
[test/examples/v3_tls_rotation.cpp](../test/examples/v3_tls_rotation.cpp) uses
68+
public headers and the `server::tls_credentials` store. Build with the genuine
69+
provider described in [native-tls-build.md](../docs/native-tls-build.md), then
70+
run `make -C test v3_tls_rotation` in the configured build. Supply a certificate
71+
directory containing the committed fixture names, a loop mode and a TCP port:
72+
`./test/v3_tls_rotation /path/to/test/data/tls_credentials managed 0`.
73+
The first output line is the actual listener port. The `/installed` route returns
74+
`rotation-example`. Stdin `invalid` preserves the active generation, `replace`
75+
publishes the `b.example` material, and `stop` shuts down. Existing connections
76+
continue across replacement; new handshakes select the new generation. The
77+
example smoke test and installed TCP/QUIC matrices verify these public operations.
7278

7379
Legacy v2 examples
7480
------------------

‎examples/v3_http3.cpp‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */
2+
#include <atomic>
3+
#include <fstream>
4+
#include <iostream>
5+
#include <memory>
6+
#include <thread>
7+
#include <httpserver.hpp>
8+
#include <httpserver/features.hpp>
9+
#include <httpserver/server/tls_credentials.hpp>
10+
#include "v3_host_poll.hpp"
11+
12+
namespace srv = httpserver::server;
13+
namespace http = httpserver::http;
14+
15+
int main(int argc, char** argv) {
16+
if (!httpserver::query_features().tls_provider) {
17+
std::cout << "TLS_OFF_H3_REJECTED" << std::endl; return 0;
18+
}
19+
if (argc < 3 || argc > 4) {
20+
std::cerr << "usage: v3_http3 CHAIN_PEM KEY_PEM [managed|external]\n"; return 2;
21+
}
22+
const auto read = [](const char* path) {
23+
std::ifstream input(path); return std::string(std::istreambuf_iterator<char>(input), {});
24+
};
25+
auto credentials = std::make_shared<srv::tls_credentials>();
26+
if (!credentials->replace({{{"a.example", read(argv[1]), read(argv[2]), {"h3", "h2", "http/1.1"}}}, 0}).ok()) return 1;
27+
srv::server_options options;
28+
options.concurrency().workers = 2;
29+
options.loop() = argc == 4 && std::string_view(argv[3]) == "external" ? srv::loop_mode::external : srv::loop_mode::managed;
30+
options.protocols().enable(http::protocol::http_2);
31+
options.protocols().enable(http::protocol::http_3);
32+
options.tls().provider = srv::tls_provider::system_default;
33+
options.tls().profile = srv::tls_profile::certificates;
34+
options.add_listener({"127.0.0.1", 0, true, credentials, srv::listener_transport::udp});
35+
options.add_listener({"127.0.0.1", 0, true, credentials});
36+
srv::native_server server(options);
37+
if (!server.route(http::method::known(http::method_id::get), "/hello", [](httpserver::exchange& x) -> httpserver::task<void> {
38+
x.start_response(http::status::from_code(200), {});
39+
const std::string body = "hello native h3\n";
40+
co_await x.writer().write(std::as_bytes(std::span(body)));
41+
http::fields trailers; trailers.append("x-end", "native-fin");
42+
co_await x.writer().finish(trailers);
43+
}).ok()) return 1;
44+
if (!server.route(http::method::known(http::method_id::connect), "/ws/echo", [](httpserver::exchange& x) -> httpserver::task<void> {
45+
httpserver::ws_upgrade_options upgrade;
46+
upgrade.subprotocols = {"harness.v1"}; upgrade.require_subprotocol = true;
47+
upgrade.limits = {8192, 16384, 16384, 4, 4};
48+
auto opened = co_await x.upgrade(upgrade);
49+
if (!opened.status.ok()) { x.respond(opened.rejection_status, opened.rejection_fields); co_return; }
50+
auto session = std::move(*opened.session);
51+
httpserver::resume_signal closed;
52+
session.on_close([&](auto) { closed.signal(); });
53+
for (;;) {
54+
const auto received = co_await session.receive();
55+
if (!received.status.ok() || !received.value) { co_await closed.wait(); co_return; }
56+
for (;;) {
57+
const auto sent = session.try_send(received.value->kind, received.value->data);
58+
if (sent.disposition == httpserver::websocket::send_disposition::accepted) break;
59+
if (!sent.status.ok() || !(co_await session.writable()).ok()) co_return;
60+
}
61+
}
62+
}).ok() || !server.listen().ok()) return 1;
63+
std::cout << server.get_bound_port(0) << std::endl;
64+
std::cout << "TCP_PORT " << server.get_bound_port(1) << std::endl;
65+
std::atomic<bool> stopping{false};
66+
auto* driver = server.readiness();
67+
std::thread host;
68+
if (driver) host = std::thread([&] {
69+
while (!stopping.load()) {
70+
const auto snapshot = driver->interests();
71+
const auto events = v3_host::wait(snapshot, std::chrono::steady_clock::now() + std::chrono::milliseconds(100));
72+
if (!driver->dispatch(events, std::chrono::steady_clock::now()).ok()) break;
73+
}
74+
});
75+
std::string command;
76+
while (std::getline(std::cin, command) && command != "stop") {}
77+
server.request_stop(); stopping = true;
78+
if (host.joinable()) host.join();
79+
server.stop();
80+
}

0 commit comments

Comments
 (0)