Skip to content

Commit 8ddc264

Browse files
committed
TASK-195: Serve native HTTP/2 with independent stream execution
Compose negotiated TLS H2 with bounded wire/stream affinities, hierarchical budgets, exact body credit, common routing and policy, deadlines, RFC8441, and native staged drain. Repair the shared H1 disconnect publication race exposed by parity verification. Verify 76 TLS-on and 20 TLS-off suites in normal, ASan/UBSan and TSan; all shared/static managed/external public matrices with both pinned clients; installed package parity; 145 required h2spec cases; local headers, docs, linkage, lint and distribution checks. Preserve causal failures and exact evidence in TASK-195-receipt.json and the task-local handoff.
1 parent eb4bf44 commit 8ddc264

65 files changed

Lines changed: 6525 additions & 177 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,5 @@ __pycache__/
142142
!test/integ/native_websocket_regression.py
143143

144144
!scripts/check-v3-public-tls.py
145+
!scripts/check-v3-public-http2.py
146+
!scripts/test-v3-public-http2.py

‎Makefile.am‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,3 +561,8 @@ EXTRA_DIST += scripts/audit-v3-native-linkage.sh scripts/test-v3-cutover.py scri
561561
EXTRA_DIST += scripts/check-v3-public-tls.py test/installed/v3_tls_consumer.cpp
562562

563563
EXTRA_DIST += scripts/lib/v3_public_tls_profiles.py test/installed/v3_tls_profiles.cpp test/installed/v3_tls_client.cpp
564+
EXTRA_DIST += scripts/check-v3-public-http2.py scripts/test-v3-public-http2.py \
565+
scripts/lib/v3_public_http2_inventory.py scripts/lib/v3_public_http2_h2.py scripts/lib/v3_public_http2_policy.py \
566+
test/installed/v3_http2_consumer.cpp test/installed/v3_http2_node.mjs test/installed/v3_http2_raw.mjs \
567+
examples/v3_http2.cpp specs/architecture/v3/TASK-195-design.md \
568+
specs/architecture/v3/TASK-195-implementation-evidence.md specs/architecture/v3/TASK-195-receipt.json

‎docs/native-tls-build.md‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ cross builds must run the smoke on the target in CI.
2727
`<httpserver/features.hpp>` exposes the same compiled `query_features()` declaration
2828
in both builds, without backend headers or configuration macros. Availability of
2929
the provider is distinct from availability of TCP TLS, HTTP/2, and HTTP/3. TCP TLS is available in TLS-on builds for certificate, initial mTLS and eligible
30-
external-PSK HTTP/1.0 and HTTP/1.1 in both managed and external modes. HTTP/2 and HTTP/3 remain
31-
unavailable until their owning tasks implement public listeners. `listen()`
30+
external-PSK HTTP/1.0 and HTTP/1.1 in both managed and external modes. Certificate
31+
and initial mTLS profiles also serve enabled HTTP/2 through TLS ALPN `h2`.
32+
HTTP/3 public listeners remain unavailable. `listen()`
3233
rejects unsupported protocol/profile requests before binding;
3334
`server_options::validate()` remains semantic.
3435

@@ -139,8 +140,10 @@ through retiring I/O completions. No global credential store exists.
139140
Host names use exact canonical DNS matching, with case folding and a final dot
140141
removed. Unknown or absent SNI selects `default_host` (zero by default). Each
141142
listener has its own explicit store; sharing is intentional only when the same
142-
store is assigned. ALPN may contain `http/1.1` or be empty for ordinary HTTP/1
143-
fallback. A listener with HTTP/1.1 disabled must initially use empty ALPN. Later
143+
store is assigned. Certificate and initial mTLS ALPN may contain `h2`,
144+
`http/1.1`, or both; empty ALPN selects ordinary HTTP/1 fallback. An advertised
145+
protocol must be enabled in every listener bound to the credential store. PSK
146+
profiles retain HTTP/1-only ALPN and reject `h2`. Later
144147
publication cannot negotiate a disabled ALPN; wire requests for a disabled H1
145148
version are rejected before routing. `exchange::tls()` and request hook contexts
146149
carry library-owned immutable authenticated peer metadata; plaintext uses null,
@@ -251,3 +254,25 @@ example takes `certificate-directory managed|external port`, opens a real public
251254
listener and accepts `replace`, `invalid` and `stop` on stdin. Its independent
252255
installed witness holds a usable connection across rotation and validates the
253256
new certificate plus rejected-publication preservation.
257+
258+
Public HTTP/2 uses the same native server, route table, hooks, peer policy and
259+
exchange API as HTTP/1. See [the public example](../examples/v3_http2.cpp) and
260+
[TASK-195 design](../specs/architecture/v3/TASK-195-design.md). TLS-off
261+
configuration rejects HTTP/2 before binding. TLS 1.2 HTTP/2 rejects forbidden
262+
ciphers before publishing request hooks.
263+
264+
Connection wire operations run serially; each stream has its own handler
265+
affinity on the configured workers. A running handler can occupy one worker,
266+
so applications should keep handlers cooperative and size the worker pool for
267+
their concurrent work. Receive credit follows body consumption. Configured
268+
server limits bound listener, connection and stream descendants, including
269+
header copies, body rings, output and WebSocket storage. Header, SETTINGS,
270+
body-idle, suspension, write-idle, WebSocket close and drain timers use native
271+
readiness deadlines without periodic polling.
272+
273+
RFC8441 Extended CONNECT upgrades one stream and permits ordinary siblings.
274+
Native drain publishes staged GOAWAY, finishes accepted work, and cancels
275+
remaining requests and WebSockets at its absolute deadline. A retained ticket
276+
keeps the original deadline result; its remaining count describes unfinished
277+
work at expiry, even after actual retirement. A counted handler cannot wait
278+
on its own drain ticket.

