Crafter.Network/README.md
catbot a79ab6a024 docs(http1): document the HTTP/1.1 stack, and expose the client's timeout
README: HTTP/1.1 in the intro, feature list, module list, browser-build
exclusions, dependencies and test list, plus a Components section
covering both classes, the standalone codec, what is and is not
implemented, the smuggling-shaped inputs that are rejected, and an
explicit note that this path is plaintext and belongs behind a TLS
terminator.

ClientHTTP1::timeout was hard-coded and invisible; make it a public
member alongside `limits`, mirroring the listener's timeouts.

Also pipelining coverage in ShouldSendRecieveHTTP1: two requests written
before either is answered, driven from a raw socket since ClientHTTP1
waits for each response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 01:02:37 +00:00

263 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Crafter.Network
A cross-platform C++ networking library providing TCP, QUIC, HTTP/3, HTTP/1.1, and WebTransport client/server functionality with modern C++ features. Builds for native Linux and for the browser (wasm32-wasip1).
## Overview
Crafter.Network is a C++ networking library designed for modern C++ applications. It provides TCP, QUIC, HTTP/3, HTTP/1.1, and WebTransport-over-HTTP/3 capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks including real-time multiplayer games. The same source compiles for native Linux (via msquic + POSIX sockets) and for the browser (via `fetch()` + `WebTransport` JS APIs); see [Browser build](#browser-build).
## Features
- **TCP Networking**: Client and server implementations for raw TCP connections (native only).
- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync.
- **HTTP/3**: Client and server implementations on top of QUIC. Uses ALPN `h3`, QUIC bidi streams for requests/responses, the mandatory unidirectional control stream + SETTINGS frame (RFC 9114 §6.2.1), the (empty) QPACK encoder + decoder unidi streams required by stricter peers like Chromium, and a built-in QPACK codec (RFC 9204) with the full static table, Huffman *decoding* (RFC 7541), and literal-only emission. The QPACK dynamic table is unused. The client is interoperable with mainstream public h3 endpoints (cloudflare, nghttp3-based servers, etc.).
- **HTTP/1.1**: Client and server over plain TCP (RFC 9112), for the large part of the world that is not ready for HTTP/3 — old proxies, CI tooling, load balancers, `curl` scripts. Shares the `HTTPRequest`/`HTTPResponse` types and the route-map API with the HTTP/3 stack, so a handler or call site moves between the two by changing the class name. Keep-alive and pipelining, `content-length` and `chunked` bodies with trailers, `Expect: 100-continue`, HEAD, automatic `Date`, and per-connection timeouts. Plaintext only — see [HTTP/1.1 Components](#http11-components).
- **WebTransport (server)**: `ListenerHTTP` accepts extended-CONNECT sessions (`:method=CONNECT, :protocol=webtransport`) negotiated on the existing h3 listener — no separate port or alternate stack. Both draft-02 and draft-07+ identifier sets are advertised in SETTINGS so current Chrome/Edge browsers connect out of the box. Per-route handlers receive a `WebTransportSession&` and can multiplex bidirectional streams over the session.
- **Browser client**: Same C++ API compiled to wasm32-wasip1 and routed through `fetch()` (for `ClientHTTP`) and `WebTransport` (for `ClientQUIC`). Listeners and raw TCP are not compiled in the browser build — the browser is client-only.
- **Asynchronous Operations**: Thread poolbased async operations on native; the same `*Async` API on the browser side, where it's required (no synchronous I/O in the browser event loop).
- **Cross-Platform**: Native Linux (POSIX sockets + msquic) and browser (wasm32-wasip1).
- **Modern C++**: Uses C++20 modules, STL containers, and modern C++ features.
## Architecture
The library follows a modular design using C++20 modules:
### Core Modules
- `Crafter.Network`: Main module that exports all components
- `Crafter.Network:ClientTCP`: TCP client implementation (native only)
- `Crafter.Network:ListenerTCP`: TCP server implementation (native only)
- `Crafter.Network:ClientHTTP`: HTTP/3 client (ALPN `h3`). On browser builds this maps to `fetch()`.
- `Crafter.Network:ListenerHTTP`: HTTP/3 + WebTransport server (ALPN `h3`, native only)
- `Crafter.Network:HTTP`: HTTP request/response types and constructors, shared by every HTTP version
- `Crafter.Network:ClientHTTP1`: HTTP/1.1 client over TCP (native only)
- `Crafter.Network:ListenerHTTP1`: HTTP/1.1 server over TCP (native only)
- `Crafter.Network:HTTP1`: HTTP/1.1 wire format — serialisation plus an incremental parser (RFC 9112). Transport-free; usable on its own to speak HTTP/1.1 over some other byte stream. Native only, for the same reason as `:HTTP3`
- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams. On browser builds this maps to the `WebTransport` JS API.
- `Crafter.Network:ListenerQUIC`: QUIC listener accepting incoming connections (native only). Also exports `ComputeCertificateHashSHA256()` and `GetSelfSignedCertificatePath()` for browser-peer cert pinning.
- `Crafter.Network:WebTransport`: `WebTransportSession` type — the per-session handle handed to `ListenerHTTP` WT route handlers.
- `Crafter.Network:HTTP3`: HTTP/3 wire-format helpers (QUIC varint, frame layer, QPACK static-table codec, WT frame/setting constants). Re-exported on native, excluded on browser (it uses `throw` and the wasm build is `-fno-exceptions`).
## Components
### TCP Components
#### ClientTCP
```cpp
// Create a TCP client
Crafter::ClientTCP client("localhost", 8080);
client.Send("Hello World", 11);
// Receive data
std::vector<char> data = client.RecieveSync();
```
#### ListenerTCP
```cpp
// Create a TCP listener
auto callback = [](Crafter::ClientTCP* client) {
// Handle new connection
};
Crafter::ListenerTCP listener(8080, callback);
listener.ListenSyncSync(); // Synchronous listening
```
### HTTP/3 Components
HTTP/3 runs over QUIC, which always requires TLS. Pass server credentials when constructing the listener (or set `selfSigned = true` for a development-only ephemeral cert) and matching client credentials when constructing the client (`insecureNoServerValidation = true` for self-signed servers).
#### ClientHTTP
```cpp
Crafter::QUICClientCredentials creds;
creds.insecureNoServerValidation = true; // dev-only
Crafter::ClientHTTP client("localhost", 8082, creds);
Crafter::HTTPResponse response = client.Send(
Crafter::CreateRequestHTTP("GET", "/", "localhost")
);
// response.status is the numeric status as a string, e.g. "200"
```
#### ListenerHTTP
```cpp
std::unordered_map<std::string,
std::function<Crafter::HTTPResponse(const Crafter::HTTPRequest&)>> routes;
routes["/hello"] = [](const Crafter::HTTPRequest&) {
return Crafter::CreateResponseHTTP("200", "Hello World!");
};
Crafter::QUICServerCredentials creds;
creds.selfSigned = true; // dev-only
Crafter::ListenerHTTP listener(8082, creds, routes);
listener.Listen();
```
The `HTTPRequest` exposes the four HTTP/3 pseudo-headers (`method`, `scheme`, `authority`, `path`) as named struct fields rather than mixing them into the regular `headers` map. Routes are dispatched by exact match on `path`; unmatched paths return a synthetic 404.
### HTTP/1.1 Components
HTTP/3 is not something every peer can be made to speak. `ClientHTTP1` and `ListenerHTTP1` provide the same API over plain TCP, using the same `HTTPRequest`/`HTTPResponse` types and the same route-map shape, so a handler can be registered with both and served over either protocol.
#### ClientHTTP1
```cpp
Crafter::ClientHTTP1 client("localhost", 8080);
Crafter::HTTPResponse response = client.Send(
Crafter::CreateRequestHTTP("GET", "/", "localhost")
);
```
The connection is persistent: the first `Send()` dials, and later calls reuse the socket unless the peer asked for it to be closed. A pooled connection that the peer closed while it looked idle — the race HTTP/1.1 keep-alive cannot avoid — is redialled once and the request replayed; nothing is replayed once a response byte has arrived, and a freshly dialled connection is never retried on, so a genuinely broken server surfaces as an exception rather than a retry loop. `authority` defaults to the host:port the client was constructed with (the port is elided when it is 80).
#### ListenerHTTP1
```cpp
std::unordered_map<std::string,
std::function<Crafter::HTTPResponse(const Crafter::HTTPRequest&)>> routes;
routes["/hello"] = [](const Crafter::HTTPRequest&) {
return Crafter::CreateResponseHTTP("200", "Hello World!");
};
Crafter::ListenerAsyncHTTP1 listener(8080, std::move(routes));
```
Each accepted connection gets its own thread and is served sequentially until the peer closes it, a `Connection: close` is seen, or a timeout expires (`keepAliveTimeout`, default 15 s between requests; `requestTimeout`, default 30 s for one request to arrive). A dedicated thread rather than a ThreadPool task is deliberate: keep-alive connections are idle most of their life and would otherwise pin every pool thread.
Routing matches `path` exactly and then falls back to the query-stripped path, so `/thing?x=1` reaches the handler registered for `/thing` while the handler still sees the full target in `request.path`. A handler that throws becomes a 500; an unknown path a 404; a request we refuse to parse a 400. `Date` is stamped automatically unless the handler set one, HEAD returns the headers a GET would have produced with no body, and a handler can end the connection by answering with a `connection: close` header.
Implemented: keep-alive, pipelining, `content-length` and `chunked` request bodies with trailers, `Expect: 100-continue`, absolute-form request targets, obs-fold, and HTTP/1.0 peers (which only get connection reuse when they ask for it). Not implemented: TLS, CONNECT tunnels, `Upgrade`, and chunked *responses* — handlers return a complete body, so responses are always `content-length` framed.
Ambiguous framing is rejected rather than guessed at, because guessing is how request smuggling happens (RFC 9112 §11.2): `Content-Length` together with `Transfer-Encoding`, disagreeing duplicate `Content-Length` values, and whitespace between a field name and its colon are all 400s. CR/LF in a header value we are asked to *send* throws instead of splitting the message.
#### No TLS
`ClientHTTP1`/`ListenerHTTP1` speak `http://` only. There is no `https://` support and no plan to link a TLS stack into this path: for encrypted traffic use `ClientHTTP`/`ListenerHTTP` (HTTP/3 over QUIC, which is always encrypted), or terminate TLS in a proxy in front of the HTTP/1.1 endpoint. Do not expose an HTTP/1.1 listener directly to the internet.
#### HTTP/1.1 wire format on its own
`Crafter::HTTP1` exposes the codec without the transport — `SerializeRequest` / `SerializeResponse` and an incremental `MessageParser` that takes arbitrary byte chunks and yields one message at a time. Useful for speaking HTTP/1.1 over a byte stream this library does not own.
```cpp
Crafter::HTTP1::MessageParser parser(Crafter::HTTP1::MessageKind::Response);
parser.SetRequestMethod("GET"); // framing depends on the request method
parser.Feed(chunk.data(), chunk.size());
if (parser.Complete()) {
Crafter::HTTPResponse response = parser.TakeResponse();
parser.Reset(); // rearm; keeps any pipelined bytes
}
```
### WebTransport Components
`ListenerHTTP` has a WT-aware constructor overload that takes a second route map keyed by `:path`. When the map is non-empty the listener advertises both draft-02 (`SETTINGS_ENABLE_WEBTRANSPORT = 0x2b603742`) and draft-07+ (`SETTINGS_WT_MAX_SESSIONS = 0xc671706a`) identifiers in its SETTINGS frame so current browsers connect. An extended-CONNECT request (`:method=CONNECT, :protocol=webtransport`) whose `:path` matches a registered route is accepted with a `200` (no FIN), upgraded into a `WebTransportSession`, and dispatched on the ThreadPool. Plain HTTP/3 routes and WT routes coexist on the same listener and port.
```cpp
std::unordered_map<std::string,
std::function<Crafter::HTTPResponse(const Crafter::HTTPRequest&)>> httpRoutes;
std::unordered_map<std::string,
std::function<void(Crafter::WebTransportSession&)>> wtRoutes;
wtRoutes["/echo"] = [](Crafter::WebTransportSession& session) {
session.OnStream([](Crafter::QUICStream peerStream) {
auto bytes = peerStream.RecieveUntilCloseSync();
peerStream.SendSync(bytes.data(),
static_cast<std::uint32_t>(bytes.size()),
/*finish=*/true);
});
};
Crafter::QUICServerCredentials creds;
creds.selfSigned = true; // dev-only, see "Self-signed certs & browser peers"
Crafter::ListenerAsyncHTTP listener(4443, creds, std::move(httpRoutes), std::move(wtRoutes));
```
Browser-initiated bidi/unidi WebTransport streams arrive via `session.OnStream(...)`. The `WT_BIDI` (`0x41`) / `WT_UNIDI` (`0x54`) stream-type prefix and session-id varint are stripped before the user handler sees the stream; what the handler reads is pure session payload. Phase 1 covers session establishment + bidirectional streams; WebTransport datagrams and capsule-protocol control are stubbed for a follow-up.
### Self-signed certs & browser peers
Passing `QUICServerCredentials{selfSigned = true}` makes the listener generate (and cache) an ephemeral cert at `/tmp/crafter-network-quic-cert/{cert,key}.pem` and reuse it across server restarts while it's still valid. The cert is shaped to satisfy Chromium's `WebTransport.serverCertificateHashes` rules: ECDSA P-256, signed with ECDSA-SHA256, validity ≤14 days (10 in practice), `BasicConstraints CA:FALSE`, `KeyUsage digitalSignature`, `EKU serverAuth`, `SAN=DNS:localhost,IP:127.0.0.1,IP:::1`. To let a browser peer pin this cert without trusting a CA:
```cpp
auto certPath = Crafter::GetSelfSignedCertificatePath();
auto hash = Crafter::ComputeCertificateHashSHA256(certPath); // 32-byte SHA-256 of cert DER
// Publish hash to the browser (e.g. write hex to a file the page can fetch);
// on the browser side feed it into QUICClientCredentials::serverCertificateHash.
```
For production use a real cert (`certPath` + `keyPath` on `QUICServerCredentials`).
## Browser build
Crafter.Network compiles for `wasm32-wasip1` via [Crafter.Build](https://forgejo.catcrafts.net/Catcrafts/Crafter.Build); the build path is selected automatically when `cfg.target.find("wasm") != npos`. On that target:
- `CRAFTER_NETWORK_BROWSER` is defined. Synchronous methods on `ClientHTTP` / `QUICStream` / `ClientQUIC` are not compiled — only the `*Async` variants are available.
- `ClientHTTP` calls into `crafterNetworkFetch` (JS) which delegates to `fetch()`. An empty `host` is a same-origin sentinel: the path is passed through as the URL, so `ClientHTTP("", 0).SendAsync({.path="/data.json"}, ...)` fetches from the page origin.
- `ClientQUIC` calls into `crafterNetworkWtConnect` which constructs a `WebTransport(url, opts)` against `https://{host}:{port}/{alpn}` (i.e. `alpn` is the WebTransport URL path on this target). `QUICClientCredentials::serverCertificateHash` is forwarded as `serverCertificateHashes`; leaving it zeroed makes the browser fall back to its normal trust store.
- `ListenerTCP`, `ListenerHTTP`, `ListenerQUIC`, `ClientTCP`, and the sync receive/send paths are excluded — the browser is client-only. So are `ClientHTTP1` / `ListenerHTTP1` / the `HTTP1` codec: a page cannot open a raw TCP socket anyway, and `fetch()` behind `ClientHTTP` already negotiates whatever HTTP version the server offers.
- `additional/network-env.js` is shipped alongside the produced `.wasm` and merged into the runtime's `env` import object by `EnableWasiBrowserRuntime`.
A worked example pairing a wasm browser client with a native server lives in [examples/SimpleClient/](examples/SimpleClient/). Build the server with `crafter-build --target=x86_64-pc-linux-gnu`, run it, then run `crafter-build` (no `--target`) to produce the wasm and serve it over HTTPS.
## Build Configuration
The project is a single Crafter.Build configuration (`crafter-network`, `ConfigurationType::LibraryStatic`). Target selection and debug flags are handled by `ApplyStandardArgs`:
- `crafter-build` — host native (x86_64-pc-linux-gnu by default), msquic + listeners + sync APIs.
- `crafter-build --target=wasm32-wasip1` — browser build, fetch + WebTransport, async-only API; defines `CRAFTER_NETWORK_BROWSER`, drops msquic.
- `crafter-build test [globs]` — build and run tests under `tests/`.
## Testing
The library includes tests covering:
- HTTP/3 round-trip (`ShouldSendRecieveHTTP`) — canonical local client/server round-trip
- HTTP/3 connection multiplexing (`ShouldSendRecieveKeepaliveHTTP`) — two requests share one QUIC connection
- HTTP/3 large body transfer (`ShouldSendRecieveLargeHTTP`) — 10 MiB POST
- HTTP/3 external interop (`ShouldSend`) — live fetch from `cloudflare-quic.com:443`, exercises real TLS chain validation, mandatory control stream, peer-initiated unidi streams, and QPACK Huffman decoding
- QUIC reliable streams (`ShouldSendRecieveQUICStream`)
- QUIC unreliable datagrams (`ShouldSendRecieveQUICDatagram`)
- WebTransport echo (`ShouldEchoWebTransport`) — extended-CONNECT acceptance, draft-02 SETTINGS, bidi data stream framing (`WT_STREAM 0x41` + session-id varint), and byte-for-byte echo
- HTTP/1.1 wire format (`ShouldParseHTTP1`) — serialisation, incremental parsing fed one byte at a time, chunked bodies with trailers, pipelining, interim 1xx, HTTP/1.0 and HEAD framing, and the malformed inputs the parser is required to reject
- HTTP/1.1 round-trip (`ShouldSendRecieveHTTP1`) — routing, query strings, HEAD, 404 and a throwing handler
- HTTP/1.1 keep-alive (`ShouldSendRecieveKeepaliveHTTP1`) — connection reuse, handler-requested close, and recovery from a pooled connection the server closed
- HTTP/1.1 large body transfer (`ShouldSendRecieveLargeHTTP1`) — 10 MiB in both directions on one connection
- HTTP/1.1 interop (`ShouldInteropCurlHTTP1`) — `curl` against `ListenerHTTP1` (keep-alive reuse, chunked upload, `Expect: 100-continue`, HEAD) and `ClientHTTP1` against python3's `http.server`, which answers HTTP/1.0 with `Connection: close`
- HTTP/1.1 under abuse (`ShouldSurviveAbuseHTTP1`) — 24 concurrent keep-alive clients, peers that vanish mid-request or send garbage, and a stalled peer that must be timed out
The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. `ShouldInteropCurlHTTP1` skips whichever half is unavailable when `curl` or `python3` is not installed, so it passes on a bare machine — install both to actually exercise it.
## Dependencies
- **Crafter.Thread**: Thread pool management for asynchronous operations.
- **msquic** (native target only) — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` recursively into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=quictls`, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC and HTTP/3 modules. Skipped entirely on browser builds.
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`. The same path also produces the cert hash that browser peers need for `serverCertificateHashes`.
- The HTTP/1.1 stack has no dependencies beyond POSIX sockets — no TLS library, no msquic.
- **Browser build** has no extra dependencies beyond Crafter.Build's `wasi-browser` runtime: HTTP delegates to the browser's `fetch()`, QUIC to its `WebTransport`. The JS glue lives in `additional/network-env.js` and is shipped alongside the produced `.wasm`.
## Usage Example
```cpp
#include <Crafter.Network>
#include <iostream>
int main() {
Crafter::QUICClientCredentials creds;
creds.insecureNoServerValidation = true;
Crafter::ClientHTTP client("localhost", 8443, creds);
auto response = client.Send(Crafter::CreateRequestHTTP("GET", "/", "localhost"));
std::cout << "Status: " << response.status << std::endl;
std::cout << "Body: " << response.body << std::endl;
return 0;
}
```
## License
This library is licensed under the GNU Lesser General Public License, version 3 only (LGPL-3.0-only). See [LICENSE](LICENSE) for the license text. The LGPL supplements the GNU General Public License version 3, a copy of which is included as [LICENSE.GPL](LICENSE.GPL).
The example code under [examples/](examples/) is licensed under the MIT license (see [examples/SimpleClient/LICENSE](examples/SimpleClient/LICENSE)), so it can be freely copied into your own projects.
## Copyright
Copyright (C) 2026 Catcrafts®
Catcrafts.net