diff --git a/README.md b/README.md index 608966d..efaf1e7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Crafter.Network is a C++ networking library designed for modern C++ applications - **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). +- **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 — including the `fallback` hook for paths that cannot be enumerated — 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 pool–based 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). @@ -28,7 +28,7 @@ The library follows a modular design using C++20 modules: - `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:HTTP`: HTTP request/response types, constructors, and `PathWithoutQueryHTTP`, 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` @@ -91,7 +91,7 @@ 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. +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` and then on the query-stripped path; anything still unmatched goes to [`fallback`](#routes-that-cannot-be-enumerated) if one is set and returns a synthetic 404 otherwise. ### HTTP/1.1 Components @@ -121,7 +121,7 @@ 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. +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 (or `fallback`, see [Routes that cannot be enumerated](#routes-that-cannot-be-enumerated)); 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. @@ -145,6 +145,27 @@ if (parser.Complete()) { } ``` +### Routes that cannot be enumerated + +The route map only answers paths known when the listener is built. `/shop/`, `/order/`, `/posts/` cannot be pre-registered — the token space is unbounded and the product set changes while the server runs. Both listeners therefore take an optional `fallback`, called for any request the route map missed, with the full target still in `request.path`: + +```cpp +auto router = [](const Crafter::HTTPRequest& request) { + auto path = Crafter::PathWithoutQueryHTTP(request.path); // everything up to '?' + if (path.starts_with("/shop/")) return RenderProduct(path.substr(6)); + return Crafter::CreateResponseHTTP("404", "Not Found"); +}; + +Crafter::ListenerAsyncHTTP1 listener(8080, std::move(routes), router); // HTTP/1.1 +Crafter::ListenerAsyncHTTP quic(4443, creds, std::move(routes), router); // same handler over HTTP/3 +``` + +Dispatch precedence is the same on both: exact `path`, then the query-stripped path, then `fallback`, then a synthetic 404. So a fallback only ever sees what the route map did not claim, and leaving it unset keeps the previous behaviour exactly. + +This is deliberately a hook rather than a pattern-matching syntax. Consumers that already have a router — one shared between a wasm frontend and the server, say, so a URL cannot mean different things to a crawler and to the app — keep using it, and there is no second route table to disagree with the first. A throwing fallback becomes a 500, like any other handler. On `ListenerHTTP` it applies to `routes` only; an unmatched WebTransport CONNECT is still a 404, since a WT handler has a different signature. + +`fallback` is a plain public member on `ListenerHTTP`/`ListenerHTTP1` and may be assigned before `Listen()`. The `ListenerAsync*` wrappers start accepting inside their constructor, so there they have to be passed as the trailing constructor argument shown above — assigning afterwards races the accept loop. + ### 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. @@ -220,6 +241,7 @@ The library includes tests covering: - 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 +- Fallback routing (`ShouldFallbackUnknownRoutes`) — one route map plus a `fallback` replayed over both `ListenerHTTP1` and `ListenerHTTP`, asserting identical answers: exact routes win, query strings still route to the bare path, the fallback sees the full target, a throwing fallback is a 500, and an unset fallback still means a synthetic 404 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.