‎docs/v3-api.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ selection, budgets, timeouts, worker concurrency and loop ownership.
1212
\ref httpserver::server::native_server validates before binding and owns shutdown.
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.
15-
Public H2/H3 listeners still return not_supported before binding.
15+
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.
17+
HTTP/2 has no cleartext upgrade or prior-knowledge listener.
1618
Provider availability from \ref httpserver::query_features is a separate fact.
1719

1820
\include v3_protocol_options.cpp

‎examples/README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,16 @@ empty.
207207

208208
Thanks to https://devcenter.heroku.com/articles/ssl-certificate-self
209209
for these instructions.
210+
211+
`v3_http2.cpp` serves `/hello` through one certificate listener and one route
212+
table for HTTP/1.1 and HTTP/2. Build against the installed package:
213+
214+
```sh
215+
c++ -std=c++20 v3_http2.cpp $(pkg-config --cflags --libs libhttpserver) -pthread -o v3_http2
216+
./v3_http2 a-chain.pem a-key.pem
217+
```
218+
219+
The example prints its loopback port and stops when stdin receives a line.
220+
The certificate chain must identify `a.example`; clients should verify its CA
221+
and SNI. HTTP/2 requires a TLS-on build. The installed H2 matrix compiles and
222+
runs this example with both shared and static linkage.

‎examples/v3_http2.cpp‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */
2+
#include <fstream>
3+
#include <iostream>
4+
#include <memory>
5+
#include <string>
6+
#include <httpserver.hpp>
7+
#include <httpserver/server/tls_credentials.hpp>
8+
9+
namespace srv = httpserver::server;
10+
namespace http = httpserver::http;
11+
12+
std::string read_pem(const char* path) {
13+
std::ifstream input(path);
14+
return {std::istreambuf_iterator<char>(input), {}};
15+
}
16+
17+
int main(int argc, char** argv) {
18+
if (argc != 3) return 2;
19+
auto credentials = std::make_shared<srv::tls_credentials>();
20+
srv::tls_host_credentials host{"a.example", read_pem(argv[1]), read_pem(argv[2]), {"h2", "http/1.1"}};
21+
if (!credentials->replace({{host}, 0}).ok()) return 1;
22+
srv::server_options options;
23+
options.protocols().enable(http::protocol::http_2);
24+
options.tls().provider = srv::tls_provider::system_default;
25+
options.tls().profile = srv::tls_profile::certificates;
26+
options.add_listener({"127.0.0.1", 0, true, credentials});
27+
srv::native_server server(options);
28+
if (!server.route(http::method::known(http::method_id::get), "/hello", [](httpserver::exchange& x) -> httpserver::task<void> {
29+
x.start_response(http::status::from_code(200), {});
30+
const std::string text = "hello h2\n";
31+
co_await x.writer().write(std::as_bytes(std::span(text)));
32+
co_await x.writer().finish();
33+
}).ok() || !server.listen().ok()) return 1;
34+
std::cout << server.get_bound_port(0) << std::endl;
35+
std::string command;
36+
std::getline(std::cin, command);
37+
server.stop();
38+
}

‎scripts/check-v3-installed-consumer.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ def package_lane(args, source, output, mode, inventory):
102102
command([sys.executable, str(source / 'scripts/check-v3-public-tls.py'), '--prefix', str(prefix),
103103
'--provider-prefix', str(args.provider_prefix), '--build', str(build), '--tls', mode,
104104
'--compiler', args.compiler, '--output', str(lane / 'public-tls')], source, runtime_env)
105+
command([args.client_python, str(source / 'scripts/check-v3-public-http2.py'), '--prefix', str(prefix),
106+
'--provider-prefix', str(args.provider_prefix), '--build', str(build), '--tls', mode,
107+
'--compiler', args.compiler, '--node', args.node, '--output', str(lane / 'public-http2')], source, runtime_env)
105108
receipt = audit.audit(prefix, build, consumer_dir / ('v3_package_consumer' + suffix), mode,
106109
'strict', args.provider_prefix, args.compiler)
107110
receipt.update(consumer_metadata=metadata, public=public, integrations=integrations)
@@ -122,6 +125,8 @@ def main():
122125
parser.add_argument('--jobs', type=int, default=2)
123126
parser.add_argument('--compiler', default='c++')
124127
parser.add_argument('--cc', default='cc')
128+
parser.add_argument('--client-python', default=sys.executable)
129+
parser.add_argument('--node', default='node')
125130
parser.add_argument('--make', default='make')
126131
args = parser.parse_args()
127132
source = Path(__file__).resolve().parent.parent

0 commit comments

Comments
 (0)