Skip to content

Commit e83857a

Browse files
committed
Merge TASK-195: Serve public HTTP2 streams through native listeners
2 parents eb4bf44 + 7269440 commit e83857a

66 files changed

Lines changed: 6552 additions & 177 deletions

File tree

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)