diff --git a/README.md b/README.md index bd9fcdd..1076a2e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,19 @@ # Crafter.Network -A cross-platform C++ networking library providing TCP, QUIC, HTTP/3, and WebTransport client/server functionality with modern C++ features. Builds for native Linux and for the browser (wasm32-wasip1). +A cross-platform C++ networking library providing TCP and HTTP client/server functionality with modern C++ features. ## Overview -Crafter.Network is a C++ networking library designed for modern C++ applications. It provides TCP, QUIC, HTTP/3, 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). +Crafter.Network is a comprehensive networking library designed for modern C++ applications. It provides TCP, HTTP, and QUIC networking capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks including real-time multiplayer games. ## 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.). -- **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). -- **Cross-Platform**: Native Linux (POSIX sockets + msquic) and browser (wasm32-wasip1). -- **Modern C++**: Uses C++20 modules, STL containers, and modern C++ features. +- **TCP Networking**: Client and server implementations for TCP connections +- **HTTP Support**: Full HTTP client and server implementations with routing capabilities +- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync +- **Asynchronous Operations**: Thread pool-based async operations for improved performance +- **Cross-Platform**: Built for Unix-like systems with socket-based networking +- **Modern C++**: Uses C++ modules, STL containers, and modern C++ features ## Architecture @@ -23,15 +21,13 @@ 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 -- `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`). +- `Crafter.Network:ClientTCP`: TCP client implementation +- `Crafter.Network:ListenerTCP`: TCP server implementation +- `Crafter.Network:ClientHTTP`: HTTP client implementation +- `Crafter.Network:ListenerHTTP`: HTTP server implementation +- `Crafter.Network:HTTP`: HTTP protocol utilities and data structures +- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams +- `Crafter.Network:ListenerQUIC`: QUIC listener accepting incoming connections ## Components @@ -57,117 +53,54 @@ 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). +### HTTP Components #### ClientHTTP ```cpp -Crafter::QUICClientCredentials creds; -creds.insecureNoServerValidation = true; // dev-only -Crafter::ClientHTTP client("localhost", 8082, creds); +// Create an HTTP client +Crafter::ClientHTTP client("httpbin.org", 80); -Crafter::HTTPResponse response = client.Send( - Crafter::CreateRequestHTTP("GET", "/", "localhost") -); -// response.status is the numeric status as a string, e.g. "200" +// Send HTTP request +std::string request = Crafter::CreateRequestHTTP("GET", "/get", "httpbin.org"); +Crafter::HTTPResponse response = client.Send(request); ``` #### ListenerHTTP ```cpp -std::unordered_map> routes; -routes["/hello"] = [](const Crafter::HTTPRequest&) { - return Crafter::CreateResponseHTTP("200", "Hello World!"); +// Create an HTTP listener with routes +std::unordered_map> routes; +routes["/hello"] = [](const Crafter::HTTPRequest& req) { + return Crafter::CreateResponseHTTP("200 OK", "Hello World!"); }; -Crafter::QUICServerCredentials creds; -creds.selfSigned = true; // dev-only -Crafter::ListenerHTTP listener(8082, creds, routes); +Crafter::ListenerHTTP listener(8080, 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. - -### 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> httpRoutes; -std::unordered_map> wtRoutes; - -wtRoutes["/echo"] = [](Crafter::WebTransportSession& session) { - session.OnStream([](Crafter::QUICStream peerStream) { - auto bytes = peerStream.RecieveUntilCloseSync(); - peerStream.SendSync(bytes.data(), - static_cast(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. -- `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`: +The project uses a configuration system with multiple build targets: -- `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/`. +- **base**: Core interfaces only +- **lib**: Static library build with dependencies +- **lib-debug**: Debug static library build +- **lib-shared**: Shared library build with dependencies ## 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 - -The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. +The library includes comprehensive tests covering: +- Compilation verification +- HTTP receive functionality +- HTTP send functionality +- HTTP send/receive operations +- Keep-alive HTTP operations +- Large HTTP data transfers ## 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. +- Crafter.Thread: Thread pool management for asynchronous operations +- **msquic** — 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 modules. - 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`. -- **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 @@ -176,14 +109,15 @@ The external-interop test requires outbound UDP/443; if your network blocks it t #include int main() { - Crafter::QUICClientCredentials creds; - creds.insecureNoServerValidation = true; - Crafter::ClientHTTP client("localhost", 8443, creds); - - auto response = client.Send(Crafter::CreateRequestHTTP("GET", "/", "localhost")); - + // Simple HTTP client example + Crafter::ClientHTTP client("httpbin.org", 80); + + auto request = Crafter::CreateRequestHTTP("GET", "/get", "httpbin.org"); + auto response = client.Send(request); + std::cout << "Status: " << response.status << std::endl; std::cout << "Body: " << response.body << std::endl; + return 0; } ``` @@ -195,4 +129,4 @@ This library is licensed under the GNU Lesser General Public License version 3.0 ## Copyright Copyright (C) 2026 Catcrafts® -Catcrafts.net +Catcrafts.net \ No newline at end of file diff --git a/additional/network-env.js b/additional/network-env.js deleted file mode 100644 index 3545c77..0000000 --- a/additional/network-env.js +++ /dev/null @@ -1,329 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. -*/ - -// JS bridge for the CRAFTER_NETWORK_BROWSER build of Crafter.Network. -// Populates `window.crafter_webbuild_env` with the env imports the .wasm -// declares (crafterNetworkFetch, crafterNetworkWt*), and drives the -// reader loops that feed responses back into the wasm exports -// (CrafterNetworkOnFetchComplete, CrafterNetworkOnWt*). -// -// Crafter.Build's wasi-browser runtime merges this object into the -// WebAssembly import object as the `env` module before instantiation — -// mirrors additional/dom-env.js in Crafter.Graphics. - -const __decoder = new TextDecoder(); -const __encoder = new TextEncoder(); - -function __wasm() { return window.crafter_webbuild_wasi.instance.exports; } -function __memBuf() { return window.crafter_webbuild_wasi.instance.exports.memory.buffer; } - -function __readUtf8(ptr, len) { - return __decoder.decode(new Uint8Array(__memBuf(), ptr, len)); -} -function __readBytes(ptr, len) { - // Copy out of the linear memory — the underlying buffer is detached - // whenever wasm grows its memory, so handing the view straight back - // to fetch / WebTransport would risk operating on a freed view. - return new Uint8Array(__memBuf(), ptr, len).slice(); -} -function __allocCopy(bytes) { - const ptr = __wasm().WasmAlloc(bytes.length); - new Uint8Array(__memBuf(), ptr, bytes.length).set(bytes); - return ptr; -} -function __allocUtf8(str) { - return __allocCopy(__encoder.encode(str)); -} - -// ─── fetch() bridge ─────────────────────────────────────────────────── - -function crafterNetworkFetch(methodPtr, methodLen, - urlPtr, urlLen, - headersPtr, headersLen, - bodyPtr, bodyLen, - callbackId) { - const method = __readUtf8(methodPtr, methodLen); - const url = __readUtf8(urlPtr, urlLen); - const init = { method, headers: {} }; - - if (headersLen > 0) { - const headerStr = __readUtf8(headersPtr, headersLen); - for (const line of headerStr.split('\n')) { - const sep = line.indexOf(': '); - if (sep > 0) init.headers[line.slice(0, sep)] = line.slice(sep + 2); - } - } - if (bodyLen > 0 && method !== 'GET' && method !== 'HEAD') { - init.body = __readBytes(bodyPtr, bodyLen); - } - - fetch(url, init).then(async (response) => { - // Lowercase to match HTTP/3 convention the native client uses. - const headerLines = []; - response.headers.forEach((value, name) => headerLines.push(`${name.toLowerCase()}: ${value}`)); - const headerStr = headerLines.join('\n'); - const bodyBuf = await response.arrayBuffer(); - - const headerEncoded = __encoder.encode(headerStr); - const headerPtr = headerEncoded.length > 0 ? __allocCopy(headerEncoded) : 0; - const respBodyPtr = bodyBuf.byteLength > 0 ? __allocCopy(new Uint8Array(bodyBuf)) : 0; - __wasm().CrafterNetworkOnFetchComplete(callbackId, - response.status, - headerPtr, headerEncoded.length, - respBodyPtr, bodyBuf.byteLength); - }).catch((err) => { - const msg = String(err && err.message ? err.message : err); - const encoded = __encoder.encode(msg); - const ptr = encoded.length > 0 ? __allocCopy(encoded) : 0; - __wasm().CrafterNetworkOnFetchError(callbackId, ptr, encoded.length); - }); -} - -// ─── WebTransport bridge ────────────────────────────────────────────── -// -// Connection-handle and stream-handle counters are monotone. Each -// connection's incoming-stream + datagram reader loops are started as -// soon as wt.ready resolves and run until the session closes. Outgoing -// operations queued before ready are gated behind a per-handle `ready` -// promise. - -let __wtNextHandle = 0; -let __wtNextStream = 0; -const __wtSessions = new Map(); // handle → { wt, ready, streams: Set } -const __wtStreams = new Map(); // streamId → { connection, writer, reader, writable, readable } - -function crafterNetworkWtConnect(hostPtr, hostLen, port, alpnPtr, alpnLen, certHashPtr, certHashLen) { - const host = __readUtf8(hostPtr, hostLen); - const alpn = __readUtf8(alpnPtr, alpnLen); - const url = `https://${host}:${port}/${alpn}`; - const opts = {}; - if (certHashLen > 0) { - const hash = __readBytes(certHashPtr, certHashLen); - opts.serverCertificateHashes = [{ algorithm: 'sha-256', value: hash }]; - } - - let wt; - try { - wt = new WebTransport(url, opts); - } catch (err) { - console.error('Crafter.Network: WebTransport ctor failed:', err); - return 0; - } - - const handle = ++__wtNextHandle; - const session = { wt, ready: false, streams: new Set(), closed: false }; - __wtSessions.set(handle, session); - - wt.ready.then(() => { - session.ready = true; - __wasm().CrafterNetworkOnWtReady(handle); - __wtRunIncomingLoop(handle, wt.incomingBidirectionalStreams, /*bidi=*/true); - __wtRunIncomingLoop(handle, wt.incomingUnidirectionalStreams, /*bidi=*/false); - __wtRunDatagramLoop(handle, wt); - }).catch((err) => { - __wtFireClosed(handle, String(err && err.message ? err.message : err)); - }); - - wt.closed.then(() => { - __wtFireClosed(handle, ''); - }).catch((err) => { - __wtFireClosed(handle, String(err && err.message ? err.message : err)); - }); - - return handle; -} - -function __wtFireClosed(handle, message) { - const session = __wtSessions.get(handle); - if (!session || session.closed) return; - session.closed = true; - // Wake up any per-stream receivers with a synthetic FIN so the C++ - // state machine terminates pending callbacks instead of hanging. - for (const streamId of session.streams) { - __wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1); - __wtStreams.delete(streamId); - } - session.streams.clear(); - const msgPtr = message ? __allocUtf8(message) : 0; - __wasm().CrafterNetworkOnWtClosed(handle, msgPtr, message ? __encoder.encode(message).length : 0); -} - -function crafterNetworkWtClose(handle) { - const session = __wtSessions.get(handle); - if (!session) return; - try { session.wt.close(); } catch (_) { /* already closing */ } - __wtSessions.delete(handle); -} - -function crafterNetworkWtOpenStream(handle, unidirectional) { - const session = __wtSessions.get(handle); - if (!session || session.closed) return 0; - const streamId = ++__wtNextStream; - const record = { connection: handle, writer: null, reader: null, - pendingOps: [], opened: false, unidirectional: !!unidirectional }; - __wtStreams.set(streamId, record); - session.streams.add(streamId); - - const opener = session.ready - ? Promise.resolve() - : session.wt.ready; - opener.then(() => { - const p = unidirectional - ? session.wt.createUnidirectionalStream() - : session.wt.createBidirectionalStream(); - return p; - }).then((stream) => { - // For a bidi stream `stream` is a WebTransportBidirectionalStream - // with .readable and .writable. For a unidi outgoing stream - // `stream` is itself a WritableStream. - if (unidirectional) { - record.writer = stream.getWriter(); - } else { - record.writer = stream.writable.getWriter(); - record.reader = stream.readable.getReader(); - __wtRunStreamReader(streamId); - } - record.opened = true; - for (const op of record.pendingOps) op(); - record.pendingOps.length = 0; - }).catch((err) => { - console.error(`Crafter.Network: openStream(${streamId}) failed`, err); - // Tell C++ the stream is dead so pending receivers unblock. - __wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1); - __wtStreams.delete(streamId); - session.streams.delete(streamId); - }); - - return streamId; -} - -function crafterNetworkWtStreamWrite(streamId, bufPtr, bufLen, finish, callbackId) { - const record = __wtStreams.get(streamId); - if (!record) { - if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); - return; - } - const data = bufLen > 0 ? __readBytes(bufPtr, bufLen) : new Uint8Array(0); - const doWrite = () => { - const p = data.length > 0 ? record.writer.write(data) : Promise.resolve(); - p.then(() => { - if (finish) { try { return record.writer.close(); } catch (_) {} } - }).then(() => { - if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); - }).catch((err) => { - console.error(`Crafter.Network: write(${streamId}) failed`, err); - if (callbackId !== 0) __wasm().CrafterNetworkOnWtStreamWriteComplete(callbackId); - }); - }; - if (record.opened) doWrite(); - else record.pendingOps.push(doWrite); -} - -function crafterNetworkWtStreamStop(streamId) { - const record = __wtStreams.get(streamId); - if (!record) return; - try { record.writer && record.writer.close(); } catch (_) {} - try { record.reader && record.reader.cancel(); } catch (_) {} - const session = __wtSessions.get(record.connection); - if (session) session.streams.delete(streamId); - __wtStreams.delete(streamId); -} - -function crafterNetworkWtSendDatagram(handle, bufPtr, bufLen) { - const session = __wtSessions.get(handle); - if (!session || session.closed) return; - const data = __readBytes(bufPtr, bufLen); - const send = () => { - const writer = session.wt.datagrams.writable.getWriter(); - writer.write(data).catch((err) => { - console.error('Crafter.Network: sendDatagram failed', err); - }).finally(() => { - try { writer.releaseLock(); } catch (_) {} - }); - }; - if (session.ready) send(); - else session.wt.ready.then(send).catch(() => {}); -} - -async function __wtRunStreamReader(streamId) { - const record = __wtStreams.get(streamId); - if (!record || !record.reader) return; - try { - while (true) { - const { value, done } = await record.reader.read(); - if (value && value.byteLength > 0) { - const ptr = __allocCopy(value); - __wasm().CrafterNetworkOnWtStreamChunk(streamId, ptr, value.byteLength, done ? 1 : 0); - } else if (done) { - __wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1); - } - if (done) break; - } - } catch (err) { - // Reader cancelled or stream closed. Dispatch a synthetic FIN so - // any pending C++ receiver wakes up. - __wasm().CrafterNetworkOnWtStreamChunk(streamId, 0, 0, 1); - } -} - -async function __wtRunIncomingLoop(handle, source, bidi) { - const session = __wtSessions.get(handle); - if (!session) return; - const reader = source.getReader(); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) break; - const streamId = ++__wtNextStream; - const record = { connection: handle, writer: null, reader: null, - pendingOps: [], opened: true, unidirectional: !bidi }; - if (bidi) { - record.writer = value.writable.getWriter(); - record.reader = value.readable.getReader(); - } else { - record.reader = value.getReader(); - } - __wtStreams.set(streamId, record); - session.streams.add(streamId); - __wasm().CrafterNetworkOnWtIncomingStream(handle, streamId, bidi ? 1 : 0); - __wtRunStreamReader(streamId); - } - } catch (_) { /* session closed */ } -} - -async function __wtRunDatagramLoop(handle, wt) { - const reader = wt.datagrams.readable.getReader(); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) break; - if (value && value.byteLength > 0) { - const ptr = __allocCopy(value); - __wasm().CrafterNetworkOnWtDatagram(handle, ptr, value.byteLength); - } - } - } catch (_) { /* session closed */ } -} - -// ─── Export env object ──────────────────────────────────────────────── - -if (!window.crafter_webbuild_env) { - window.crafter_webbuild_env = {}; -} -Object.assign(window.crafter_webbuild_env, { - crafterNetworkFetch, - crafterNetworkWtConnect, - crafterNetworkWtClose, - crafterNetworkWtOpenStream, - crafterNetworkWtStreamWrite, - crafterNetworkWtStreamStop, - crafterNetworkWtSendDatagram, -}); diff --git a/examples/SimpleClient/cert-hash.txt b/examples/SimpleClient/cert-hash.txt deleted file mode 100644 index 3363252..0000000 --- a/examples/SimpleClient/cert-hash.txt +++ /dev/null @@ -1 +0,0 @@ -b7c4a81084fc56f45f1a6025fafdc8a1b05bf8388947f1840608da565cd22c8e \ No newline at end of file diff --git a/examples/SimpleClient/main.cpp b/examples/SimpleClient/main.cpp deleted file mode 100644 index 3414cd4..0000000 --- a/examples/SimpleClient/main.cpp +++ /dev/null @@ -1,227 +0,0 @@ -// Crafter.Network SimpleClient example. -// -// Browser build (wasm32-wasip1, default): -// crafter-build -// Serve bin// on a static HTTPS server, open index.html -// in Chrome and watch DevTools → Console. -// -// Native server build (e.g. x86_64-pc-linux-gnu): -// crafter-build --target=x86_64-pc-linux-gnu -// Runs a WebTransport echo server on port 4443 that the browser demo -// connects to at wt://localhost:4443/echo. - -import Crafter.Network; -import std; -#ifndef CRAFTER_NETWORK_BROWSER -import Crafter.Thread; -#include -#include -#endif - -using namespace Crafter; - -namespace { - -#ifdef CRAFTER_NETWORK_BROWSER - void RunFetchDemo() { - // httpbin.org sets Access-Control-Allow-Origin: * and returns a - // small JSON echo, so it works as a smoke test from any origin. - // Swap host/port/path for your own service when you have one. - std::println(std::cout, "[Crafter.Network] HTTP GET httpbin.org/get ..."); - - // Heap-allocated and intentionally leaked — fetch() resolves - // after main() returns and the JS event loop keeps the wasm - // instance alive. A real app would tie the lifetime to a session - // object that lives for as long as the page does. - auto* client = new ClientHTTP("httpbin.org", 443); - - HTTPRequest req; - req.method = "GET"; - req.path = "/get"; - - client->SendAsync(req, - [](HTTPResponse response) { - std::println(std::cout, - "[Crafter.Network] fetch OK — status {}, body {} bytes", - response.status, response.body.size()); - if (!response.body.empty()) { - auto preview = response.body.substr(0, std::min(80, response.body.size())); - std::println(std::cout, "[Crafter.Network] body[0..80]: {}", preview); - } - }, - [](std::string error) { - std::println(std::cerr, "[Crafter.Network] fetch ERROR: {}", error); - }); - } - - // Parse 64-char lowercase hex into a 32-byte digest. Returns all-zero - // bytes (treated as "no hash") if the input is malformed. - std::array ParseHexHash(std::string_view hex) { - std::array out{}; - if (hex.size() < 64) return out; - auto nibble = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); - if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); - return -1; - }; - for (std::size_t i = 0; i < 32; ++i) { - int hi = nibble(hex[2 * i]); - int lo = nibble(hex[2 * i + 1]); - if (hi < 0 || lo < 0) return std::array{}; - out[i] = static_cast((hi << 4) | lo); - } - return out; - } - - void StartWebTransportEcho(std::array certHash) { - const char* wtHost = "localhost"; - std::uint16_t wtPort = 4443; - const char* wtPath = "echo"; - std::println(std::cout, "[Crafter.Network] WebTransport connect {}:{}/{} ...", wtHost, wtPort, wtPath); - - QUICClientCredentials creds; - creds.serverCertificateHash = certHash; - auto* conn = new ClientQUIC(wtHost, wtPort, wtPath, creds); - - conn->OnDatagram([](std::vector bytes) { - std::string text(bytes.begin(), bytes.end()); - std::println(std::cout, "[Crafter.Network] WT datagram echo: {} ({} bytes)", text, bytes.size()); - }); - - // Leak the stream so its handle survives past this function's - // return. Read fires after the echo server has sent the bytes - // back, which happens after the JS event loop runs. finish=true - // closes the send-side so the server's RecieveUntilCloseSync - // returns and the echo handler runs. - static QUICStream s = conn->OpenStream(/*unidirectional=*/false); - constexpr std::string_view payload = "hello from crafter.network"; - s.SendAsync(payload.data(), static_cast(payload.size()), /*finish=*/true, []{ - std::println(std::cout, "[Crafter.Network] WT stream write OK"); - s.RecieveUntilCloseAsync([](std::vector bytes) { - std::string text(bytes.begin(), bytes.end()); - std::println(std::cout, "[Crafter.Network] WT stream echo: {} ({} bytes)", text, bytes.size()); - }); - }); - } - - void RunWebTransportDemo() { - // Chrome refuses self-signed WebTransport certs unless we pass their - // SHA-256 via `serverCertificateHashes`. Our native server writes the - // hex digest to ./cert-hash.txt; we fetch it from the same origin as - // this wasm (`ClientHTTP("", 0)` is the same-origin sentinel). Serve - // the wasm from the directory the server is running in so the file - // is reachable, then refresh. - static ClientHTTP origin("", 0); - HTTPRequest req; - req.method = "GET"; - req.path = "/cert-hash.txt"; - origin.SendAsync(req, - [](HTTPResponse resp) { - if (resp.status != "200") { - std::println(std::cerr, - "[Crafter.Network] /cert-hash.txt → HTTP {} — start the native server " - "from this directory so the hash file is reachable", - resp.status); - return; - } - auto hash = ParseHexHash(resp.body); - bool zero = true; - for (auto b : hash) if (b) { zero = false; break; } - if (zero) { - std::println(std::cerr, - "[Crafter.Network] /cert-hash.txt is empty or malformed; expected 64 hex chars"); - return; - } - std::println(std::cout, "[Crafter.Network] using cert hash from /cert-hash.txt"); - StartWebTransportEcho(hash); - }, - [](std::string err) { - std::println(std::cerr, - "[Crafter.Network] could not fetch /cert-hash.txt: {} — is the static server " - "serving the directory the native server runs in?", err); - }); - } - -#else // native server - - static std::atomic gRunning{true}; - - void RunEchoServer() { - ThreadPool::Start(); - - QUICServerCredentials creds; - creds.selfSigned = true; - - // Compute the SHA-256 of the self-signed cert so the browser peer - // can pass it via WebTransport's serverCertificateHashes option. - // Chrome won't accept self-signed certs without this. We also write - // the hash hex to ./cert-hash.txt alongside the binary so a static - // file server serving the wasm can hand it back to the page. - std::string certPath = GetSelfSignedCertificatePath(); - auto hash = ComputeCertificateHashSHA256(certPath); - std::string hashHex; - for (auto b : hash) { - constexpr char hex[] = "0123456789abcdef"; - hashHex.push_back(hex[b >> 4]); - hashHex.push_back(hex[b & 0xf]); - } - std::ofstream("cert-hash.txt") << hashHex; - - std::unordered_map> httpRoutes = { - {"/health", [](const HTTPRequest&) { - HTTPResponse r; - r.status = "200"; - r.body = "ok"; - return r; - }}, - }; - std::unordered_map> wtRoutes = { - {"/echo", [](WebTransportSession& session) { - session.OnStream([](QUICStream stream) { - try { - auto bytes = stream.RecieveUntilCloseSync(); - stream.SendSync(bytes.data(), - static_cast(bytes.size()), - /*finish=*/true); - } catch (...) {} - }); - }}, - }; - - ListenerAsyncHTTP server(4443, creds, std::move(httpRoutes), std::move(wtRoutes)); - std::println(std::cout, "[Crafter.Network] echo server listening on port 4443"); - std::println(std::cout, "[Crafter.Network] WebTransport /echo — bidi streams echoed back"); - std::println(std::cout, "[Crafter.Network] HTTP GET /health — returns 200 ok"); - std::println(std::cout, "[Crafter.Network] cert SHA-256: {}", hashHex); - std::println(std::cout, "[Crafter.Network] (also written to ./cert-hash.txt for the browser to fetch)"); - std::println(std::cout, "[Crafter.Network] Press Ctrl-C to stop"); - - std::signal(SIGINT, [](int) { gRunning = false; }); - std::signal(SIGTERM, [](int) { gRunning = false; }); - while (gRunning) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - std::println(std::cout, "[Crafter.Network] shutting down"); - server.Stop(); - } - -#endif - -} // namespace - -int main() { -#ifdef CRAFTER_NETWORK_BROWSER - // Full-buffer stdout means async-callback prints never reach the - // console after main() returns. unitbuf flushes after every insert - // so logs show up live. - std::cout << std::unitbuf; - std::cerr << std::unitbuf; - std::println(std::cout, "[Crafter.Network] SimpleClient starting (browser)"); - RunFetchDemo(); - RunWebTransportDemo(); - std::println(std::cout, "[Crafter.Network] main() returning (async work continues in JS event loop)"); -#else - RunEchoServer(); -#endif - return 0; -} diff --git a/examples/SimpleClient/project.cpp b/examples/SimpleClient/project.cpp deleted file mode 100644 index 5eac1a6..0000000 --- a/examples/SimpleClient/project.cpp +++ /dev/null @@ -1,79 +0,0 @@ -import std; -import Crafter.Build; -namespace fs = std::filesystem; -using namespace Crafter; - -extern "C" Configuration CrafterBuildProject(std::span args) { - // Dispatch on --target: wasm32-* → browser client; anything else → native server. - // Plain `crafter-build` (no --target) defaults to the browser client so the - // primary use case keeps working without flags. - bool isBrowser = true; - for (const auto& a : args) { - if (a.starts_with("--target=")) { - isBrowser = (a.find("wasm") != std::string_view::npos); - break; - } - } - - std::vector netArgs(args.begin(), args.end()); - - if (isBrowser) { - // Force wasm32-wasip1 when no --target was supplied. - bool hasTarget = false; - for (const auto& a : netArgs) { - if (a.starts_with("--target=")) { hasTarget = true; break; } - } - if (!hasTarget) netArgs.emplace_back("--target=wasm32-wasip1"); - - Configuration* network = LocalProject({ - .projectFile = "../../project.cpp", - .args = netArgs, - }); - - Configuration cfg; - cfg.path = "./"; - cfg.name = "SimpleClient"; - cfg.outputName = "SimpleClient"; - cfg.type = ConfigurationType::Executable; - cfg.target = "wasm32-wasip1"; - // Mirror CRAFTER_NETWORK_BROWSER so main.cpp sees the same API surface - // as the wasm .pcm (Crafter.Build does not propagate defines from deps). - cfg.defines.push_back({"CRAFTER_NETWORK_BROWSER", ""}); - ApplyStandardArgs(cfg, args); - cfg.dependencies = { network }; - cfg.files = { "cert-hash.txt" }; - - std::array ifaces = {}; - std::array impls = { "main" }; - cfg.GetInterfacesAndImplementations(ifaces, impls); - - // Crafter.Network ships additional/network-env.js via cfg.files; - // Crafter.Build propagates it to the output dir alongside the .wasm. - // EnableWasiBrowserRuntime wires it into the generated index.html. - EnableWasiBrowserRuntime(cfg); - - return cfg; - } - - // Native server — echo server on port 4443. - Configuration* network = LocalProject({ - .projectFile = "../../project.cpp", - .args = netArgs, - }); - - Configuration cfg; - cfg.path = "./"; - cfg.name = "SimpleServer"; - cfg.outputName = "SimpleServer"; - cfg.type = ConfigurationType::Executable; - ApplyStandardArgs(cfg, args); - cfg.dependencies = { network }; - cfg.linkFlags.push_back("-Wl,--export-dynamic"); - cfg.linkFlags.push_back("-ldl"); - - std::array ifaces = {}; - std::array impls = { "main" }; - cfg.GetInterfacesAndImplementations(ifaces, impls); - - return cfg; -} diff --git a/implementations/Crafter.Network-ClientHTTP-Browser.cpp b/implementations/Crafter.Network-ClientHTTP-Browser.cpp deleted file mode 100644 index ca14bea..0000000 --- a/implementations/Crafter.Network-ClientHTTP-Browser.cpp +++ /dev/null @@ -1,199 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -// CRAFTER_NETWORK_BROWSER implementation of ClientHTTP. Each SendAsync -// hands its request to the JS bridge (additional/network-env.js), which -// runs a fetch() and dispatches the result back through the -// CrafterNetworkOnFetchComplete / CrafterNetworkOnFetchError wasm exports. - -module; -module Crafter.Network:ClientHTTP_impl; -import :ClientHTTP; -import :HTTP; -import std; - -using namespace Crafter; - -namespace Crafter::NetworkBrowserBindings { - // External linkage so the import_module/import_name attributes wire up. - __attribute__((import_module("env"), import_name("crafterNetworkFetch"))) - void crafterNetworkFetch( - const char* method, std::int32_t methodLen, - const char* url, std::int32_t urlLen, - const char* headers, std::int32_t headersLen, - const char* body, std::int32_t bodyLen, - std::int32_t callbackId); -} - -namespace { - struct FetchCallbacks { - std::function onSuccess; - std::function onError; - }; - - // JS dispatches back into wasm via a stable id we mint here. The id - // counter is monotone — wraparound at 2 billion fetches is not a - // realistic concern. The map is touched only from the JS event loop - // (single-threaded in the browser), so no synchronisation is needed. - std::unordered_map& Callbacks() { - static std::unordered_map m; - return m; - } - std::int32_t NextId() { - static std::int32_t counter = 0; - return ++counter; - } - - // Serialise headers as newline-separated "name: value" pairs. The JS - // side splits on '\n' and the first ": " for header construction. - std::string SerialiseHeaders(const std::unordered_map& headers) { - std::string out; - bool first = true; - for (const auto& [name, value] : headers) { - if (!first) out += '\n'; - first = false; - out += name; - out += ": "; - out += value; - } - return out; - } - - // Parse a "name: value\nname2: value2" blob into the HTTPResponse map. - // Names are kept verbatim (fetch surfaces them lowercase already on the - // browser side via response.headers.forEach). - void ParseHeaders(std::string_view raw, HTTPResponse& response) { - std::size_t pos = 0; - while (pos < raw.size()) { - std::size_t end = raw.find('\n', pos); - std::string_view line = raw.substr(pos, end == std::string_view::npos ? raw.size() - pos : end - pos); - std::size_t sep = line.find(": "); - if (sep != std::string_view::npos) { - response.headers.emplace(std::string(line.substr(0, sep)), - std::string(line.substr(sep + 2))); - } - if (end == std::string_view::npos) break; - pos = end + 1; - } - } -} - -struct ClientHTTP::Impl {}; - -ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials) - : host(host), port(port), impl(std::make_unique()) {} - -ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds) - : ClientHTTP(host.c_str(), port, std::move(creds)) {} - -ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default; -ClientHTTP::~ClientHTTP() = default; - -void ClientHTTP::SendAsync(const HTTPRequest& request, - std::function onSuccess, - std::function onError) { - std::int32_t id = NextId(); - Callbacks().emplace(id, FetchCallbacks{std::move(onSuccess), std::move(onError)}); - - std::string method = request.method.empty() ? std::string("GET") : request.method; - std::string path = request.path.empty() ? std::string("/") : request.path; - // Sentinel: a ClientHTTP constructed with an empty host fetches against - // the page's own origin. fetch(url) in JS handles a leading-slash path - // by resolving it against window.location, so we just hand the path - // straight through. Useful for fetching files served by whatever static - // server is hosting the wasm (e.g. ./cert-hash.txt for WebTransport). - std::string url; - if (host.empty()) { - url = path; - } else { - std::string scheme = request.scheme.empty() ? std::string("https") : request.scheme; - std::string authority = request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority; - url = scheme + "://" + authority + path; - } - std::string headerStr = SerialiseHeaders(request.headers); - - Crafter::NetworkBrowserBindings::crafterNetworkFetch( - method.data(), static_cast(method.size()), - url.data(), static_cast(url.size()), - headerStr.data(), static_cast(headerStr.size()), - request.body.data(), static_cast(request.body.size()), - id); -} - -extern "C" { - // JS allocates `headersPtr` and `bodyPtr` via WasmAlloc, copies the - // response into them, then transfers ownership across this call. We - // free the buffers after copying into the HTTPResponse. - __attribute__((export_name("CrafterNetworkOnFetchComplete"))) - void CrafterNetworkOnFetchComplete(std::int32_t callbackId, - std::int32_t status, - char* headersPtr, std::int32_t headersLen, - char* bodyPtr, std::int32_t bodyLen) { - auto& callbacks = Callbacks(); - auto it = callbacks.find(callbackId); - if (it == callbacks.end()) { - std::free(headersPtr); - std::free(bodyPtr); - return; - } - - HTTPResponse response; - response.status = std::to_string(status); - if (headersPtr && headersLen > 0) { - ParseHeaders(std::string_view(headersPtr, static_cast(headersLen)), response); - } - if (bodyPtr && bodyLen > 0) { - response.body.assign(bodyPtr, static_cast(bodyLen)); - } - std::free(headersPtr); - std::free(bodyPtr); - - auto onSuccess = std::move(it->second.onSuccess); - callbacks.erase(it); - if (onSuccess) onSuccess(std::move(response)); - } - - __attribute__((export_name("CrafterNetworkOnFetchError"))) - void CrafterNetworkOnFetchError(std::int32_t callbackId, - char* messagePtr, std::int32_t messageLen) { - auto& callbacks = Callbacks(); - auto it = callbacks.find(callbackId); - if (it == callbacks.end()) { - std::free(messagePtr); - return; - } - std::string msg(messagePtr ? messagePtr : "", static_cast(messageLen)); - std::free(messagePtr); - - auto onError = std::move(it->second.onError); - callbacks.erase(it); - if (onError) onError(std::move(msg)); - } - - // WasmAlloc / WasmFree are the buffer-marshalling primitives the JS - // bridge calls into. Crafter.Graphics's Dom backend defines the same - // pair; we mark ours weak so the two libraries can coexist in one - // executable without a duplicate-symbol error. - __attribute__((export_name("WasmAlloc"), weak)) - void* WasmAlloc(std::int32_t size) { return std::malloc(static_cast(size)); } - - __attribute__((export_name("WasmFree"), weak)) - void WasmFree(void* ptr) { std::free(ptr); } -} diff --git a/implementations/Crafter.Network-ClientHTTP.cpp b/implementations/Crafter.Network-ClientHTTP.cpp index 6e4ea70..a470cf6 100644 --- a/implementations/Crafter.Network-ClientHTTP.cpp +++ b/implementations/Crafter.Network-ClientHTTP.cpp @@ -19,168 +19,190 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + module Crafter.Network:ClientHTTP_impl; import :ClientHTTP; -import :ClientQUIC; -import :HTTP; -import :HTTP3; import Crafter.Thread; import std; using namespace Crafter; -struct ClientHTTP::Impl { - ClientQUIC quic; - // Outgoing control stream — RFC 9114 §6.2.1: each peer MUST open a - // unidirectional control stream and send a SETTINGS frame as its first - // frame. Most real h3 servers (cloudflare, nghttp3, lsquic, …) close - // the connection with H3_MISSING_SETTINGS if we don't. The stream - // stays open for the lifetime of the connection; we never FIN it. - QUICStream controlStream; +ClientHTTP::ClientHTTP(const char* host, std::uint16_t port): host(host), port(port), client(host, port) { - Impl(const char* host, std::uint16_t port, QUICClientCredentials creds) - : quic(host, port, std::string(HTTP3::kAlpn), creds) { - // Drain any unidi streams the server opens to us (its control - // stream + optional QPACK encoder/decoder streams). We don't act - // on the contents — SETTINGS we accept by defaults, dynamic-table - // mutations we discard since we operate with no dynamic table. - // Any bidi stream from the server would be a server push, which - // we don't support — best-effort drain it as well. - quic.OnStream([](QUICStream stream) { - try { - while (true) (void)stream.RecieveSync(); - } catch (...) { - // Stream / connection closed. Done. +} + +ClientHTTP::ClientHTTP(std::string host, std::uint16_t port): ClientHTTP(host.c_str(), port) { + +} + +HTTPResponse ClientHTTP::Send(const char* request, std::uint32_t length) { + std::cout << "Send started" << std::endl; + client.Send(request, length); + std::cout << "Send Complete" << std::endl; + std::vector buffer; + HTTPResponse response; + std::uint32_t i = 0; + std::uint32_t statusStart = 0; + while(true) { + try { + buffer = client.RecieveSync(); + std::cout << "Recieved: " << buffer.size() << std::endl; + } catch(const SocketClosedException& e) { + std::cout << "Retry" << std::endl; + client.Stop(); + client.Connect(); + client.Send(request, length); + buffer = client.RecieveSync(); + std::cout << "Recieved: " << buffer.size() << std::endl; + } + + for(; i < buffer.size(); i++) { + if(buffer[i] == ' ') { + statusStart = i; + break; } - }); + } + for(; i < buffer.size(); i++) { + if(buffer[i] == '\r') { + response.status.assign(buffer.data()+statusStart+1, i-statusStart-1); + break; + } + } + i+=2; + while(i < buffer.size()) { + std::uint32_t headerStart = i; + std::string headerName; + for(; i < buffer.size(); i++) { + if(buffer[i] == ':') { + headerName.assign(buffer.data()+headerStart, i-headerStart); + std::transform(headerName.begin(), headerName.end(), headerName.begin(), [](unsigned char c){ return std::tolower(c); }); + i+=2; + break; + } + } + headerStart = i; + std::string headerValue; + for(; i < buffer.size(); i++) { + if(buffer[i] == '\r' && buffer[i+1] == '\n') { + headerValue.assign(buffer.data()+headerStart, i-headerStart); + response.headers.insert({headerName, headerValue}); + if(buffer[i+2] == '\r'){ + goto headersComplete; + } else{ + i+=2; + break; + } + } + } + } - controlStream = quic.OpenStream(/*unidirectional=*/true); - auto prelude = HTTP3::BuildControlStreamPrelude(); - controlStream.SendSync(prelude.data(), - static_cast(prelude.size()), - /*finish=*/false); + i = 0; } -}; - -ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds) - : host(host), port(port), impl(std::make_unique(host, port, std::move(creds))) {} - -ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds) - : ClientHTTP(host.c_str(), port, std::move(creds)) {} - -ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default; -ClientHTTP::~ClientHTTP() = default; - -namespace { - // Parse a sequence of HTTP/3 frames from `bytes`. Populates response from - // the first HEADERS frame and concatenates all DATA payloads. Trailing - // HEADERS frames (trailers) are decoded but discarded. Throws on - // malformed input. - HTTPResponse ParseResponseFrames(const std::vector& bytes) { - HTTPResponse response; - bool sawHeaders = false; - std::size_t pos = 0; - const auto* p = reinterpret_cast(bytes.data()); - std::size_t avail = bytes.size(); - - while (pos < avail) { - std::uint64_t frameType = 0, frameLen = 0; - std::size_t cn = 0; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) { - throw HTTP3::HTTP3ProtocolError("truncated frame type"); + headersComplete:; + std::cout << "Header complete" << std::endl; + i+=4; + std::unordered_map::iterator it = response.headers.find("content-length"); + if(it != response.headers.end()) + { + const int lenght = std::stoi(it->second); + std::cout << "Content lenght: " << lenght << std::endl; + response.body.resize(lenght, 0); + if(i < buffer.size()){ + std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i); + } + const int remaining = lenght-(buffer.size()-i); + std::cout << "Remain: " << remaining << std::endl; + if(remaining > 0){ + std::vector bodyBuffer = client.RecieveUntilFullSync(remaining); + std::memcpy(&response.body[ buffer.size()-i], bodyBuffer.data(), bodyBuffer.size()); + std::cout << "Recieved: " << bodyBuffer.size() << std::endl; + } + } else { + std::cout << "No Content Lenght" << std::endl; + std::unordered_map::iterator it = response.headers.find("transfer-encoding"); + if(it != response.headers.end() && it->second == "chunked") { + std::cout << "Chunked" << std::endl; + while(i < buffer.size()){ + std::string lenght; + int lenghtStart = i; + for(; i < buffer.size(); i++) { + if(buffer[i] == '\r') { + lenght.assign(buffer.data()+lenghtStart, i-lenghtStart); + break; + } + } + i+=2; + int lenghtInt = stoi(lenght, 0, 8); + if(lenghtInt != 0){ + int oldSize = response.body.size(); + response.body.resize(oldSize+lenghtInt, 0); + if(buffer.size() < lenghtInt) { + std::memcpy(&response.body[oldSize], buffer.data()+i, buffer.size()-i); + std::vector bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-buffer.size()); + std::memcpy(&response.body[oldSize+(buffer.size()-i)], buffer.data(), buffer.size()); + } else { + std::memcpy(&response.body[oldSize], buffer.data()+i, lenghtInt); + i+=lenghtInt; + } + } else{ + goto bodyFinished; + } } - pos += cn; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) { - throw HTTP3::HTTP3ProtocolError("truncated frame length"); - } - pos += cn; - if (pos + frameLen > avail) { - throw HTTP3::HTTP3ProtocolError("frame length runs past buffer"); - } - if (frameType == HTTP3::kFrameHeaders) { - auto fields = HTTP3::DecodeFieldSection(p + pos, static_cast(frameLen)); - if (!sawHeaders) { - for (auto& [name, value] : fields) { - if (name == ":status") { - response.status = std::move(value); - } else if (!name.empty() && name[0] == ':') { - // Unknown response pseudo-header — ignore. - } else { - response.headers.emplace(std::move(name), std::move(value)); + while(true) { + std::vector bodyBuffer = client.RecieveSync(); + int i2 = 0; + while(i2 < bodyBuffer.size()){ + std::string lenght; + int lenghtStart = i2; + for(; i2 < bodyBuffer.size(); i2++) { + if(buffer[i2] == '\r') { + lenght.assign(bodyBuffer.data()+lenghtStart, i2-lenghtStart); + break; } } - sawHeaders = true; + i2+=2; + int lenghtInt = stoi(lenght, 0, 8); + if(lenghtInt != 0){ + int oldSize = response.body.size(); + response.body.resize(oldSize+lenghtInt, 0); + if(bodyBuffer.size() < lenghtInt) { + std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, bodyBuffer.size()-i2); + std::vector bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-bodyBuffer.size()); + std::memcpy(&response.body[oldSize+(bodyBuffer.size()-i2)], bodyBuffer2.data(), bodyBuffer2.size()); + } else { + std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, lenghtInt); + i2+=lenghtInt; + } + } else{ + goto bodyFinished; + } } - // Trailer HEADERS frames are skipped; the field section was - // already decoded above and the contents discarded. - } else if (frameType == HTTP3::kFrameData) { - response.body.append(reinterpret_cast(p + pos), - static_cast(frameLen)); - } else { - // Unknown frame types are reserved/extensions — RFC 9114 §9 - // says skip them. } - pos += static_cast(frameLen); + bodyFinished:; + } else { + std::cout << "Recv until close" << std::endl; + std::vector bodyBuffer = client.RecieveUntilCloseSync(); + response.body.resize((buffer.size()-i)+(bodyBuffer.size()), 0); + if(i < buffer.size()){ + std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i); + } + std::memcpy(&response.body[buffer.size()-i], bodyBuffer.data(), bodyBuffer.size()); + std::cout << "Closed" << std::endl; } - if (!sawHeaders) { - throw HTTP3::HTTP3ProtocolError("response stream had no HEADERS frame"); - } - return response; } + std::cout << "Response recieved" << std::endl; + return response; } - -void ClientHTTP::SendAsync(const HTTPRequest& request, - std::function onSuccess, - std::function onError) { - HTTPRequest copy = request; - ThreadPool::Enqueue([this, copy = std::move(copy), - onSuccess = std::move(onSuccess), - onError = std::move(onError)]() mutable { - try { - HTTPResponse response = this->Send(copy); - if (onSuccess) onSuccess(std::move(response)); - } catch (const std::exception& e) { - if (onError) onError(e.what()); - } catch (...) { - if (onError) onError("unknown error"); - } - }); -} - -HTTPResponse ClientHTTP::Send(const HTTPRequest& request) { - QUICStream stream = impl->quic.OpenStream(); - - // Pseudo-headers MUST appear before regular fields (RFC 9114 §4.3). - std::vector> fields; - fields.reserve(4 + request.headers.size()); - fields.emplace_back(":method", request.method.empty() ? std::string("GET") : request.method); - fields.emplace_back(":scheme", request.scheme.empty() ? std::string("https") : request.scheme); - fields.emplace_back(":authority", - request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority); - fields.emplace_back(":path", request.path.empty() ? std::string("/") : request.path); - for (const auto& [name, value] : request.headers) { - // HTTP/3 forbids uppercase in field names — lowercase defensively. - std::string lower = name; - std::ranges::transform(lower, lower.begin(), - [](unsigned char c){ return static_cast(std::tolower(c)); }); - fields.emplace_back(std::move(lower), value); - } - - auto encoded = HTTP3::EncodeFieldSection(fields); - - std::vector wire; - HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size()); - if (!request.body.empty()) { - HTTP3::WriteFrame(wire, HTTP3::kFrameData, - reinterpret_cast(request.body.data()), - request.body.size()); - } - - // Send the entire request and FIN our send-side. HTTP/3 servers need FIN - // to know the request is complete — there's no Content-Length signal. - stream.SendSync(wire.data(), static_cast(wire.size()), /*finish=*/true); - - auto raw = stream.RecieveUntilCloseSync(); - return ParseResponseFrames(raw); +HTTPResponse ClientHTTP::Send(std::string request) { + return Send(request.c_str(), request.size()); } diff --git a/implementations/Crafter.Network-ClientQUIC-Browser.cpp b/implementations/Crafter.Network-ClientQUIC-Browser.cpp deleted file mode 100644 index 7372433..0000000 --- a/implementations/Crafter.Network-ClientQUIC-Browser.cpp +++ /dev/null @@ -1,443 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -// CRAFTER_NETWORK_BROWSER implementation of ClientQUIC / QUICStream backed -// by the browser's WebTransport API. WebTransport is HTTP/3-based and -// expects a URL — we use https://${host}:${port}/${alpn}. Native msquic -// is not linked into the browser build. -// -// Async-only: synchronous send/receive methods on QUICStream are not -// compiled (gated out in the interface). Everything goes through the -// existing *Async / OnStream / OnDatagram callbacks. The JS bridge -// (additional/network-env.js) runs the WebTransport reader loops and -// dispatches each chunk back through the wasm exports declared below. - -module; -module Crafter.Network:ClientQUIC_impl; -import :ClientQUIC; -import std; - -using namespace Crafter; - -namespace Crafter::NetworkBrowserBindings { - __attribute__((import_module("env"), import_name("crafterNetworkWtConnect"))) - std::int32_t crafterNetworkWtConnect( - const char* host, std::int32_t hostLen, - std::int32_t port, - const char* alpn, std::int32_t alpnLen, - const std::uint8_t* certHash, std::int32_t certHashLen); - - __attribute__((import_module("env"), import_name("crafterNetworkWtClose"))) - void crafterNetworkWtClose(std::int32_t handle); - - __attribute__((import_module("env"), import_name("crafterNetworkWtOpenStream"))) - std::int32_t crafterNetworkWtOpenStream(std::int32_t handle, std::int32_t unidirectional); - - __attribute__((import_module("env"), import_name("crafterNetworkWtStreamWrite"))) - void crafterNetworkWtStreamWrite(std::int32_t streamId, - const char* buf, std::int32_t bufLen, - std::int32_t finish, - std::int32_t callbackId); - - __attribute__((import_module("env"), import_name("crafterNetworkWtStreamStop"))) - void crafterNetworkWtStreamStop(std::int32_t streamId); - - __attribute__((import_module("env"), import_name("crafterNetworkWtSendDatagram"))) - void crafterNetworkWtSendDatagram(std::int32_t handle, - const char* buf, std::int32_t bufLen); -} - -namespace Crafter::NetworkBrowser { - // ─── Receive state machine per stream ───────────────────────────────── - // - // The JS reader loop pushes every chunk it sees through - // CrafterNetworkOnWtStreamChunk. We buffer them until the user calls - // one of the Receive*Async variants — at which point we either - // dispatch immediately (chunks already queued) or wait for the next - // chunk to arrive. A FIN signal marks the end of the peer's send- - // side; further chunks after FIN do not arrive. - // - // StreamState / ConnectionState live in a named namespace (not - // anonymous) because they are referenced from the QUICStream::Impl - // and ClientQUIC::Impl definitions below — private nested types of - // exported classes can't have TU-local member types without a - // diagnostic. - - enum class RecvMode { None, Once, UntilClose, UntilFull }; - - struct StreamState { - // 0 means destroyed / closed. - std::int32_t handle = 0; - - // Buffered chunks not yet delivered to a pending callback. - std::vector buffer; - bool finReceived = false; - bool closed = false; - - // Pending one-shot receive. - RecvMode mode = RecvMode::None; - std::uint32_t target = 0; - std::function)> cb; - - ClientQUIC* connection = nullptr; - }; - - struct ConnectionState { - std::int32_t handle = 0; - bool ready = false; - bool closed = false; - std::function onStream; - std::function)> onDatagram; - }; - - // Handle → state. Allocated on the heap so the pointer is stable - // across QUICStream / ClientQUIC moves (each holds a unique_ptr - // that wraps a pointer into these maps via its handle). - inline std::unordered_map& Streams() { - static std::unordered_map m; - return m; - } - inline std::unordered_map& Connections() { - static std::unordered_map m; - return m; - } - inline std::unordered_map>& WriteCallbacks() { - static std::unordered_map> m; - return m; - } - inline std::int32_t NextWriteCallbackId() { - static std::int32_t counter = 0; - return ++counter; - } - - inline void TryDispatchRecv(StreamState& s) { - if (s.mode == RecvMode::None) return; - - if (s.mode == RecvMode::Once) { - if (!s.buffer.empty()) { - auto chunk = std::move(s.buffer); - s.buffer.clear(); - auto cb = std::move(s.cb); - s.mode = RecvMode::None; - if (cb) cb(std::move(chunk)); - } else if (s.finReceived || s.closed) { - auto cb = std::move(s.cb); - s.mode = RecvMode::None; - if (cb) cb({}); - } - return; - } - if (s.mode == RecvMode::UntilClose) { - if (s.finReceived || s.closed) { - auto chunk = std::move(s.buffer); - s.buffer.clear(); - auto cb = std::move(s.cb); - s.mode = RecvMode::None; - if (cb) cb(std::move(chunk)); - } - return; - } - if (s.mode == RecvMode::UntilFull) { - if (s.buffer.size() >= s.target) { - std::vector chunk(s.buffer.begin(), s.buffer.begin() + s.target); - s.buffer.erase(s.buffer.begin(), s.buffer.begin() + s.target); - auto cb = std::move(s.cb); - s.mode = RecvMode::None; - if (cb) cb(std::move(chunk)); - } else if (s.finReceived || s.closed) { - // Peer closed before we got the requested byte count — - // deliver whatever's left. Mirrors the native variant's - // "throws if peer closes early" only loosely (we have no - // exception channel from JS). - auto chunk = std::move(s.buffer); - s.buffer.clear(); - auto cb = std::move(s.cb); - s.mode = RecvMode::None; - if (cb) cb(std::move(chunk)); - } - } - } -} - -// All implementation-private state types live in Crafter::NetworkBrowser -// (above). Pull them in unqualified for readability. -using Crafter::NetworkBrowser::StreamState; -using Crafter::NetworkBrowser::ConnectionState; -using Crafter::NetworkBrowser::RecvMode; -using Crafter::NetworkBrowser::Streams; -using Crafter::NetworkBrowser::Connections; -using Crafter::NetworkBrowser::WriteCallbacks; -using Crafter::NetworkBrowser::NextWriteCallbackId; -using Crafter::NetworkBrowser::TryDispatchRecv; - -// ─── QUICStream::Impl ──────────────────────────────────────────────────── - -struct QUICStream::Impl { - StreamState state; -}; - -QUICStream::QUICStream() : impl(std::make_unique()) {} - -QUICStream::QUICStream(std::int32_t streamHandle, ClientQUIC* conn, - bool canSendArg, bool canReceiveArg) - : canSend(canSendArg), canReceive(canReceiveArg), - impl(std::make_unique()) -{ - connection = conn; - impl->state.handle = streamHandle; - impl->state.connection = conn; - Streams()[streamHandle] = &impl->state; -} - -QUICStream::QUICStream(QUICStream&&) noexcept = default; -QUICStream& QUICStream::operator=(QUICStream&&) noexcept = default; - -QUICStream::~QUICStream() { - if (impl && impl->state.handle != 0) { - Streams().erase(impl->state.handle); - Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle); - } -} - -void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish, - std::function onSent) { - if (!impl || impl->state.handle == 0 || impl->state.closed) { - if (onSent) onSent(); - return; - } - std::int32_t cbId = 0; - if (onSent) { - cbId = NextWriteCallbackId(); - WriteCallbacks()[cbId] = std::move(onSent); - } - Crafter::NetworkBrowserBindings::crafterNetworkWtStreamWrite( - impl->state.handle, - static_cast(buffer), - static_cast(size), - finish ? 1 : 0, - cbId); -} - -void QUICStream::RecieveAsync(std::function)> cb) { - if (!impl) { if (cb) cb({}); return; } - impl->state.mode = RecvMode::Once; - impl->state.cb = std::move(cb); - TryDispatchRecv(impl->state); -} - -void QUICStream::RecieveUntilCloseAsync(std::function)> cb) { - if (!impl) { if (cb) cb({}); return; } - impl->state.mode = RecvMode::UntilClose; - impl->state.cb = std::move(cb); - TryDispatchRecv(impl->state); -} - -void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize, - std::function)> cb) { - if (!impl) { if (cb) cb({}); return; } - impl->state.mode = RecvMode::UntilFull; - impl->state.target = bufferSize; - impl->state.cb = std::move(cb); - TryDispatchRecv(impl->state); -} - -void QUICStream::Stop() { - if (impl && impl->state.handle != 0) { - Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle); - Streams().erase(impl->state.handle); - impl->state.handle = 0; - impl->state.closed = true; - } -} - -// ─── ClientQUIC::Impl ──────────────────────────────────────────────────── - -struct ClientQUIC::Impl { - ConnectionState state; -}; - -namespace { - QUICStream MakeStreamFromHandle(std::int32_t streamId, ClientQUIC* conn, - bool canSend, bool canReceive) { - return QUICStream{streamId, conn, canSend, canReceive}; - } -} - -ClientQUIC::ClientQUIC(const char* host, std::uint16_t port, std::string alpnArg, - QUICClientCredentials creds) - : alpn(std::move(alpnArg)), impl(std::make_unique()) { - // Zeroed hash means "no pinning" — JS passes an empty array, browser - // falls back to its trust store. A non-zero hash is forwarded as a - // serverCertificateHashes entry (Chrome only, < 14 day cert validity). - const std::uint8_t* hashPtr = nullptr; - std::int32_t hashLen = 0; - for (std::uint8_t b : creds.serverCertificateHash) { - if (b != 0) { hashPtr = creds.serverCertificateHash.data(); hashLen = 32; break; } - } - std::string hostStr = host; - impl->state.handle = Crafter::NetworkBrowserBindings::crafterNetworkWtConnect( - hostStr.data(), static_cast(hostStr.size()), - static_cast(port), - alpn.data(), static_cast(alpn.size()), - hashPtr, hashLen); - // wasm builds run with -fno-exceptions; a failed JS-side allocation - // leaves the connection in a closed state and the first operation will - // produce a sentinel result (OpenStream returns a default-constructed - // QUICStream, SendDatagram silently drops, OnStream/OnDatagram never - // fire). The constructor cannot signal the failure synchronously. - if (impl->state.handle == 0) { - impl->state.closed = true; - } else { - Connections()[impl->state.handle] = &impl->state; - } -} - -ClientQUIC::ClientQUIC(std::string host, std::uint16_t port, std::string alpnArg, - QUICClientCredentials creds) - : ClientQUIC(host.c_str(), port, std::move(alpnArg), std::move(creds)) {} - -ClientQUIC::ClientQUIC(ClientQUIC&&) noexcept = default; - -ClientQUIC::~ClientQUIC() { - if (impl && impl->state.handle != 0) { - Connections().erase(impl->state.handle); - Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle); - } -} - -QUICStream ClientQUIC::OpenStream(bool unidirectional) { - if (!impl || impl->state.handle == 0 || impl->state.closed) { - return QUICStream{}; // default-constructed: closed sentinel - } - std::int32_t streamId = Crafter::NetworkBrowserBindings::crafterNetworkWtOpenStream( - impl->state.handle, unidirectional ? 1 : 0); - if (streamId == 0) { - return QUICStream{}; - } - return MakeStreamFromHandle(streamId, this, - /*canSend=*/true, - /*canReceive=*/!unidirectional); -} - -void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) { - if (!impl || impl->state.handle == 0 || impl->state.closed) return; - Crafter::NetworkBrowserBindings::crafterNetworkWtSendDatagram( - impl->state.handle, - static_cast(buffer), - static_cast(size)); -} - -void ClientQUIC::OnStream(std::function cb) { - if (impl) impl->state.onStream = std::move(cb); -} - -void ClientQUIC::OnDatagram(std::function)> cb) { - if (impl) impl->state.onDatagram = std::move(cb); -} - -void ClientQUIC::Stop() { - if (impl && impl->state.handle != 0) { - Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle); - Connections().erase(impl->state.handle); - impl->state.handle = 0; - impl->state.closed = true; - } -} - -// ─── WASM exports the JS bridge dispatches back through ────────────────── - -extern "C" { - __attribute__((export_name("CrafterNetworkOnWtReady"))) - void CrafterNetworkOnWtReady(std::int32_t connectionHandle) { - auto it = Connections().find(connectionHandle); - if (it == Connections().end()) return; - it->second->ready = true; - } - - __attribute__((export_name("CrafterNetworkOnWtClosed"))) - void CrafterNetworkOnWtClosed(std::int32_t connectionHandle, - char* messagePtr, std::int32_t /*messageLen*/) { - std::free(messagePtr); - auto it = Connections().find(connectionHandle); - if (it == Connections().end()) return; - it->second->closed = true; - it->second->ready = false; - // The JS bridge dispatches CrafterNetworkOnWtStreamChunk(_, nullptr, 0, - // fin=1) for each stream that belonged to this connection so pending - // receivers terminate. We don't iterate stream state here. - } - - __attribute__((export_name("CrafterNetworkOnWtIncomingStream"))) - void CrafterNetworkOnWtIncomingStream(std::int32_t connectionHandle, - std::int32_t streamId, - std::int32_t bidirectional) { - auto it = Connections().find(connectionHandle); - if (it == Connections().end() || !it->second->onStream) { - // No registered handler — close the stream JS-side to free - // resources. Mirrors what the native msquic backend does - // when a stream arrives before OnStream is registered (it - // queues, but in the browser we don't have a backing buffer - // to queue against without leaking). - Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(streamId); - return; - } - QUICStream stream = MakeStreamFromHandle(streamId, /*conn=*/nullptr, - /*canSend=*/bidirectional != 0, - /*canReceive=*/true); - it->second->onStream(std::move(stream)); - } - - __attribute__((export_name("CrafterNetworkOnWtStreamChunk"))) - void CrafterNetworkOnWtStreamChunk(std::int32_t streamId, - char* dataPtr, std::int32_t dataLen, - std::int32_t fin) { - auto it = Streams().find(streamId); - if (it == Streams().end()) { std::free(dataPtr); return; } - StreamState& s = *it->second; - if (dataPtr && dataLen > 0) { - s.buffer.insert(s.buffer.end(), dataPtr, dataPtr + dataLen); - } - std::free(dataPtr); - if (fin) s.finReceived = true; - TryDispatchRecv(s); - } - - __attribute__((export_name("CrafterNetworkOnWtStreamWriteComplete"))) - void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId) { - auto it = WriteCallbacks().find(callbackId); - if (it == WriteCallbacks().end()) return; - auto cb = std::move(it->second); - WriteCallbacks().erase(it); - if (cb) cb(); - } - - __attribute__((export_name("CrafterNetworkOnWtDatagram"))) - void CrafterNetworkOnWtDatagram(std::int32_t connectionHandle, - char* dataPtr, std::int32_t dataLen) { - auto it = Connections().find(connectionHandle); - if (it == Connections().end() || !it->second->onDatagram) { - std::free(dataPtr); - return; - } - std::vector data(dataPtr, dataPtr + dataLen); - std::free(dataPtr); - it->second->onDatagram(std::move(data)); - } -} diff --git a/implementations/Crafter.Network-ClientQUIC.cpp b/implementations/Crafter.Network-ClientQUIC.cpp index 0f71494..6bb4b23 100644 --- a/implementations/Crafter.Network-ClientQUIC.cpp +++ b/implementations/Crafter.Network-ClientQUIC.cpp @@ -150,8 +150,6 @@ struct QUICStream::Impl { } }; -QUICStream::QUICStream() = default; - QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection) : handle(handle), connection(connection), impl(std::make_unique()) { @@ -161,9 +159,7 @@ QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection) } QUICStream::QUICStream(QUICStream&& other) noexcept - : handle(other.handle), connection(other.connection), - canSend(other.canSend), canReceive(other.canReceive), - impl(std::move(other.impl)) + : handle(other.handle), connection(other.connection), impl(std::move(other.impl)) { other.handle = nullptr; other.connection = nullptr; @@ -174,8 +170,6 @@ QUICStream& QUICStream::operator=(QUICStream&& other) noexcept { Stop(); handle = other.handle; connection = other.connection; - canSend = other.canSend; - canReceive = other.canReceive; impl = std::move(other.impl); other.handle = nullptr; other.connection = nullptr; @@ -189,26 +183,12 @@ QUICStream::~QUICStream() { void QUICStream::Stop() { if (!handle) return; - // If the stream's SHUTDOWN_COMPLETE event has already fired, msquic has - // internally called StreamClose for us (see Impl::Callback) and the - // handle is no longer valid — calling StreamShutdown on it trips a - // quic_bugcheck inside msquic. Skip in that case. This is the common - // path for short-lived request/response streams where both peers FIN - // before the wrapper is destroyed. - bool alreadyClosed = false; - if (impl) { - std::lock_guard lk(impl->mtx); - alreadyClosed = impl->shutdownComplete; - } - if (!alreadyClosed) { - Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0); - } + Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0); handle = nullptr; - if (impl) impl->handle = nullptr; } void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) { - if (!handle || !canSend) throw QUICClosedException(); + if (!handle) throw QUICClosedException(); auto* copy = new char[size]; std::memcpy(copy, buffer, size); QUIC_BUFFER quicBuf{}; @@ -230,7 +210,7 @@ void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) { } std::vector QUICStream::RecieveSync() { - if (!handle || !canReceive) throw QUICClosedException(); + if (!handle) throw QUICClosedException(); std::unique_lock lk(impl->mtx); impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; }); if (!impl->pending.empty()) { @@ -242,7 +222,7 @@ std::vector QUICStream::RecieveSync() { } std::vector QUICStream::RecieveUntilCloseSync() { - if (!handle || !canReceive) throw QUICClosedException(); + if (!handle) throw QUICClosedException(); std::vector out; while (true) { std::unique_lock lk(impl->mtx); @@ -257,7 +237,7 @@ std::vector QUICStream::RecieveUntilCloseSync() { } std::vector QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) { - if (!handle || !canReceive) throw QUICClosedException(); + if (!handle) throw QUICClosedException(); std::vector out; out.reserve(bufferSize); while (out.size() < bufferSize) { @@ -280,18 +260,6 @@ std::vector QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) { return out; } -void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish, - std::function onSent) { - // Copy now: the caller's buffer may not outlive the enqueued task. - std::vector copy(static_cast(buffer), - static_cast(buffer) + size); - ThreadPool::Enqueue([this, copy = std::move(copy), finish, onSent = std::move(onSent)]() mutable { - try { this->SendSync(copy.data(), static_cast(copy.size()), finish); } - catch (...) { /* swallowed — callback still fires so the caller can move on */ } - if (onSent) onSent(); - }); -} - void QUICStream::RecieveAsync(std::function)> cb) { ThreadPool::Enqueue([this, cb]{ cb(this->RecieveSync()); }); } @@ -302,27 +270,6 @@ void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize, std::functionRecieveUntilFullSync(bufferSize)); }); } -void QUICStream::PrependReceived(std::vector bytes) { - if (bytes.empty() || !impl) return; - { - std::lock_guard lk(impl->mtx); - impl->pending.push_front(std::move(bytes)); - } - impl->cv.notify_all(); -} - -std::uint64_t QUICStream::GetStreamId() const { - if (!handle) throw QUICException("GetStreamId: stream is not open"); - QUIC_UINT62 id = 0; - std::uint32_t size = sizeof(id); - QUIC_STATUS s = Runtime().api->GetParam(handle, QUIC_PARAM_STREAM_ID, &size, &id); - if (QUIC_FAILED(s)) { - throw QUICException(std::format("GetParam(QUIC_PARAM_STREAM_ID) failed: 0x{:x}", - static_cast(s))); - } - return static_cast(id); -} - // ---------------- ClientQUIC::Impl ---------------- struct ClientQUIC::Impl { HQUIC connection = nullptr; @@ -338,12 +285,6 @@ struct ClientQUIC::Impl { std::function onStream; std::function)> onDatagram; std::deque> datagramQueue; - // Streams the peer started before the user installed an OnStream - // handler. Without this backlog the early streams (e.g. an h3 server's - // control stream right after handshake) would be aborted in the - // PEER_STREAM_STARTED branch and the connection would die with - // H3_MISSING_SETTINGS on the peer side. - std::deque pendingStreams; ClientQUIC* outer = nullptr; @@ -384,30 +325,18 @@ struct ClientQUIC::Impl { } case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: { HQUIC streamHandle = ev->PEER_STREAM_STARTED.Stream; - bool unidirectional = (ev->PEER_STREAM_STARTED.Flags - & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) != 0; QUICStream stream(streamHandle, self->outer); - if (unidirectional) { - // Peer-initiated unidi: peer sends, we read; we cannot send. - stream.canSend = false; - stream.canReceive = true; + if (self->onStream) { + auto cb = self->onStream; + auto* shared = new QUICStream(std::move(stream)); + ThreadPool::Enqueue([cb, shared]{ + cb(std::move(*shared)); + delete shared; + }); + } else { + // No handler: shut down to avoid leaking a stream. + Runtime().api->StreamShutdown(streamHandle, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); } - std::function cb; - { - std::lock_guard lk(self->mtx); - cb = self->onStream; - if (!cb) { - // Buffer until OnStream is installed; OnStream's - // setter drains this queue. - self->pendingStreams.push_back(std::move(stream)); - return QUIC_STATUS_SUCCESS; - } - } - auto* shared = new QUICStream(std::move(stream)); - ThreadPool::Enqueue([cb, shared]{ - cb(std::move(*shared)); - delete shared; - }); return QUIC_STATUS_SUCCESS; } case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: { @@ -453,16 +382,6 @@ static HQUIC OpenClientConfiguration(const std::string& alpn, const QUICClientCr settings.IdleTimeoutMs = 30'000; settings.IsSet.DatagramReceiveEnabled = 1; settings.DatagramReceiveEnabled = 1; - // Allow the server to open unidi/bidi streams to us. msquic defaults - // both peer-stream-count limits to 0; with that, the server's HTTP/3 - // control stream + QPACK encoder/decoder streams can't be created and - // most h3 servers will close the connection after handshake. We don't - // currently use server push (h3 pushes ride on unidi 0x01 streams) but - // the bidi cap is harmless to grant. - settings.IsSet.PeerUnidiStreamCount = 1; - settings.PeerUnidiStreamCount = 16; - settings.IsSet.PeerBidiStreamCount = 1; - settings.PeerBidiStreamCount = 16; HQUIC cfg = nullptr; QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1, @@ -551,26 +470,18 @@ void ClientQUIC::Stop() { Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0); } -QUICStream ClientQUIC::OpenStream(bool unidirectional) { +QUICStream ClientQUIC::OpenStream() { HQUIC streamHandle = nullptr; QUICStream stream; stream.impl = std::make_unique(); stream.impl->connection = this; - QUIC_STREAM_OPEN_FLAGS openFlags = unidirectional - ? QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL - : QUIC_STREAM_OPEN_FLAG_NONE; - QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, openFlags, + QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, QUIC_STREAM_OPEN_FLAG_NONE, reinterpret_cast(&QUICStream::Impl::Callback), stream.impl.get(), &streamHandle); if (QUIC_FAILED(s)) throw QUICException(std::format("StreamOpen failed: 0x{:x}", static_cast(s))); stream.handle = streamHandle; stream.connection = this; stream.impl->handle = streamHandle; - if (unidirectional) { - // We initiated the unidi stream: we send, peer reads. - stream.canSend = true; - stream.canReceive = false; - } s = Runtime().api->StreamStart(streamHandle, QUIC_STREAM_START_FLAG_NONE); if (QUIC_FAILED(s)) { Runtime().api->StreamClose(streamHandle); @@ -599,21 +510,7 @@ void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) { } void ClientQUIC::OnStream(std::function cb) { - std::deque backlog; - { - std::lock_guard lk(impl->mtx); - impl->onStream = cb; - std::swap(backlog, impl->pendingStreams); - } - while (!backlog.empty()) { - auto* shared = new QUICStream(std::move(backlog.front())); - backlog.pop_front(); - auto handler = cb; - ThreadPool::Enqueue([handler, shared]{ - handler(std::move(*shared)); - delete shared; - }); - } + impl->onStream = std::move(cb); } void ClientQUIC::OnDatagram(std::function)> cb) { diff --git a/implementations/Crafter.Network-ListenerHTTP.cpp b/implementations/Crafter.Network-ListenerHTTP.cpp index 6514659..3124eef 100644 --- a/implementations/Crafter.Network-ListenerHTTP.cpp +++ b/implementations/Crafter.Network-ListenerHTTP.cpp @@ -19,429 +19,224 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + module Crafter.Network:ListenerHTTP_impl; import :ListenerHTTP; -import :ListenerQUIC; -import :ClientQUIC; -import :HTTP; -import :HTTP3; -import :WebTransport; -import Crafter.Thread; +import :ClientTCP; import std; +import Crafter.Thread; using namespace Crafter; -namespace { - // Parse a complete request stream's bytes into an HTTPRequest. The stream - // is closed by the peer with FIN, so we read until close and then - // frame-walk the bytes (HEADERS [+ DATA]*). - HTTPRequest ParseRequestFrames(const std::vector& bytes) { - HTTPRequest request; - bool sawHeaders = false; - std::size_t pos = 0; - const auto* p = reinterpret_cast(bytes.data()); - std::size_t avail = bytes.size(); +ListenerHTTP::ListenerHTTP(std::uint16_t port, std::unordered_map> routes): routes(routes) { + sockaddr_in servAddr; + bzero((char*)&servAddr, sizeof(servAddr)); + servAddr.sin_family = AF_INET; + servAddr.sin_addr.s_addr = htonl(INADDR_ANY); + servAddr.sin_port = htons(port); - while (pos < avail) { - std::uint64_t frameType = 0, frameLen = 0; - std::size_t cn = 0; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) { - throw HTTP3::HTTP3ProtocolError("truncated frame type"); - } - pos += cn; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) { - throw HTTP3::HTTP3ProtocolError("truncated frame length"); - } - pos += cn; - if (pos + frameLen > avail) { - throw HTTP3::HTTP3ProtocolError("frame length runs past buffer"); - } - if (frameType == HTTP3::kFrameHeaders) { - auto fields = HTTP3::DecodeFieldSection(p + pos, static_cast(frameLen)); - if (!sawHeaders) { - for (auto& [name, value] : fields) { - if (name == ":method") request.method = std::move(value); - else if (name == ":scheme") request.scheme = std::move(value); - else if (name == ":authority") request.authority = std::move(value); - else if (name == ":path") request.path = std::move(value); - else if (!name.empty() && name[0] == ':') { - // Pass through other pseudo-headers (e.g. :protocol - // for extended CONNECT — RFC 8441 / RFC 9220) as - // regular headers so the routing layer can see them. - request.headers.emplace(std::move(name), std::move(value)); - } else { - request.headers.emplace(std::move(name), std::move(value)); - } - } - sawHeaders = true; - } - } else if (frameType == HTTP3::kFrameData) { - request.body.append(reinterpret_cast(p + pos), - static_cast(frameLen)); - } else { - // Skip unknown frames (RFC 9114 §9 — reserved/extension frame - // types are silently ignored). - } - pos += static_cast(frameLen); - } - if (!sawHeaders) { - throw HTTP3::HTTP3ProtocolError("request stream had no HEADERS frame"); - } - return request; + s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + throw std::runtime_error("Error establishing the server socket"); } - // Serialise a response into HEADERS [+ DATA] frames. - std::vector SerializeResponse(const HTTPResponse& response) { - std::vector> fields; - fields.reserve(1 + response.headers.size()); - fields.emplace_back(":status", response.status.empty() ? std::string("200") : response.status); - for (const auto& [name, value] : response.headers) { - std::string lower = name; - std::ranges::transform(lower, lower.begin(), - [](unsigned char c){ return static_cast(std::tolower(c)); }); - fields.emplace_back(std::move(lower), value); - } - auto encoded = HTTP3::EncodeFieldSection(fields); - - std::vector wire; - HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size()); - if (!response.body.empty()) { - HTTP3::WriteFrame(wire, HTTP3::kFrameData, - reinterpret_cast(response.body.data()), - response.body.size()); - } - return wire; + int opt = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { + throw std::runtime_error("Error setting SO_REUSEADDR"); } - // Read enough bytes from `stream` to decode one QUIC varint starting at - // `buffer[offset]`. Appends consumed chunks to `buffer` and advances - // `offset` past the varint. Returns the decoded value. Throws - // QUICClosedException if the peer closes before the varint is complete. - std::uint64_t ReadVarintFromStream(QUICStream& stream, - std::vector& buffer, - std::size_t& offset) { - std::uint64_t value = 0; - std::size_t consumed = 0; - while (true) { - const auto* p = reinterpret_cast(buffer.data() + offset); - if (HTTP3::DecodeVarint(p, buffer.size() - offset, value, consumed)) { - offset += consumed; - return value; - } - auto chunk = stream.RecieveSync(); // throws QUICClosed on FIN-without-data - buffer.insert(buffer.end(), chunk.begin(), chunk.end()); - } + int bindStatus = bind(s, (struct sockaddr*)&servAddr, sizeof(servAddr)); + if (bindStatus < 0) { + throw std::runtime_error(std::format("Error binding the server socket: {}", std::strerror(errno))); + } + + if (listen(s, 5) < 0) { + throw std::runtime_error("Error starting to listen on the server socket"); } } -// One accepted-CONNECT WebTransport session living inside a peer. -struct WTSessionEntry { - std::unique_ptr session; -}; - -// Per-peer state for an accepted connection. Holds the connection wrapper, -// the server-side control stream, and the WebTransport sessions that have -// been upgraded on this connection. -struct PeerState { - std::unique_ptr quic; - QUICStream controlStream; - // QPACK encoder/decoder streams; opened immediately after the control - // stream and never written to again (we run without a dynamic table). - // Their presence is what lets strict HTTP/3 stacks like Chromium decide - // the peer is ready for request streams. - QUICStream qpackEncoderStream; - QUICStream qpackDecoderStream; - std::mutex wtMtx; - // Session id == CONNECT stream's QUIC stream id, supplied by the peer - // as the first varint of every WT data stream. - std::unordered_map> wtSessions; -}; - -struct ListenerHTTP::Impl { - std::unique_ptr listener; - std::mutex peersMtx; - std::vector> peers; - bool running = true; -}; - -namespace { - // Build the per-connection bidi-stream handler. Demuxes WT streams from - // HTTP/3 request streams by peeking the first varint on the wire. Lives - // as a free helper so both ListenerHTTP constructors can install it. - std::function MakeBidiHandler( - ListenerHTTP* self, PeerState* peerState, - const std::unordered_map>* routes, - const std::unordered_map>* wtRoutes) - { - return [self, peerState, routes, wtRoutes](QUICStream stream) { - try { - // ── Phase A: identify the stream kind ───────────────────── - // - // We peek the leading varint(s) off the wire incrementally. - // For HTTP/3 streams that's `frame_type, frame_length`. For - // a WT_STREAM the body runs to FIN so there is no length. - std::vector peeked; - std::size_t cursor = 0; - std::uint64_t firstType = ReadVarintFromStream(stream, peeked, cursor); - - if (firstType == HTTP3::kFrameWtStream && !wtRoutes->empty()) { - // ── WT bidi data stream — second varint is session id. - std::uint64_t sessionId = ReadVarintFromStream(stream, peeked, cursor); - std::vector remaining(peeked.begin() + cursor, peeked.end()); - - WebTransportSession* session = nullptr; - { - std::lock_guard lk(peerState->wtMtx); - auto it = peerState->wtSessions.find(sessionId); - if (it == peerState->wtSessions.end()) return; - session = it->second->session.get(); - } - if (!remaining.empty()) { - stream.PrependReceived(std::move(remaining)); - } - WebTransportDeliverStream(*session, std::move(stream)); - return; - } - - // ── Phase B: HTTP/3 request stream ──────────────────────── - // - // First frame must be HEADERS. Read its length varint, then - // its payload, then look at :method / :protocol. The - // remaining stream content (DATA frames, FIN) is only - // consumed for non-CONNECT requests — WebTransport CONNECT - // never sends a body and the stream stays open for the - // session's lifetime. - std::uint64_t headerLen = ReadVarintFromStream(stream, peeked, cursor); - std::size_t payloadStart = cursor; - while (peeked.size() < payloadStart + headerLen) { - auto chunk = stream.RecieveSync(); - peeked.insert(peeked.end(), chunk.begin(), chunk.end()); - } - - if (firstType != HTTP3::kFrameHeaders) { - // Some other top-level frame — not a valid request. - return; - } - - auto fields = HTTP3::DecodeFieldSection( - reinterpret_cast(peeked.data() + payloadStart), - static_cast(headerLen)); - - HTTPRequest request; - for (auto& [name, value] : fields) { - if (name == ":method") request.method = std::move(value); - else if (name == ":scheme") request.scheme = std::move(value); - else if (name == ":authority") request.authority = std::move(value); - else if (name == ":path") request.path = std::move(value); - else request.headers.emplace(std::move(name), std::move(value)); - } - - // Extended CONNECT? RFC 8441 / draft-ietf-webtrans-http3. - auto protoIt = request.headers.find(":protocol"); - if (request.method == "CONNECT" && protoIt != request.headers.end() - && protoIt->second == "webtransport") - { - auto wtIt = wtRoutes->find(request.path); - if (wtIt == wtRoutes->end()) { - HTTPResponse nf; nf.status = "404"; nf.body = "WebTransport route not found"; - auto wire = SerializeResponse(nf); - try { stream.SendSync(wire.data(), static_cast(wire.size()), true); } catch (...) {} - return; - } - - // Accept: 200 OK HEADERS without FIN. The peer sees the - // session as "ready" once it reads this and keeps the - // CONNECT stream open as the session-control stream. - HTTPResponse ok; ok.status = "200"; - auto wire = SerializeResponse(ok); - stream.SendSync(wire.data(), static_cast(wire.size()), /*finish=*/false); - - std::uint64_t sessionId = stream.GetStreamId(); - - auto entry = std::make_unique(); - entry->session = std::make_unique(); - WebTransportSession* sessionPtr = entry->session.get(); - WebTransportInitialise(*sessionPtr, - peerState->quic.get(), - std::move(stream), - sessionId, - request.path); - { - std::lock_guard lk(peerState->wtMtx); - peerState->wtSessions.emplace(sessionId, std::move(entry)); - } - auto handler = wtIt->second; - ThreadPool::Enqueue([handler, sessionPtr]{ - try { handler(*sessionPtr); } catch (...) {} - }); - return; - } - - // Plain HTTP/3 request. Drain remaining DATA frames + FIN, - // reconstruct the body, dispatch. - std::vector remainingFrames(peeked.begin() + payloadStart + headerLen, peeked.end()); - try { - auto rest = stream.RecieveUntilCloseSync(); - remainingFrames.insert(remainingFrames.end(), rest.begin(), rest.end()); - } catch (...) {} - std::size_t pos = 0; - const auto* p = reinterpret_cast(remainingFrames.data()); - std::size_t avail = remainingFrames.size(); - while (pos < avail) { - std::uint64_t frameType = 0, frameLen = 0; - std::size_t cn = 0; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) break; - pos += cn; - if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) break; - pos += cn; - if (pos + frameLen > avail) break; - if (frameType == HTTP3::kFrameData) { - request.body.append(reinterpret_cast(p + pos), - static_cast(frameLen)); - } - pos += static_cast(frameLen); - } - - HTTPResponse response; - auto it = routes->find(request.path); - if (it != routes->end()) { - response = it->second(request); - } else { - response.status = "404"; - response.body = "Not Found"; - } - - auto wire = SerializeResponse(response); - stream.SendSync(wire.data(), - static_cast(wire.size()), - /*finish=*/true); - } catch (const std::exception& e) { - try { - HTTPResponse err; - err.status = "500"; - err.body = e.what(); - auto wire = SerializeResponse(err); - stream.SendSync(wire.data(), - static_cast(wire.size()), - /*finish=*/true); - } catch (...) {} - } - }; - } -} - -ListenerHTTP::ListenerHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> r) - : ListenerHTTP(port, std::move(creds), std::move(r), {}) -{} - -ListenerHTTP::ListenerHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> r, - std::unordered_map> wt) - : routes(std::move(r)) - , wtRoutes(std::move(wt)) - , alpn(HTTP3::kAlpn) - , impl(std::make_unique()) -{ - auto onConnect = [this](ClientQUIC* peer) { - auto state = std::make_unique(); - state->quic.reset(peer); - PeerState* statePtr = state.get(); - - peer->OnStream([this, statePtr](QUICStream stream) { - if (!stream.canSend) { - // Peer-initiated unidi: client's control stream + optional - // QPACK encoder/decoder streams. Phase 1 drains — peer- - // initiated WT unidi streams are deferred to a later phase. - try { - while (true) (void)stream.RecieveSync(); - } catch (...) {} - return; - } - // Bidi: either HTTP/3 request or WT data stream. Demux inside. - auto handler = MakeBidiHandler(this, statePtr, &this->routes, &this->wtRoutes); - handler(std::move(stream)); - }); - - // Open our outgoing control stream and write the SETTINGS prelude. - // When wtRoutes is non-empty we advertise WebTransport support so - // the browser will issue extended CONNECT against us. - try { - state->controlStream = peer->OpenStream(/*unidirectional=*/true); - auto prelude = this->wtRoutes.empty() - ? HTTP3::BuildControlStreamPrelude() - : HTTP3::BuildWebTransportControlStreamPrelude(/*maxSessions=*/16); - state->controlStream.SendSync(prelude.data(), - static_cast(prelude.size()), - /*finish=*/false); - - // QPACK encoder + decoder streams (RFC 9204 §5). We don't use the - // dynamic table, so these streams stay idle for the lifetime of - // the connection. They're not optional in practice: Chromium and - // some other HTTP/3 stacks won't issue any request stream until - // they've seen both stream types from the peer, even when the - // encoder is silent. - state->qpackEncoderStream = peer->OpenStream(/*unidirectional=*/true); - std::uint8_t encType = static_cast(HTTP3::kStreamQpackEnc); - state->qpackEncoderStream.SendSync(&encType, 1, /*finish=*/false); - - state->qpackDecoderStream = peer->OpenStream(/*unidirectional=*/true); - std::uint8_t decType = static_cast(HTTP3::kStreamQpackDec); - state->qpackDecoderStream.SendSync(&decType, 1, /*finish=*/false); - } catch (...) { - // Connection died mid-handshake; drop the peer. - } - - std::lock_guard lk(impl->peersMtx); - impl->peers.push_back(std::move(state)); - }; - - impl->listener = std::make_unique(port, - std::string(HTTP3::kAlpn), - std::move(creds), - onConnect); -} - -ListenerHTTP::ListenerHTTP(ListenerHTTP&&) noexcept = default; - ListenerHTTP::~ListenerHTTP() { - if (impl) Stop(); + if(s != -1) { + Stop(); + } } void ListenerHTTP::Stop() { - if (!impl) return; - impl->running = false; - if (impl->listener) impl->listener->Stop(); + running = false; + shutdown(s, SHUT_RDWR); + close(s); + s = -1; + for(ListenerHTTPClient* client : clients) { + client->client.Stop(); + client->thread.join(); + delete client; + } } void ListenerHTTP::Listen() { - if (!impl || !impl->listener) return; - impl->listener->ListenSyncAsync(); + while(running) { + sockaddr_in newSockAddr; + socklen_t newSockAddrSize = sizeof(newSockAddr); + int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize); + if(!running) { + return; + } + if (client > 0) { + clients.push_back(new ListenerHTTPClient(this, client)); + } else { + std::cerr << "Error accepting request from client!" << std::endl; + } + std::erase_if(clients, [](ListenerHTTPClient* client) { + if (client->disconnected.load()) { + client->thread.join(); + delete client; + return true; + } + return false; + }); + } } -ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes) - : listener(port, std::move(creds), std::move(routes)) - , thread(&ListenerHTTP::Listen, &listener) -{} +ListenerHTTPClient::ListenerHTTPClient(ListenerHTTP* server, int s) : server(server), client(s), thread(&ListenerHTTPClient::ListenRoutes, this), disconnected(false) { + +} -ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes, - std::unordered_map> wtRoutes) - : listener(port, std::move(creds), std::move(routes), std::move(wtRoutes)) - , thread(&ListenerHTTP::Listen, &listener) -{} +void ListenerHTTPClient::ListenRoutes() { + try { + while(true) { + std::vector buffer; + HTTPRequest request; + std::string route; + std::uint32_t i = 0; + std::uint32_t routeStart = 0; + while(true) { + buffer = client.RecieveSync(); + while(true) { + std::string str(buffer.begin(), buffer.end()); + for(; i < buffer.size(); i++) { + if(buffer[i] == ' ') { + request.method.assign(buffer.data(), i); + break; + } + } + for(; i < buffer.size(); i++) { + if(buffer[i] == '/') { + routeStart = i; + break; + } + } + for(; i < buffer.size(); i++) { + if(buffer[i] == ' ') { + route.assign(buffer.data()+routeStart, i-routeStart); + break; + } + } + for(; i < buffer.size(); i++) { + if(buffer[i] == '\r' && buffer[i+1] == '\n') { + break; + } + } + i+=2; + while(i < buffer.size()) { + std::uint32_t headerStart = i; + std::string headerName; + for(; i < buffer.size(); i++) { + if(buffer[i] == ':') { + headerName.assign(buffer.data()+headerStart, i-headerStart); + std::transform(headerName.begin(), headerName.end(), headerName.begin(), [](unsigned char c){ return std::tolower(c); }); + i++; + break; + } + } + headerStart = i; + std::string headerValue; + for(; i < buffer.size(); i++) { + if(buffer[i] == '\r' && buffer[i+1] == '\n') { + headerValue.assign(buffer.data()+headerStart, i-headerStart); + request.headers.insert({headerName, headerValue}); + if(buffer[i+2] == '\r'){ + goto headersComplete; + } else{ + i+=2; + break; + } + } + } + } + i = 0; + } + headersComplete:; + i+=4; + std::unordered_map::iterator it = request.headers.find("content-length"); + if(it != request.headers.end()) { + const int lenght = std::stoi(it->second); + request.body.resize(lenght, 0); + if(lenght > 0 ){ + std::int_fast32_t remaining = lenght-(buffer.size()-i); + if(remaining < 0) { + std::memcpy(&request.body[0], buffer.data()+i, lenght); + std::string response = server->routes.at(route)(request); + client.Send(&response[0], response.size()); + i+=lenght; + } else if(remaining == 0){ + std::memcpy(&request.body[0], buffer.data()+i, lenght); + std::string response = server->routes.at(route)(request); + client.Send(&response[0], response.size()); + break; + } else { + std::memcpy(&request.body[0], buffer.data()+i, buffer.size()-i); + std::vector bodyBuffer = client.RecieveUntilFullSync(remaining); + std::memcpy(&request.body[buffer.size()-i], bodyBuffer.data(), remaining); + std::string response = server->routes.at(route)(request); + client.Send(&response[0], response.size()); + break; + } + } else { + std::string response = server->routes.at(route)(request); + client.Send(&response[0], response.size()); + if(i == buffer.size()) { + break; + } + } + } else { + std::string response = server->routes.at(route)(request); + client.Send(&response[0], response.size()); + if(i == buffer.size()) { + break; + } + } + } + } + } catch(SocketClosedException& e) { + disconnected.store(true); + } +} + + +ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, std::unordered_map> routes): listener(port, routes), thread(&ListenerHTTP::Listen, &listener) { + +} ListenerAsyncHTTP::~ListenerAsyncHTTP() { - Stop(); + if(listener.s != -1) { + Stop(); + } } void ListenerAsyncHTTP::Stop() { - listener.Stop(); - if (thread.joinable()) thread.join(); -} + listener.Stop(); + thread.join(); +} \ No newline at end of file diff --git a/implementations/Crafter.Network-ListenerQUIC.cpp b/implementations/Crafter.Network-ListenerQUIC.cpp index c9e0c9f..022b7b9 100644 --- a/implementations/Crafter.Network-ListenerQUIC.cpp +++ b/implementations/Crafter.Network-ListenerQUIC.cpp @@ -21,7 +21,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA module; #include #include -#include #include #include module Crafter.Network:ListenerQUIC_impl; @@ -85,12 +84,6 @@ namespace { // mkdtemp'd directory under /tmp for the lifetime of the process. // Intended for dev / LAN play / tests — production should pass real // cert/key paths. - // - // Cert shape: ECDSA P-256, validity 13 days, SAN={DNS:localhost, - // IP:127.0.0.1, IP:::1}. These constraints are mandated by Chromium's - // WebTransport `serverCertificateHashes` (must be ECDSA P-256, validity - // <14 days, SAN with the connect target). msquic accepts the same cert - // unchanged, so existing pure-QUIC callers are unaffected. struct SelfSignedCert { std::string certPath; std::string keyPath; @@ -101,62 +94,24 @@ namespace { std::lock_guard lk(mtx); if (cached) return *cached; - // Stable on-disk location so the cert (and therefore its SHA-256) is - // reused across server restarts. Without this, a browser peer that - // pinned the cert hash on a previous run would see a hash mismatch - // the moment we restart. We only regenerate if the cert file is - // missing or has expired. - std::filesystem::path dir = "/tmp/crafter-network-quic-cert"; - std::error_code ec; - std::filesystem::create_directories(dir, ec); - if (ec) { - throw QUICException(std::format( - "could not create cert cache dir {}: {}", dir.string(), ec.message())); + char tmpl[] = "/tmp/crafter-quic-cert-XXXXXX"; + if (mkdtemp(tmpl) == nullptr) { + throw QUICException("mkdtemp failed for self-signed cert dir"); } + std::string dir = tmpl; SelfSignedCert s; - s.keyPath = (dir / "key.pem").string(); - s.certPath = (dir / "cert.pem").string(); - - bool needRegen = !std::filesystem::exists(s.certPath, ec) - || !std::filesystem::exists(s.keyPath, ec); - if (!needRegen) { - // Use openssl to ask whether the cert is still valid. -checkend 0 - // returns 0 if the cert is still good, non-zero if expired. - int rc = std::system(std::format( - "openssl x509 -in '{}' -noout -checkend 0 >/dev/null 2>&1", - s.certPath).c_str()); - if (rc != 0) needRegen = true; - } - if (needRegen) { - // Inline openssl config so we get exactly the extensions Chromium's - // WebTransport cert-hash verifier accepts (BasicConstraints CA:FALSE, - // KeyUsage digitalSignature, EKU serverAuth, SAN). Skipping the - // implicit subjectKeyIdentifier / authorityKeyIdentifier that - // `openssl req -x509 -addext ...` would otherwise add. - std::string cmd = std::format( - "openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 " - "-keyout '{}' -out '{}' -days 10 -nodes " - "-extensions v3_wt " - "-config /dev/stdin >/dev/null 2>&1 <<'CONFIG'\n" - "[req]\n" - "distinguished_name = req_dn\n" - "prompt = no\n" - "[req_dn]\n" - "CN = localhost\n" - "[v3_wt]\n" - "basicConstraints = critical, CA:FALSE\n" - "keyUsage = critical, digitalSignature\n" - "extendedKeyUsage = serverAuth\n" - "subjectAltName = DNS:localhost, IP:127.0.0.1, IP:::1\n" - "CONFIG\n", - s.keyPath, s.certPath); - int rc = std::system(cmd.c_str()); - if (rc != 0) { - throw QUICException(std::format( - "openssl CLI failed to generate self-signed cert " - "(exit {}); install openssl or pass certPath/keyPath", - rc)); - } + s.keyPath = dir + "/key.pem"; + s.certPath = dir + "/cert.pem"; + std::string cmd = std::format( + "openssl req -x509 -newkey rsa:2048 -keyout '{}' -out '{}' " + "-days 1 -nodes -subj '/CN=localhost' >/dev/null 2>&1", + s.keyPath, s.certPath); + int rc = std::system(cmd.c_str()); + if (rc != 0) { + throw QUICException(std::format( + "openssl CLI failed to generate self-signed cert " + "(exit {}); install openssl or pass certPath/keyPath", + rc)); } cached = std::move(s); return *cached; @@ -374,53 +329,3 @@ void ListenerQUIC::ListenAsyncSync() { void ListenerQUIC::ListenAsyncAsync() { impl->acceptLoop = std::thread([this]{ ListenSyncAsync(); }); } - -std::array Crafter::ComputeCertificateHashSHA256(const std::string& certPath) { - // Convert PEM → DER → SHA-256 via openssl, capture hex digest from stdout. - // openssl pipes give us "SHA2-256(stdin)= \n"; we parse the trailing - // hex run. Shelling out keeps msquic the sole TLS dependency. - std::string cmd = std::format( - "openssl x509 -in '{}' -outform der | openssl dgst -sha256 2>/dev/null", - certPath); - FILE* p = popen(cmd.c_str(), "r"); - if (!p) throw QUICException("popen failed while computing cert hash"); - std::string out; - char chunk[256]; - while (auto n = std::fread(chunk, 1, sizeof(chunk), p)) { - out.append(chunk, n); - } - int rc = pclose(p); - if (rc != 0) { - throw QUICException(std::format( - "openssl failed to compute SHA-256 of {} (exit {})", certPath, rc)); - } - auto eq = out.rfind('='); - if (eq == std::string::npos) { - throw QUICException("could not parse openssl dgst output"); - } - std::string hex; - for (std::size_t i = eq + 1; i < out.size(); ++i) { - char c = out[i]; - if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { - hex.push_back(c); - } - } - if (hex.size() != 64) { - throw QUICException(std::format( - "unexpected SHA-256 hex length {} (expected 64)", hex.size())); - } - std::array result{}; - for (std::size_t i = 0; i < 32; ++i) { - auto nibble = [](char c) -> std::uint8_t { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); - return 10 + (c - 'A'); - }; - result[i] = (nibble(hex[2 * i]) << 4) | nibble(hex[2 * i + 1]); - } - return result; -} - -std::string Crafter::GetSelfSignedCertificatePath() { - return GetSelfSignedCert().certPath; -} diff --git a/implementations/Crafter.Network-WebTransport.cpp b/implementations/Crafter.Network-WebTransport.cpp deleted file mode 100644 index c002ddf..0000000 --- a/implementations/Crafter.Network-WebTransport.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -module Crafter.Network:WebTransport_impl; -import :WebTransport; -import :ClientQUIC; -import :HTTP3; -import Crafter.Thread; -import std; - -using namespace Crafter; - -struct WebTransportSession::Impl { - // Non-owning. The session is constructed and owned by ListenerHTTP, and - // ListenerHTTP keeps the underlying ClientQUIC alive in its PeerState - // for the duration of the connection — so this pointer is valid as - // long as the session is. - ClientQUIC* connection = nullptr; - - // The HTTP/3 extended-CONNECT bidi stream the session was upgraded on. - // Stays open for the session's lifetime. Phase 1 closes it with a bare - // FIN; later phases will emit a CLOSE_WEBTRANSPORT_SESSION capsule. - QUICStream connectStream; - - std::mutex mtx; - std::function onStream; - std::deque pendingStreams; - std::function)> onDatagram; // Phase 2 - - bool closed = false; -}; - -WebTransportSession::WebTransportSession() - : impl(std::make_unique()) -{} - -WebTransportSession::WebTransportSession(WebTransportSession&&) noexcept = default; -WebTransportSession& WebTransportSession::operator=(WebTransportSession&&) noexcept = default; - -WebTransportSession::~WebTransportSession() { - if (impl) Close(); -} - -QUICStream WebTransportSession::OpenStream(bool unidirectional) { - if (!impl || impl->closed || !impl->connection) { - throw QUICClosedException(); - } - QUICStream stream = impl->connection->OpenStream(unidirectional); - auto prefix = unidirectional - ? HTTP3::BuildWtUnidiPrefix(sessionId) - : HTTP3::BuildWtBidiPrefix(sessionId); - // Write the WT_STREAM (bidi) or stream-type (unidi) prefix as the - // first send on the stream. The peer reads it to associate this - // stream with our session before treating the rest as opaque payload. - stream.SendSync(prefix.data(), static_cast(prefix.size()), /*finish=*/false); - return stream; -} - -void WebTransportSession::OnStream(std::function callback) { - std::deque drained; - { - std::lock_guard lk(impl->mtx); - impl->onStream = callback; - drained.swap(impl->pendingStreams); - } - // Dispatch any streams that arrived before the handler was installed. - // Each goes to the ThreadPool so user code runs off the demuxer thread. - for (auto& s : drained) { - auto* shared = new QUICStream(std::move(s)); - ThreadPool::Enqueue([callback, shared]{ - callback(std::move(*shared)); - delete shared; - }); - } -} - -void WebTransportSession::OnDatagram(std::function)> callback) { - // Phase 1 stub. Phase 2 will plumb QUIC datagrams through here after - // demuxing on quarter_session_id. - if (impl) impl->onDatagram = std::move(callback); -} - -void WebTransportSession::SendDatagram(const void*, std::uint32_t) { - // Phase 1 stub — would prepend quarter_session_id varint and call - // connection->SendDatagram. Drops silently for now. -} - -void WebTransportSession::Close() { - if (!impl || impl->closed) return; - impl->closed = true; - try { - // Empty FIN on the CONNECT stream. Chrome / Firefox both treat - // peer-FIN of the CONNECT stream as session-close. - impl->connectStream.SendSync(nullptr, 0, /*finish=*/true); - } catch (...) { - // Connection may already be gone — that's fine. - } -} - -// ─── Internal ListenerHTTP-facing helpers ─────────────────────────────── -// -// Declared (not exported) in the interface partition so ListenerHTTP_impl -// can call them; defined here. Friendship in WebTransportSession gives -// them access to the private Impl. - -namespace Crafter { - void WebTransportInitialise(WebTransportSession& session, - ClientQUIC* connection, - QUICStream connectStream, - std::uint64_t sessionId, - std::string path) { - session.impl->connection = connection; - session.impl->connectStream = std::move(connectStream); - session.sessionId = sessionId; - session.path = std::move(path); - } - - void WebTransportDeliverStream(WebTransportSession& session, QUICStream stream) { - std::function cb; - { - std::lock_guard lk(session.impl->mtx); - cb = session.impl->onStream; - if (!cb) { - session.impl->pendingStreams.push_back(std::move(stream)); - return; - } - } - auto* shared = new QUICStream(std::move(stream)); - ThreadPool::Enqueue([cb, shared]{ - cb(std::move(*shared)); - delete shared; - }); - } -} diff --git a/interfaces/Crafter.Network-ClientHTTP.cppm b/interfaces/Crafter.Network-ClientHTTP.cppm index c75cf23..e4df28e 100644 --- a/interfaces/Crafter.Network-ClientHTTP.cppm +++ b/interfaces/Crafter.Network-ClientHTTP.cppm @@ -20,51 +20,19 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA export module Crafter.Network:ClientHTTP; import std; +import :ClientTCP; import :HTTP; -import :ClientQUIC; namespace Crafter { - // HTTP/3 client over QUIC. The constructor establishes the QUIC connection - // (TLS handshake + ALPN "h3"); each Send() opens a fresh request stream - // on the multiplexed connection. Thread-affinity: Send() is not safe to - // call from multiple threads concurrently against the same ClientHTTP, - // but distinct ClientHTTP instances are independent. - // - // For local development against a self-signed listener, pass - // QUICClientCredentials{insecureNoServerValidation = true}. - // - // Browser build: the request is dispatched via the browser's fetch() - // and the synchronous Send() is not compiled — use SendAsync instead. - // The ClientHTTP instance does not maintain a persistent connection - // (fetch is request-scoped); host and port are stored and prefixed to - // the request path on each call. QUICClientCredentials is ignored. - export class ClientHTTP { - public: - std::string host; - std::uint16_t port; - - ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds = {}); - ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds = {}); - - ~ClientHTTP(); - ClientHTTP(const ClientHTTP&) = delete; - ClientHTTP(ClientHTTP&&) noexcept; - -#ifndef CRAFTER_NETWORK_BROWSER - // Send a request and synchronously read back the full response. - HTTPResponse Send(const HTTPRequest& request); -#endif - - // Send a request and deliver the response (or an error) via callback. - // Available on both native and browser builds. Native dispatches on - // Crafter.Thread's ThreadPool; browser uses fetch() and resolves on - // the JS event loop. - void SendAsync(const HTTPRequest& request, - std::function onSuccess, - std::function onError); - - private: - struct Impl; - std::unique_ptr impl; - }; -} + export class ClientHTTP { + public: + std::string host; + std::uint16_t port; + ClientHTTP(const char* host, std::uint16_t port); + ClientHTTP(std::string host, std::uint16_t port); + HTTPResponse Send(const char* request, std::uint32_t length); + HTTPResponse Send(std::string request); + private: + ClientTCP client; + }; +} \ No newline at end of file diff --git a/interfaces/Crafter.Network-ClientQUIC.cppm b/interfaces/Crafter.Network-ClientQUIC.cppm index 1ad567e..1d31b8c 100644 --- a/interfaces/Crafter.Network-ClientQUIC.cppm +++ b/interfaces/Crafter.Network-ClientQUIC.cppm @@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifndef CRAFTER_NETWORK_BROWSER #include -#endif export module Crafter.Network:ClientQUIC; import std; @@ -47,58 +45,32 @@ namespace Crafter { }; // Client-side credential validation. By default we require a real cert. - // insecureNoServerValidation disables peer cert checks — only for dev, - // and silently ignored in the browser build (browsers enforce their own - // certificate policy). For browser dev against a self-signed listener, - // populate serverCertificateHash with the SHA-256 of the server's DER - // certificate; on the browser it is forwarded to WebTransport's - // serverCertificateHashes option. A zeroed array means "unused" — the - // browser will then require a publicly trusted cert. + // insecureNoServerValidation disables peer cert checks — only for dev. export struct QUICClientCredentials { bool insecureNoServerValidation = false; - std::array serverCertificateHash{}; }; export class ClientQUIC; - // A reliable, ordered stream within a QUIC connection. May be - // bidirectional or unidirectional; for unidi streams either canSend or - // canReceive will be false depending on which side initiated. Owned by - // ClientQUIC; do not destroy directly. Obtain via ClientQUIC::OpenStream - // (optionally with unidirectional=true) or via the on-stream callback - // for inbound streams initiated by the peer. + // A reliable, ordered, bidirectional stream within a QUIC connection. + // Owned by ClientQUIC; do not destroy directly. Obtain via + // ClientQUIC::OpenStream() or via the on-stream callback for inbound + // streams initiated by the peer. export class QUICStream { public: -#ifndef CRAFTER_NETWORK_BROWSER // Underlying msquic HQUIC handle. Treated as opaque by callers. HQUIC handle = nullptr; -#endif // The connection that owns this stream (non-owning). ClientQUIC* connection = nullptr; - // Direction flags. Bidi streams have both true; outgoing unidi sets - // canReceive=false; incoming unidi (peer-initiated) sets canSend=false. - bool canSend = true; - bool canReceive = true; - - QUICStream(); -#ifndef CRAFTER_NETWORK_BROWSER + QUICStream() = default; QUICStream(HQUIC handle, ClientQUIC* connection); -#else - // Browser-only constructor: wraps a JS-side WebTransport stream - // identified by its integer handle. Used by ClientQUIC::OpenStream - // and by the incoming-stream dispatcher in the JS bridge — not - // intended for direct use. - QUICStream(std::int32_t handle, ClientQUIC* connection, - bool canSend, bool canReceive); -#endif ~QUICStream(); QUICStream(const QUICStream&) = delete; QUICStream(QUICStream&&) noexcept; QUICStream& operator=(QUICStream&&) noexcept; -#ifndef CRAFTER_NETWORK_BROWSER // Send a buffer. If finish=true, the send-side of the stream is closed // after the buffer is delivered (peer will see graceful shutdown). // Blocks until msquic accepts the buffer; throws on stream/conn close. @@ -114,36 +86,12 @@ namespace Crafter { // Read exactly bufferSize bytes; throws if the peer closes early. std::vector RecieveUntilFullSync(std::uint32_t bufferSize); -#endif - // Send a buffer. If finish=true, the send-side is closed after the - // buffer is delivered. onSent fires once the transport has accepted - // the buffer (native) or the WritableStream writer has resolved - // (browser). Available on both native and browser builds. - void SendAsync(const void* buffer, std::uint32_t size, bool finish, - std::function onSent); - - // Async receive variants. Dispatched on Crafter.Thread's ThreadPool - // (native) or driven by a per-stream JS reader loop (browser). + // Async variants: dispatched on Crafter.Thread's ThreadPool. void RecieveAsync(std::function)> recieveCallback); void RecieveUntilCloseAsync(std::function)> recieveCallback); void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function)> recieveCallback); -#ifndef CRAFTER_NETWORK_BROWSER - // Advanced: re-inject already-consumed bytes at the front of the - // receive queue so the next Recieve* call sees them. Used by - // protocol demuxers (e.g. the WebTransport stream router in - // ListenerHTTP) that need to peek a prefix off the wire, then hand - // the stream to user code as if the prefix had never been read. - void PrependReceived(std::vector bytes); - - // Underlying QUIC stream id. Stable for the stream's lifetime. - // Browsers identify WT streams by the session's CONNECT stream id, - // so the server has to query and remember it at session creation - // time. Throws if the stream is not yet started. - std::uint64_t GetStreamId() const; -#endif - // Cleanly shut down the stream (both directions). void Stop(); @@ -154,9 +102,8 @@ namespace Crafter { }; // A QUIC connection. On the client side, constructing one initiates the - // handshake and (on native) blocks until it succeeds, or throws on - // failure. On the server side, ListenerQUIC instantiates these for - // accepted peers. + // handshake and blocks until it succeeds (or throws on failure). On the + // server side, ListenerQUIC instantiates these for accepted peers. // // A connection multiplexes: // - Reliable, ordered streams (open via OpenStream() / observe inbound @@ -166,28 +113,10 @@ namespace Crafter { // Lifetime: ~ClientQUIC closes the connection. Streams obtained from // OpenStream() are scoped to the connection and must be destroyed (or // moved out) before the ClientQUIC. - // - // Browser build: the only QUIC-shaped API the browser exposes is - // WebTransport, which is HTTP/3-based and reached at a fixed URL. Here: - // - The constructor returns immediately; the connection is opened in - // the background. Operations issued before the connection is ready - // are queued JS-side until WebTransport's "ready" promise resolves - // (or fail with QUICClosedException if the connection rejects). - // - `alpn` is mapped to the URL path: new WebTransport( - // `https://${host}:${port}/${alpn}`). The QUIC-layer ALPN itself - // is fixed to "h3" by the browser and cannot be customised. - // - The server side must accept WebTransport sessions (HTTP/3 extended - // CONNECT) on the path equal to `alpn`. Plain QUIC with a custom - // ALPN — what ListenerQUIC offers today — is not reachable from a - // browser. - // - Synchronous send/receive methods are not compiled. Use the *Async - // variants instead. export class ClientQUIC { public: // ALPN identifier exchanged in the handshake. Both peers must agree. // For 3DForts use e.g. "f3d/1" or similar — a short stable token. - // On the browser build, this is the WebTransport URL path instead - // of an ALPN token; see the class comment above. std::string alpn; // Client constructor: connects to host:port using QUIC. ALPN must @@ -197,22 +126,18 @@ namespace Crafter { ClientQUIC(std::string host, std::uint16_t port, std::string alpn, QUICClientCredentials creds = {}); -#ifndef CRAFTER_NETWORK_BROWSER // Server-side constructor used by ListenerQUIC for accepted peers. // Takes ownership of an already-accepted msquic connection handle // and the server configuration handle. Not intended for direct use. ClientQUIC(HQUIC connectionHandle, HQUIC serverConfiguration, std::string alpn); -#endif ~ClientQUIC(); ClientQUIC(const ClientQUIC&) = delete; ClientQUIC(ClientQUIC&&) noexcept; - // Open a new stream initiated by this side. Defaults to bidirectional; - // pass unidirectional=true to open a one-way send stream (used for - // HTTP/3's control + QPACK encoder/decoder streams). + // Open a new bidirectional stream initiated by this side. // Blocks until the stream is started; throws on failure. - QUICStream OpenStream(bool unidirectional = false); + QUICStream OpenStream(); // Send a datagram. Best-effort: may be silently dropped under loss // or congestion. Size must fit within the path MTU (msquic surfaces @@ -228,19 +153,15 @@ namespace Crafter { // msquic worker; copy/queue and return promptly. void OnDatagram(std::function)> callback); -#ifndef CRAFTER_NETWORK_BROWSER // Block the caller until the next datagram arrives; returns it. // Throws QUICClosedException if the connection closes first. std::vector RecieveDatagramSync(); -#endif // Cleanly shut down the connection. void Stop(); -#ifndef CRAFTER_NETWORK_BROWSER // Underlying handle for advanced use (parameter queries, etc.). HQUIC GetHandle() const; -#endif private: struct Impl; diff --git a/interfaces/Crafter.Network-ClientTCP.cppm b/interfaces/Crafter.Network-ClientTCP.cppm index c020945..efca771 100755 --- a/interfaces/Crafter.Network-ClientTCP.cppm +++ b/interfaces/Crafter.Network-ClientTCP.cppm @@ -18,7 +18,6 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifndef CRAFTER_NETWORK_BROWSER #include #include #include @@ -32,11 +31,9 @@ module; #include #include #include -#endif export module Crafter.Network:ClientTCP; import std; -#ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { export class SocketClosedException : public std::exception { public: @@ -71,5 +68,4 @@ namespace Crafter { hostent* host; sockaddr_in serv_addr; }; -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Network-HTTP.cppm b/interfaces/Crafter.Network-HTTP.cppm index 84f2652..b6788a5 100644 --- a/interfaces/Crafter.Network-HTTP.cppm +++ b/interfaces/Crafter.Network-HTTP.cppm @@ -22,98 +22,63 @@ export module Crafter.Network:HTTP; import std; namespace Crafter { - // HTTP/3 request as carried over a QUIC bidirectional stream. The four - // pseudo-headers (method/scheme/authority/path) are split out as named - // fields rather than living in the headers map, because RFC 9114 forbids - // them from appearing in the regular header section and this shape makes - // route dispatch and request construction cleaner. `headers` keys are - // expected lowercase; HTTP/3 forbids uppercase characters in field names. export struct HTTPRequest { std::string method; - std::string scheme = "https"; - std::string authority; - std::string path = "/"; std::unordered_map headers; std::string body; }; - // HTTP/3 response. `status` is the numeric three-digit code as a string - // (e.g. "200") — HTTP/3 has no reason phrase. `headers` keys are expected - // lowercase. export struct HTTPResponse { - std::string status = "200"; + std::string status; std::unordered_map headers; std::string body; }; - export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority) { - HTTPRequest r; - r.method = std::move(method); - r.path = std::move(path); - r.authority = std::move(authority); - return r; + export constexpr std::string CreateResponseHTTP(std::string status) { + return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n", status); + } + + export constexpr std::string CreateResponseHTTP(std::string status, std::unordered_map headers) { + std::string headersString; + for (auto const& [key, val] : headers) { + headersString+=std::format("{}: {}\r\n", key, val); + } + return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: 0\r\n{}\r\n", status, headersString); + } + + export constexpr std::string CreateResponseHTTP(std::string status, std::string body) { + return std::format("HTTP/1.1 {}\r\nContent-Length: {}\r\nConnection: keep-alive\r\n\r\n{}", status, body.size(), body); + } + + export constexpr std::string CreateResponseHTTP(std::string status, std::unordered_map headers, std::string body) { + std::string headersString; + for (auto const& [key, val] : headers) { + headersString+=std::format("{}: {}\r\n", key, val); + } + return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: {}\r\n{}\r\n{}", status, body.size(), headersString, body); } - export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority, - std::unordered_map headers) { - HTTPRequest r; - r.method = std::move(method); - r.path = std::move(path); - r.authority = std::move(authority); - r.headers = std::move(headers); - return r; - } + export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host) { + return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nContent-Length: 0\r\nHost: {}\r\n\r\n", method, route, host); + } - export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority, - std::string body) { - HTTPRequest r; - r.method = std::move(method); - r.path = std::move(path); - r.authority = std::move(authority); - r.body = std::move(body); - return r; - } + export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::unordered_map headers) { + std::string headersString; + for (auto const& [key, val] : headers) { + headersString+=std::format("{}: {}\r\n", key, val); + } + return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\nAccept-Encoding: identity\r\nHost: {}\r\n{}\r\n", method, route, host, headersString); + } - export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority, - std::unordered_map headers, - std::string body) { - HTTPRequest r; - r.method = std::move(method); - r.path = std::move(path); - r.authority = std::move(authority); - r.headers = std::move(headers); - r.body = std::move(body); - return r; - } + export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::string body) { + return std::format("{} {} HTTP/1.1\r\nContent-Length: {}\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nHost: {}\r\n\r\n{}", method, route, body.size(), host, body); + } - export inline HTTPResponse CreateResponseHTTP(std::string status) { - HTTPResponse r; - r.status = std::move(status); - return r; - } - - export inline HTTPResponse CreateResponseHTTP(std::string status, - std::unordered_map headers) { - HTTPResponse r; - r.status = std::move(status); - r.headers = std::move(headers); - return r; - } - - export inline HTTPResponse CreateResponseHTTP(std::string status, std::string body) { - HTTPResponse r; - r.status = std::move(status); - r.body = std::move(body); - return r; - } - - export inline HTTPResponse CreateResponseHTTP(std::string status, - std::unordered_map headers, - std::string body) { - HTTPResponse r; - r.status = std::move(status); - r.headers = std::move(headers); - r.body = std::move(body); - return r; - } -} + export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::unordered_map headers, std::string body) { + std::string headersString; + for (auto const& [key, val] : headers) { + headersString+=std::format("{}: {}\r\n", key, val); + } + return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: {}\r\nHost: {}\r\nAccept-Encoding: identity\r\n{}\r\n{}", method, route, body.size(), host, headersString, body); + } +} \ No newline at end of file diff --git a/interfaces/Crafter.Network-HTTP3.cppm b/interfaces/Crafter.Network-HTTP3.cppm deleted file mode 100644 index 07b01d5..0000000 --- a/interfaces/Crafter.Network-HTTP3.cppm +++ /dev/null @@ -1,651 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -// HTTP/3 wire-format helpers. This partition is intentionally NOT re-exported -// from Crafter.Network — it's an internal building block shared between the -// ClientHTTP and ListenerHTTP implementation files. -// -// Scope: -// - QUIC variable-length integers (RFC 9000 §16) -// - HTTP/3 frame type/length codec (RFC 9114 §7) -// - QPACK field-section encode/decode (RFC 9204) limited to the static -// table + literal representations. No dynamic table, no Huffman. This -// suffices because both peers in this library are this same library; -// interoperability with browsers/curl over h3 would additionally require -// a Huffman codec which is out of scope here. - -export module Crafter.Network:HTTP3; -import std; - -namespace Crafter::HTTP3 { - // ---------------- ALPN ---------------- - // RFC 9114 §3.1 — final h3 ALPN identifier. - export inline constexpr std::string_view kAlpn = "h3"; - - // ---------------- Frame types (RFC 9114 §7.2) ---------------- - export inline constexpr std::uint64_t kFrameData = 0x00; - export inline constexpr std::uint64_t kFrameHeaders = 0x01; - export inline constexpr std::uint64_t kFrameSettings = 0x04; - // WebTransport bidirectional stream frame type (draft-ietf-webtrans-http3). - // Distinct from normal HTTP/3 frames — its body is unbounded (runs to FIN) - // rather than length-prefixed, and the first bytes of the body are the - // session id varint. - export inline constexpr std::uint64_t kFrameWtStream = 0x41; - - // ---------------- Unidirectional stream types (RFC 9114 §6.2) ---------------- - export inline constexpr std::uint64_t kStreamControl = 0x00; - export inline constexpr std::uint64_t kStreamQpackEnc = 0x02; - export inline constexpr std::uint64_t kStreamQpackDec = 0x03; - // WebTransport unidirectional stream type (draft-ietf-webtrans-http3). - // After this varint comes a session id varint, then opaque payload to FIN. - export inline constexpr std::uint64_t kStreamWt = 0x54; - - // ---------------- SETTINGS parameter identifiers ---------------- - // Required to negotiate WebTransport over HTTP/3 + HTTP/3 datagrams. - export inline constexpr std::uint64_t kSettingQpackMaxTableCapacity = 0x01; // RFC 9204 - export inline constexpr std::uint64_t kSettingQpackBlockedStreams = 0x07; // RFC 9204 - export inline constexpr std::uint64_t kSettingEnableConnectProtocol = 0x08; // RFC 9220 - export inline constexpr std::uint64_t kSettingH3Datagram = 0x33; // RFC 9297 - // Legacy identifiers from older WebTransport / H3-DATAGRAM drafts. Chrome - // (as of M120-ish) advertises and looks for the draft-02 / draft-04 ids - // alongside the RFC ones; if we only send the modern ids it decides we - // don't support WebTransport and aborts with ERR_METHOD_NOT_SUPPORTED. - export inline constexpr std::uint64_t kSettingH3DatagramDraft04 = 0xffd277; // draft-ietf-masque-h3-datagram-04 - export inline constexpr std::uint64_t kSettingEnableWebTransport = 0x2b603742; // draft-02 boolean - export inline constexpr std::uint64_t kSettingWtMaxSessions = 0xc671706a; // draft-ietf-webtrans-http3 (-07+) - - // ---------------- Errors ---------------- - export class HTTP3ProtocolError : public std::runtime_error { - public: - using std::runtime_error::runtime_error; - }; - - // ---------------- QUIC varint (RFC 9000 §16) ---------------- - // Encodes value into the smallest of {1, 2, 4, 8}-byte forms. - export inline void EncodeVarint(std::uint64_t value, std::vector& out) { - if (value < (1ULL << 6)) { - out.push_back(static_cast(value)); - } else if (value < (1ULL << 14)) { - out.push_back(static_cast(0x40 | (value >> 8))); - out.push_back(static_cast(value & 0xFF)); - } else if (value < (1ULL << 30)) { - out.push_back(static_cast(0x80 | ((value >> 24) & 0x3F))); - out.push_back(static_cast((value >> 16) & 0xFF)); - out.push_back(static_cast((value >> 8) & 0xFF)); - out.push_back(static_cast(value & 0xFF)); - } else if (value < (1ULL << 62)) { - out.push_back(static_cast(0xC0 | ((value >> 56) & 0x3F))); - out.push_back(static_cast((value >> 48) & 0xFF)); - out.push_back(static_cast((value >> 40) & 0xFF)); - out.push_back(static_cast((value >> 32) & 0xFF)); - out.push_back(static_cast((value >> 24) & 0xFF)); - out.push_back(static_cast((value >> 16) & 0xFF)); - out.push_back(static_cast((value >> 8) & 0xFF)); - out.push_back(static_cast(value & 0xFF)); - } else { - throw HTTP3ProtocolError("varint value exceeds 2^62-1"); - } - } - - // Returns true on success. On false, no consumed/value mutation observed. - export inline bool DecodeVarint(const std::uint8_t* data, std::size_t available, - std::uint64_t& value, std::size_t& consumed) { - if (available == 0) return false; - std::uint8_t first = data[0]; - std::size_t len = std::size_t{1} << (first >> 6); - if (available < len) return false; - std::uint64_t v = first & 0x3F; - for (std::size_t i = 1; i < len; ++i) { - v = (v << 8) | data[i]; - } - value = v; - consumed = len; - return true; - } - - // ---------------- QPACK / HPACK-style integer (RFC 7541 §5.1) ---------------- - // Different beast from QUIC varint. N-bit prefix integer used inside QPACK - // representations; the high (8-N) bits of the first byte carry pattern flags. - export inline void EncodeQpackInt(std::vector& out, std::uint8_t topBits, - int N, std::uint64_t value) { - std::uint8_t mask = static_cast((1U << N) - 1); - if (value < mask) { - out.push_back(static_cast(topBits | value)); - return; - } - out.push_back(static_cast(topBits | mask)); - value -= mask; - while (value >= 128) { - out.push_back(static_cast((value & 0x7F) | 0x80)); - value >>= 7; - } - out.push_back(static_cast(value)); - } - - // Decode N-bit-prefix integer. data[0] holds prefix flags; the low N bits - // contribute to the value (continuation bytes follow if low N bits == mask). - export inline bool DecodeQpackInt(const std::uint8_t* data, std::size_t available, - int N, std::uint64_t& value, std::size_t& consumed) { - if (available == 0) return false; - std::uint8_t mask = static_cast((1U << N) - 1); - std::uint8_t first = data[0] & mask; - if (first < mask) { - value = first; - consumed = 1; - return true; - } - std::uint64_t v = mask; - int shift = 0; - std::size_t i = 1; - while (true) { - if (i >= available) return false; - std::uint8_t b = data[i++]; - v += static_cast(b & 0x7F) << shift; - if ((b & 0x80) == 0) break; - shift += 7; - if (shift > 63) return false; - } - value = v; - consumed = i; - return true; - } - - // ---------------- Huffman codec (RFC 7541 Appendix B) ---------------- - // Decode-only. Real h3 peers (browsers, curl, cloudflare, etc.) Huffman- - // encode header values by default, so without this any external interop - // collapses on the first response. We don't emit Huffman ourselves — - // peers MUST accept H=0 literal per the spec, so encoding doesn't add - // interop value, only wire compactness. The 256-entry table plus a - // straightforward bit-walking decoder is the smallest viable form. - struct HuffmanCode { - std::uint32_t code; - std::uint8_t length; - }; - inline constexpr std::array kHuffmanTable = {{ - /* 0 */ {0x1ff8, 13}, /* 1 */ {0x7fffd8, 23}, /* 2 */ {0xfffffe2, 28}, - /* 3 */ {0xfffffe3, 28}, /* 4 */ {0xfffffe4, 28}, /* 5 */ {0xfffffe5, 28}, - /* 6 */ {0xfffffe6, 28}, /* 7 */ {0xfffffe7, 28}, /* 8 */ {0xfffffe8, 28}, - /* 9 */ {0xffffea, 24}, /* 10 */ {0x3ffffffc,30}, /* 11 */ {0xfffffe9, 28}, - /* 12 */ {0xfffffea, 28}, /* 13 */ {0x3ffffffd,30}, /* 14 */ {0xfffffeb, 28}, - /* 15 */ {0xfffffec, 28}, /* 16 */ {0xfffffed, 28}, /* 17 */ {0xfffffee, 28}, - /* 18 */ {0xfffffef, 28}, /* 19 */ {0xffffff0, 28}, /* 20 */ {0xffffff1, 28}, - /* 21 */ {0xffffff2, 28}, /* 22 */ {0x3ffffffe,30}, /* 23 */ {0xffffff3, 28}, - /* 24 */ {0xffffff4, 28}, /* 25 */ {0xffffff5, 28}, /* 26 */ {0xffffff6, 28}, - /* 27 */ {0xffffff7, 28}, /* 28 */ {0xffffff8, 28}, /* 29 */ {0xffffff9, 28}, - /* 30 */ {0xffffffa, 28}, /* 31 */ {0xffffffb, 28}, /* 32 */ {0x14, 6}, - /* 33 */ {0x3f8, 10}, /* 34 */ {0x3f9, 10}, /* 35 */ {0xffa, 12}, - /* 36 */ {0x1ff9, 13}, /* 37 */ {0x15, 6}, /* 38 */ {0xf8, 8}, - /* 39 */ {0x7fa, 11}, /* 40 */ {0x3fa, 10}, /* 41 */ {0x3fb, 10}, - /* 42 */ {0xf9, 8}, /* 43 */ {0x7fb, 11}, /* 44 */ {0xfa, 8}, - /* 45 */ {0x16, 6}, /* 46 */ {0x17, 6}, /* 47 */ {0x18, 6}, - /* 48 */ {0x0, 5}, /* 49 */ {0x1, 5}, /* 50 */ {0x2, 5}, - /* 51 */ {0x19, 6}, /* 52 */ {0x1a, 6}, /* 53 */ {0x1b, 6}, - /* 54 */ {0x1c, 6}, /* 55 */ {0x1d, 6}, /* 56 */ {0x1e, 6}, - /* 57 */ {0x1f, 6}, /* 58 */ {0x5c, 7}, /* 59 */ {0xfb, 8}, - /* 60 */ {0x7ffc, 15}, /* 61 */ {0x20, 6}, /* 62 */ {0xffb, 12}, - /* 63 */ {0x3fc, 10}, /* 64 */ {0x1ffa, 13}, /* 65 */ {0x21, 6}, - /* 66 */ {0x5d, 7}, /* 67 */ {0x5e, 7}, /* 68 */ {0x5f, 7}, - /* 69 */ {0x60, 7}, /* 70 */ {0x61, 7}, /* 71 */ {0x62, 7}, - /* 72 */ {0x63, 7}, /* 73 */ {0x64, 7}, /* 74 */ {0x65, 7}, - /* 75 */ {0x66, 7}, /* 76 */ {0x67, 7}, /* 77 */ {0x68, 7}, - /* 78 */ {0x69, 7}, /* 79 */ {0x6a, 7}, /* 80 */ {0x6b, 7}, - /* 81 */ {0x6c, 7}, /* 82 */ {0x6d, 7}, /* 83 */ {0x6e, 7}, - /* 84 */ {0x6f, 7}, /* 85 */ {0x70, 7}, /* 86 */ {0x71, 7}, - /* 87 */ {0x72, 7}, /* 88 */ {0xfc, 8}, /* 89 */ {0x73, 7}, - /* 90 */ {0xfd, 8}, /* 91 */ {0x1ffb, 13}, /* 92 */ {0x7fff0, 19}, - /* 93 */ {0x1ffc, 13}, /* 94 */ {0x3ffc, 14}, /* 95 */ {0x22, 6}, - /* 96 */ {0x7ffd, 15}, /* 97 */ {0x3, 5}, /* 98 */ {0x23, 6}, - /* 99 */ {0x4, 5}, /*100 */ {0x24, 6}, /*101 */ {0x5, 5}, - /*102 */ {0x25, 6}, /*103 */ {0x26, 6}, /*104 */ {0x27, 6}, - /*105 */ {0x6, 5}, /*106 */ {0x74, 7}, /*107 */ {0x75, 7}, - /*108 */ {0x28, 6}, /*109 */ {0x29, 6}, /*110 */ {0x2a, 6}, - /*111 */ {0x7, 5}, /*112 */ {0x2b, 6}, /*113 */ {0x76, 7}, - /*114 */ {0x2c, 6}, /*115 */ {0x8, 5}, /*116 */ {0x9, 5}, - /*117 */ {0x2d, 6}, /*118 */ {0x77, 7}, /*119 */ {0x78, 7}, - /*120 */ {0x79, 7}, /*121 */ {0x7a, 7}, /*122 */ {0x7b, 7}, - /*123 */ {0x7ffe, 15}, /*124 */ {0x7fc, 11}, /*125 */ {0x3ffd, 14}, - /*126 */ {0x1ffd, 13}, /*127 */ {0xffffffc, 28}, /*128 */ {0xfffe6, 20}, - /*129 */ {0x3fffd2, 22}, /*130 */ {0xfffe7, 20}, /*131 */ {0xfffe8, 20}, - /*132 */ {0x3fffd3, 22}, /*133 */ {0x3fffd4, 22}, /*134 */ {0x3fffd5, 22}, - /*135 */ {0x7fffd9, 23}, /*136 */ {0x3fffd6, 22}, /*137 */ {0x7fffda, 23}, - /*138 */ {0x7fffdb, 23}, /*139 */ {0x7fffdc, 23}, /*140 */ {0x7fffdd, 23}, - /*141 */ {0x7fffde, 23}, /*142 */ {0xffffeb, 24}, /*143 */ {0x7fffdf, 23}, - /*144 */ {0xffffec, 24}, /*145 */ {0xffffed, 24}, /*146 */ {0x3fffd7, 22}, - /*147 */ {0x7fffe0, 23}, /*148 */ {0xffffee, 24}, /*149 */ {0x7fffe1, 23}, - /*150 */ {0x7fffe2, 23}, /*151 */ {0x7fffe3, 23}, /*152 */ {0x7fffe4, 23}, - /*153 */ {0x1fffdc, 21}, /*154 */ {0x3fffd8, 22}, /*155 */ {0x7fffe5, 23}, - /*156 */ {0x3fffd9, 22}, /*157 */ {0x7fffe6, 23}, /*158 */ {0x7fffe7, 23}, - /*159 */ {0xffffef, 24}, /*160 */ {0x3fffda, 22}, /*161 */ {0x1fffdd, 21}, - /*162 */ {0xfffe9, 20}, /*163 */ {0x3fffdb, 22}, /*164 */ {0x3fffdc, 22}, - /*165 */ {0x7fffe8, 23}, /*166 */ {0x7fffe9, 23}, /*167 */ {0x1fffde, 21}, - /*168 */ {0x7fffea, 23}, /*169 */ {0x3fffdd, 22}, /*170 */ {0x3fffde, 22}, - /*171 */ {0xfffff0, 24}, /*172 */ {0x1fffdf, 21}, /*173 */ {0x3fffdf, 22}, - /*174 */ {0x7fffeb, 23}, /*175 */ {0x7fffec, 23}, /*176 */ {0x1fffe0, 21}, - /*177 */ {0x1fffe1, 21}, /*178 */ {0x3fffe0, 22}, /*179 */ {0x1fffe2, 21}, - /*180 */ {0x7fffed, 23}, /*181 */ {0x3fffe1, 22}, /*182 */ {0x7fffee, 23}, - /*183 */ {0x7fffef, 23}, /*184 */ {0xfffea, 20}, /*185 */ {0x3fffe2, 22}, - /*186 */ {0x3fffe3, 22}, /*187 */ {0x3fffe4, 22}, /*188 */ {0x7ffff0, 23}, - /*189 */ {0x3fffe5, 22}, /*190 */ {0x3fffe6, 22}, /*191 */ {0x7ffff1, 23}, - /*192 */ {0x3ffffe0,26}, /*193 */ {0x3ffffe1, 26}, /*194 */ {0xfffeb, 20}, - /*195 */ {0x7fff1, 19}, /*196 */ {0x3fffe7, 22}, /*197 */ {0x7ffff2, 23}, - /*198 */ {0x3fffe8, 22}, /*199 */ {0x1ffffec, 25}, /*200 */ {0x3ffffe2, 26}, - /*201 */ {0x3ffffe3,26}, /*202 */ {0x3ffffe4, 26}, /*203 */ {0x7ffffde, 27}, - /*204 */ {0x7ffffdf,27}, /*205 */ {0x3ffffe5, 26}, /*206 */ {0xfffff1, 24}, - /*207 */ {0x1ffffed,25}, /*208 */ {0x7fff2, 19}, /*209 */ {0x1fffe3, 21}, - /*210 */ {0x3ffffe6,26}, /*211 */ {0x7ffffe0, 27}, /*212 */ {0x7ffffe1, 27}, - /*213 */ {0x3ffffe7,26}, /*214 */ {0x7ffffe2, 27}, /*215 */ {0xfffff2, 24}, - /*216 */ {0x1fffe4, 21}, /*217 */ {0x1fffe5, 21}, /*218 */ {0x3ffffe8, 26}, - /*219 */ {0x3ffffe9,26}, /*220 */ {0xffffffd,28}, /*221 */ {0x7ffffe3, 27}, - /*222 */ {0x7ffffe4,27}, /*223 */ {0x7ffffe5, 27}, /*224 */ {0xfffec, 20}, - /*225 */ {0xfffff3, 24}, /*226 */ {0xfffed, 20}, /*227 */ {0x1fffe6, 21}, - /*228 */ {0x3fffe9, 22}, /*229 */ {0x1fffe7, 21}, /*230 */ {0x1fffe8, 21}, - /*231 */ {0x7ffff3, 23}, /*232 */ {0x3fffea, 22}, /*233 */ {0x3fffeb, 22}, - /*234 */ {0x1ffffee,25}, /*235 */ {0x1ffffef, 25}, /*236 */ {0xfffff4, 24}, - /*237 */ {0xfffff5, 24}, /*238 */ {0x3ffffea, 26}, /*239 */ {0x7ffff4, 23}, - /*240 */ {0x3ffffeb,26}, /*241 */ {0x7ffffe6, 27}, /*242 */ {0x3ffffec, 26}, - /*243 */ {0x3ffffed,26}, /*244 */ {0x7ffffe7, 27}, /*245 */ {0x7ffffe8, 27}, - /*246 */ {0x7ffffe9,27}, /*247 */ {0x7ffffea, 27}, /*248 */ {0x7ffffeb, 27}, - /*249 */ {0xffffffe,28}, /*250 */ {0x7ffffec, 27}, /*251 */ {0x7ffffed, 27}, - /*252 */ {0x7ffffee,27}, /*253 */ {0x7ffffef, 27}, /*254 */ {0x7fffff0, 27}, - /*255 */ {0x3ffffee,26}, - }}; - - export inline std::string DecodeHuffman(const std::uint8_t* in, std::size_t inLen) { - // Bit-walking decoder. Refill a 64-bit register from the input byte - // stream, then for each output symbol scan the 256-entry table for - // the unique code that matches the top `bits` of the register at - // some length L in [5, 30]. Linear scan is acceptable for the - // header-section sizes seen in HTTP/3 traffic; the inner loop is - // hot for ~100s of bytes per request. - std::string out; - std::uint64_t reg = 0; - int bits = 0; - std::size_t pos = 0; - while (true) { - while (bits <= 56 && pos < inLen) { - reg = (reg << 8) | in[pos++]; - bits += 8; - } - if (bits == 0) return out; - - bool matched = false; - const int maxL = bits < 30 ? bits : 30; - for (int L = 5; L <= maxL; ++L) { - std::uint32_t want = static_cast( - (reg >> (bits - L)) & ((std::uint64_t{1} << L) - 1)); - for (std::size_t s = 0; s < 256; ++s) { - if (kHuffmanTable[s].length == L && kHuffmanTable[s].code == want) { - out.push_back(static_cast(s)); - bits -= L; - matched = true; - break; - } - } - if (matched) break; - } - if (!matched) { - // Tail must be ≤ 7 bits and all 1s — that's the EOS-prefix - // padding RFC 7541 §5.2 mandates. Anything else is malformed. - if (bits <= 7) { - std::uint64_t mask = (std::uint64_t{1} << bits) - 1; - if ((reg & mask) == mask) return out; - } - throw HTTP3ProtocolError("Huffman: invalid encoding"); - } - } - } - - // ---------------- QPACK static table (RFC 9204 Appendix A, subset) ---------------- - // We embed only the entries we either emit (encode) or might need to look - // up by index (decode peers using indexed/literal-with-name-ref). The - // subset covers all pseudo-headers and a few common content-type/status - // values — enough for self-interop. If the peer references an index - // outside this table we throw HTTP3ProtocolError. - struct StaticEntry { - std::string_view name; - std::string_view value; // empty if no canonical value (name-only entry) - }; - - inline constexpr std::array kStaticTable = {{ - {":authority", ""}, // 0 - {":path", "/"}, // 1 - {"age", "0"}, // 2 - {"content-disposition", ""}, // 3 - {"content-length", "0"}, // 4 - {"cookie", ""}, // 5 - {"date", ""}, // 6 - {"etag", ""}, // 7 - {"if-modified-since", ""}, // 8 - {"if-none-match", ""}, // 9 - {"last-modified", ""}, // 10 - {"link", ""}, // 11 - {"location", ""}, // 12 - {"referer", ""}, // 13 - {"set-cookie", ""}, // 14 - {":method", "CONNECT"}, // 15 - {":method", "DELETE"}, // 16 - {":method", "GET"}, // 17 - {":method", "HEAD"}, // 18 - {":method", "OPTIONS"}, // 19 - {":method", "POST"}, // 20 - {":method", "PUT"}, // 21 - {":scheme", "http"}, // 22 - {":scheme", "https"}, // 23 - {":status", "103"}, // 24 - {":status", "200"}, // 25 - {":status", "304"}, // 26 - {":status", "404"}, // 27 - {":status", "503"}, // 28 - {"accept", "*/*"}, // 29 - {"accept", "application/dns-message"}, // 30 - {"accept-encoding", "gzip, deflate, br"}, // 31 - {"accept-ranges", "bytes"}, // 32 - {"access-control-allow-headers", "cache-control"}, // 33 - {"access-control-allow-headers", "content-type"}, // 34 - {"access-control-allow-origin", "*"}, // 35 - {"cache-control", "max-age=0"}, // 36 - {"cache-control", "max-age=2592000"}, // 37 - {"cache-control", "max-age=604800"}, // 38 - {"cache-control", "no-cache"}, // 39 - {"cache-control", "no-store"}, // 40 - {"cache-control", "public, max-age=31536000"}, // 41 - {"content-encoding", "br"}, // 42 - {"content-encoding", "gzip"}, // 43 - {"content-type", "application/dns-message"}, // 44 - {"content-type", "application/javascript"}, // 45 - {"content-type", "application/json"}, // 46 - {"content-type", "application/x-www-form-urlencoded"}, // 47 - {"content-type", "image/gif"}, // 48 - {"content-type", "image/jpeg"}, // 49 - {"content-type", "image/png"}, // 50 - {"content-type", "text/css"}, // 51 - {"content-type", "text/html; charset=utf-8"}, // 52 - {"content-type", "text/plain"}, // 53 - {"content-type", "text/plain;charset=utf-8"}, // 54 - {"range", "bytes=0-"}, // 55 - {"strict-transport-security", "max-age=31536000"}, // 56 - {"strict-transport-security", "max-age=31536000; includesubdomains"}, // 57 - {"strict-transport-security", "max-age=31536000; includesubdomains; preload"}, // 58 - {"vary", "accept-encoding"}, // 59 - {"vary", "origin"}, // 60 - {"x-content-type-options", "nosniff"}, // 61 - {"x-xss-protection", "1; mode=block"}, // 62 - {":status", "100"}, // 63 - {":status", "204"}, // 64 - {":status", "206"}, // 65 - {":status", "302"}, // 66 - {":status", "400"}, // 67 - {":status", "403"}, // 68 - {":status", "421"}, // 69 - {":status", "425"}, // 70 - {":status", "500"}, // 71 - {"accept-language", ""}, // 72 - {"access-control-allow-credentials", "FALSE"}, // 73 - {"access-control-allow-credentials", "TRUE"}, // 74 - {"access-control-allow-headers", "*"}, // 75 - {"access-control-allow-methods", "get"}, // 76 - {"access-control-allow-methods", "get, post, options"}, // 77 - {"access-control-allow-methods", "options"}, // 78 - {"access-control-expose-headers", "content-length"}, // 79 - {"access-control-request-headers", "content-type"}, // 80 - {"access-control-request-method", "get"}, // 81 - {"access-control-request-method", "post"}, // 82 - {"alt-svc", "clear"}, // 83 - {"authorization", ""}, // 84 - {"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"}, // 85 - {"early-data", "1"}, // 86 - {"expect-ct", ""}, // 87 - {"forwarded", ""}, // 88 - {"if-range", ""}, // 89 - {"origin", ""}, // 90 - {"purpose", "prefetch"}, // 91 - {"server", ""}, // 92 - {"timing-allow-origin", "*"}, // 93 - {"upgrade-insecure-requests", "1"}, // 94 - {"user-agent", ""}, // 95 - {"x-forwarded-for", ""}, // 96 - {"x-frame-options", "deny"}, // 97 - {"x-frame-options", "sameorigin"}, // 98 - }}; - - // Lookup a (name, value) pair against the static table; returns -1 if not - // present. Linear scan is fine here: ~100 entries, called per header. - inline int StaticTableExactLookup(std::string_view name, std::string_view value) { - for (std::size_t i = 0; i < kStaticTable.size(); ++i) { - if (kStaticTable[i].name == name && kStaticTable[i].value == value) { - return static_cast(i); - } - } - return -1; - } - // Lookup name-only; returns the lowest matching index or -1. - inline int StaticTableNameLookup(std::string_view name) { - for (std::size_t i = 0; i < kStaticTable.size(); ++i) { - if (kStaticTable[i].name == name) return static_cast(i); - } - return -1; - } - - // ---------------- Field section codec ---------------- - // Encodes the QPACK-on-the-wire field section: a 2-byte prefix (Required - // Insert Count = 0, Sign+DeltaBase = 0 — i.e. no dynamic table reliance) - // followed by per-field representations. We pick the most compact static - // representation each header allows; never emit Huffman; never use the - // dynamic table. - export inline std::vector EncodeFieldSection( - const std::vector>& fields) { - std::vector out; - // Prefix: Required Insert Count (8-bit prefix int = 0) - EncodeQpackInt(out, 0x00, 8, 0); - // Sign + Delta Base (sign in high bit of 7-bit-prefix int = 0) - EncodeQpackInt(out, 0x00, 7, 0); - - for (const auto& [name, value] : fields) { - int exact = StaticTableExactLookup(name, value); - if (exact >= 0) { - // Indexed Field Line, T=1 (static): pattern 1Tixxxxx, 6-bit prefix. - EncodeQpackInt(out, 0xC0, 6, static_cast(exact)); - continue; - } - int nameIdx = StaticTableNameLookup(name); - if (nameIdx >= 0) { - // Literal Field Line With Name Reference, T=1 (static), - // N=0 (allow indexing on intermediaries — moot since no DT): - // pattern 01NTxxxx, 4-bit name-index prefix. - EncodeQpackInt(out, 0x50, 4, static_cast(nameIdx)); - } else { - // Literal Field Line With Literal Name, N=0, H=0: pattern - // 001NHxxx with a 3-bit name-length prefix. - EncodeQpackInt(out, 0x20, 3, name.size()); - out.insert(out.end(), name.begin(), name.end()); - } - // Value: 7-bit length prefix, H=0 (no Huffman). - EncodeQpackInt(out, 0x00, 7, value.size()); - out.insert(out.end(), value.begin(), value.end()); - } - return out; - } - - export inline std::vector> DecodeFieldSection( - const std::uint8_t* data, std::size_t available) { - std::size_t pos = 0; - std::uint64_t reqIc = 0, deltaBase = 0; - std::size_t cn = 0; - if (!DecodeQpackInt(data + pos, available - pos, 8, reqIc, cn)) { - throw HTTP3ProtocolError("QPACK: truncated Required Insert Count"); - } - pos += cn; - if (pos >= available) throw HTTP3ProtocolError("QPACK: missing Base"); - if (!DecodeQpackInt(data + pos, available - pos, 7, deltaBase, cn)) { - throw HTTP3ProtocolError("QPACK: truncated Base"); - } - pos += cn; - if (reqIc != 0) { - // Encoder used the dynamic table, which we don't track. Required - // Insert Count != 0 means we cannot decode without dynamic-table - // state; surface a clean protocol error rather than mis-decoding. - throw HTTP3ProtocolError("QPACK: dynamic table reference (Required Insert Count != 0)"); - } - - std::vector> fields; - while (pos < available) { - std::uint8_t b = data[pos]; - if ((b & 0x80) != 0) { - // 1Txxxxxx — Indexed Field Line. - bool isStatic = (b & 0x40) != 0; - std::uint64_t idx = 0; - if (!DecodeQpackInt(data + pos, available - pos, 6, idx, cn)) { - throw HTTP3ProtocolError("QPACK: truncated indexed field line"); - } - pos += cn; - if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table indexed line unsupported"); - if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static index out of range"); - const auto& e = kStaticTable[static_cast(idx)]; - fields.emplace_back(std::string(e.name), std::string(e.value)); - } else if ((b & 0xC0) == 0x40) { - // 01NTxxxx — Literal Field Line With Name Reference. - bool isStatic = (b & 0x10) != 0; - std::uint64_t idx = 0; - if (!DecodeQpackInt(data + pos, available - pos, 4, idx, cn)) { - throw HTTP3ProtocolError("QPACK: truncated literal-with-nameref index"); - } - pos += cn; - if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table name reference unsupported"); - if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static name index out of range"); - if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte"); - bool huffman = (data[pos] & 0x80) != 0; - std::uint64_t vlen = 0; - if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) { - throw HTTP3ProtocolError("QPACK: truncated value length"); - } - pos += cn; - if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: value runs past buffer"); - std::string value = huffman - ? DecodeHuffman(data + pos, static_cast(vlen)) - : std::string(reinterpret_cast(data + pos), - static_cast(vlen)); - pos += static_cast(vlen); - fields.emplace_back(std::string(kStaticTable[static_cast(idx)].name), std::move(value)); - } else if ((b & 0xE0) == 0x20) { - // 001NHxxx — Literal Field Line With Literal Name. - bool huffmanName = (b & 0x08) != 0; - std::uint64_t nlen = 0; - if (!DecodeQpackInt(data + pos, available - pos, 3, nlen, cn)) { - throw HTTP3ProtocolError("QPACK: truncated literal-name length"); - } - pos += cn; - if (pos + nlen > available) throw HTTP3ProtocolError("QPACK: literal name runs past buffer"); - std::string name = huffmanName - ? DecodeHuffman(data + pos, static_cast(nlen)) - : std::string(reinterpret_cast(data + pos), - static_cast(nlen)); - pos += static_cast(nlen); - if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte"); - bool huffmanValue = (data[pos] & 0x80) != 0; - std::uint64_t vlen = 0; - if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) { - throw HTTP3ProtocolError("QPACK: truncated literal-value length"); - } - pos += cn; - if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: literal value runs past buffer"); - std::string value = huffmanValue - ? DecodeHuffman(data + pos, static_cast(vlen)) - : std::string(reinterpret_cast(data + pos), - static_cast(vlen)); - pos += static_cast(vlen); - fields.emplace_back(std::move(name), std::move(value)); - } else { - // Indexed-with-Post-Base / Literal-with-Post-Base-Name-Reference - // both rely on a dynamic-table base offset we don't maintain. - throw HTTP3ProtocolError("QPACK: post-base reference unsupported"); - } - } - return fields; - } - - // ---------------- Frame helpers ---------------- - // A frame is: type (varint) | length (varint) | payload (length bytes). - export inline void WriteFrame(std::vector& out, std::uint64_t type, - const std::uint8_t* payload, std::size_t length) { - EncodeVarint(type, out); - EncodeVarint(static_cast(length), out); - out.insert(out.end(), payload, payload + length); - } - - // Empty SETTINGS frame body — we send no settings, accepting all defaults. - export inline std::vector BuildControlStreamPrelude() { - std::vector out; - EncodeVarint(kStreamControl, out); // unidi stream type - EncodeVarint(kFrameSettings, out); // SETTINGS frame type - EncodeVarint(0, out); // frame length 0 - return out; - } - - // Server-side variant that advertises WebTransport-over-HTTP/3 support - // to the peer. Without these three SETTINGS the browser silently rejects - // the extended CONNECT and the WebTransport.ready promise never resolves. - // `maxSessions` becomes the value of SETTINGS_WT_MAX_SESSIONS. - export inline std::vector BuildWebTransportControlStreamPrelude( - std::uint64_t maxSessions = 1) - { - // Encode the SETTINGS body first so we can write its length. The two - // QPACK settings declare we run with no dynamic table — sent - // explicitly because some HTTP/3 stacks (Chrome among them) refuse - // to consider the peer ready for extended-CONNECT until they have - // seen a baseline QPACK configuration. The draft-02 ENABLE_WEBTRANSPORT - // and draft-04 H3_DATAGRAM ids are sent alongside their RFC counterparts - // for compatibility with current Chrome (which still negotiates the - // draft form even when advertising RFC support). - std::vector body; - EncodeVarint(kSettingQpackMaxTableCapacity, body); EncodeVarint(0, body); - EncodeVarint(kSettingQpackBlockedStreams, body); EncodeVarint(0, body); - EncodeVarint(kSettingEnableConnectProtocol, body); EncodeVarint(1, body); - EncodeVarint(kSettingH3Datagram, body); EncodeVarint(1, body); - EncodeVarint(kSettingH3DatagramDraft04, body); EncodeVarint(1, body); - EncodeVarint(kSettingEnableWebTransport, body); EncodeVarint(1, body); - EncodeVarint(kSettingWtMaxSessions, body); EncodeVarint(maxSessions, body); - - std::vector out; - EncodeVarint(kStreamControl, out); - WriteFrame(out, kFrameSettings, body.data(), body.size()); - return out; - } - - // Prefix bytes that go on the front of an outgoing WT bidi stream — the - // peer reads these to know which session the stream belongs to. After - // this prefix the stream contains opaque WebTransport payload until FIN - // (there is no length field — WT_STREAM is the only HTTP/3 frame whose - // body runs to end-of-stream). - export inline std::vector BuildWtBidiPrefix(std::uint64_t sessionId) { - std::vector out; - EncodeVarint(kFrameWtStream, out); - EncodeVarint(sessionId, out); - return out; - } - - // Prefix bytes that go on the front of an outgoing WT unidi stream - // (server-initiated → client). Stream-type varint then session id. - export inline std::vector BuildWtUnidiPrefix(std::uint64_t sessionId) { - std::vector out; - EncodeVarint(kStreamWt, out); - EncodeVarint(sessionId, out); - return out; - } -} diff --git a/interfaces/Crafter.Network-ListenerHTTP.cppm b/interfaces/Crafter.Network-ListenerHTTP.cppm index b81d6ab..728b7e9 100644 --- a/interfaces/Crafter.Network-ListenerHTTP.cppm +++ b/interfaces/Crafter.Network-ListenerHTTP.cppm @@ -21,82 +21,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA export module Crafter.Network:ListenerHTTP; import std; import :HTTP; -import :ListenerQUIC; -import :ClientQUIC; -import :WebTransport; +import :ClientTCP; -#ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { - // HTTP/3 server. Wraps a ListenerQUIC: each accepted QUIC connection - // registers a per-stream handler that parses one request, dispatches it - // through the route map, and writes a response back on the same bidi - // stream. ALPN is fixed to "h3". - // - // Routes are keyed by `:path` (exact match). Unknown paths return a - // synthetic 404. Route handlers run on the ThreadPool — multiple requests - // on the same connection can therefore execute concurrently. - // - // WebTransport: pass a non-empty `wtRoutes` to additionally accept - // extended-CONNECT requests (`:method=CONNECT, :protocol=webtransport`) - // whose `:path` matches a registered route. The matching handler runs - // on the ThreadPool with a `WebTransportSession&` argument scoped to - // the session's lifetime. Sending WT-required SETTINGS happens - // automatically when wtRoutes is non-empty. - export class ListenerHTTP { - public: - // The underlying QUIC listener owns the accept loop, certificates, - // and the per-connection ClientQUIC instances. It is heap-allocated - // and owned by this Impl so that move construction/destruction is - // straightforward. - std::unordered_map> routes; - std::unordered_map> wtRoutes; - std::string alpn; + export class ListenerHTTP; + class ListenerHTTPClient { + public: + std::atomic disconnected; + ClientTCP client; + std::thread thread; + ListenerHTTP* server; + ListenerHTTPClient(ListenerHTTP* server, int s); + void ListenRoutes(); + }; - ListenerHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes); + export class ListenerHTTP { + public: + int s; + std::vector clients; + bool running = true; + const std::unordered_map> routes; + ListenerHTTP(std::uint16_t port, std::unordered_map> routes); + ~ListenerHTTP(); + void Listen(); + void Stop(); + }; - // WT-aware overload. `routes` and `wtRoutes` may both be non-empty; - // they are dispatched on disjoint criteria so they don't collide. - ListenerHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes, - std::unordered_map> wtRoutes); - - ~ListenerHTTP(); - ListenerHTTP(const ListenerHTTP&) = delete; - ListenerHTTP(ListenerHTTP&&) noexcept; - - // Block on this thread, dispatch each accepted connection on this - // thread (matches ListenerQUIC::ListenSyncSync semantics). - void Listen(); - void Stop(); - - private: - struct Impl; - std::unique_ptr impl; - }; - - // Async wrapper: runs the listener's accept loop on a background thread - // so the caller can construct it and continue. Mirrors the old - // ListenerAsyncHTTP so existing call sites keep working. - export class ListenerAsyncHTTP { - public: - ListenerHTTP listener; - std::thread thread; - - ListenerAsyncHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes); - - // WT-aware overload. - ListenerAsyncHTTP(std::uint16_t port, - QUICServerCredentials creds, - std::unordered_map> routes, - std::unordered_map> wtRoutes); - - ~ListenerAsyncHTTP(); - void Stop(); - }; + export class ListenerAsyncHTTP { + public: + ListenerHTTP listener; + std::thread thread; + ListenerAsyncHTTP(std::uint16_t port, std::unordered_map> routes); + ~ListenerAsyncHTTP(); + void Stop(); + }; } -#endif diff --git a/interfaces/Crafter.Network-ListenerQUIC.cppm b/interfaces/Crafter.Network-ListenerQUIC.cppm index 746e47b..49b3bc1 100644 --- a/interfaces/Crafter.Network-ListenerQUIC.cppm +++ b/interfaces/Crafter.Network-ListenerQUIC.cppm @@ -19,14 +19,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifndef CRAFTER_NETWORK_BROWSER #include -#endif export module Crafter.Network:ListenerQUIC; import std; import :ClientQUIC; -#ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { // Server side of a QUIC connection. Mirrors ListenerTCP in shape: // four Listen* methods covering the sync/async outer-loop x sync/async @@ -75,17 +72,4 @@ namespace Crafter { std::unique_ptr impl; std::uint32_t totalClientCounter = 0; }; - - // Compute the SHA-256 of the DER bytes of a PEM-encoded X.509 certificate. - // Returns the 32-byte digest. Intended for surfacing the self-signed cert - // hash to a browser peer (Chrome's WebTransport requires the client to - // pass this hash via `serverCertificateHashes` when peering against a - // cert that's not in the system trust store). Shells out to openssl. - export std::array ComputeCertificateHashSHA256(const std::string& certPath); - - // Path of the lazily-generated self-signed cert (PEM). Triggers generation - // on first call. Useful for piping into ComputeCertificateHashSHA256 so - // a browser peer can be told the hash to put in `serverCertificateHashes`. - export std::string GetSelfSignedCertificatePath(); } -#endif diff --git a/interfaces/Crafter.Network-ListenerTCP.cppm b/interfaces/Crafter.Network-ListenerTCP.cppm index 6acfd3c..007d6e7 100755 --- a/interfaces/Crafter.Network-ListenerTCP.cppm +++ b/interfaces/Crafter.Network-ListenerTCP.cppm @@ -22,7 +22,6 @@ export module Crafter.Network:ListenerTCP; import std; import :ClientTCP; -#ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { export class ListenerTCP { public: @@ -41,5 +40,4 @@ namespace Crafter { std::uint32_t totalClientCounter = 0; int s; }; -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Network-WebTransport.cppm b/interfaces/Crafter.Network-WebTransport.cppm deleted file mode 100644 index 9e87e57..0000000 --- a/interfaces/Crafter.Network-WebTransport.cppm +++ /dev/null @@ -1,105 +0,0 @@ -/* -Crafter®.Network -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -export module Crafter.Network:WebTransport; -import std; -import :ClientQUIC; - -#ifndef CRAFTER_NETWORK_BROWSER -namespace Crafter { - // Server-side handle to one accepted WebTransport-over-HTTP/3 session. - // Constructed by ListenerHTTP when it receives an extended-CONNECT - // request whose :path matches a registered WT route. Handed to the - // user's route handler as the only argument. - // - // API shape mirrors ClientQUIC so application code can be written once - // and used on either side of the wire — open bidi streams, register an - // OnStream handler for peer-initiated streams, close the session. - // - // Lifetime: the session owns the CONNECT stream that was upgraded. - // Destruction (or explicit Close()) FINs that stream, which the peer - // interprets as session-end. Phase 1 does not emit a CLOSE_WEBTRANSPORT - // _SESSION capsule — bare FIN is sufficient for Chrome / Firefox. - // - // Phase 1 scope: - // - bidirectional streams: OpenStream + OnStream - // - session close via Close() / destruction - // Out of scope (later phases): - // - datagrams (SendDatagram / OnDatagram are stubs that no-op) - // - unidirectional streams (OpenStream(unidirectional=true) throws) - // - capsule protocol (DRAIN/CLOSE capsules) - export class WebTransportSession { - public: - // Underlying QUIC stream id of the CONNECT stream. The peer - // identifies streams that belong to this session by this number. - std::uint64_t sessionId = 0; - - // Path the client connected to. Useful for routing within a single - // wtRoutes handler that's registered against multiple paths. - std::string path; - - WebTransportSession(); - ~WebTransportSession(); - WebTransportSession(const WebTransportSession&) = delete; - WebTransportSession(WebTransportSession&&) noexcept; - WebTransportSession& operator=(WebTransportSession&&) noexcept; - - // Open a new bidi stream toward the peer. The WT_STREAM prefix - // (frame type + session id) is written to the stream automatically - // before this returns; the caller's first Send* delivers the first - // bytes of opaque payload. Throws on connection close. - QUICStream OpenStream(bool unidirectional = false); - - // Register a handler for streams the peer opens against this - // session. Already-buffered streams that arrived before the - // handler was installed are drained into the new handler. - void OnStream(std::function callback); - - // Register a handler for datagrams the peer sends on this - // session. Phase 1 STUB — datagrams are not yet plumbed through. - void OnDatagram(std::function)> callback); - - // Send a datagram. Phase 1 STUB — silently drops. - void SendDatagram(const void* buffer, std::uint32_t size); - - // FIN the CONNECT stream. Subsequent OpenStream calls throw; any - // pending receivers on owned streams will fail with the connection - // close. Idempotent. - void Close(); - - private: - struct Impl; - std::unique_ptr impl; - friend class ListenerHTTP; - friend void WebTransportInitialise(WebTransportSession&, ClientQUIC*, QUICStream, - std::uint64_t, std::string); - friend void WebTransportDeliverStream(WebTransportSession&, QUICStream); - }; - - // Internal — used by ListenerHTTP's WT demuxer. Not exported (and only - // visible to other TUs within the Crafter.Network module). - void WebTransportInitialise(WebTransportSession& session, - ClientQUIC* connection, - QUICStream connectStream, - std::uint64_t sessionId, - std::string path); - void WebTransportDeliverStream(WebTransportSession& session, QUICStream stream); -} -#endif diff --git a/interfaces/Crafter.Network.cppm b/interfaces/Crafter.Network.cppm index a6713ad..1c6d696 100755 --- a/interfaces/Crafter.Network.cppm +++ b/interfaces/Crafter.Network.cppm @@ -26,12 +26,4 @@ export import :ClientHTTP; export import :ListenerHTTP; export import :HTTP; export import :ClientQUIC; -export import :ListenerQUIC; -export import :WebTransport; -#ifndef CRAFTER_NETWORK_BROWSER -// Exposed so user code can build WebTransport clients by hand against a -// ClientQUIC until we ship a ClientWebTransport wrapper. Most callers do -// not need the HTTP/3 frame helpers directly. Excluded from the browser -// build — HTTP3 uses throw and the wasm target runs with -fno-exceptions. -export import :HTTP3; -#endif \ No newline at end of file +export import :ListenerQUIC; \ No newline at end of file diff --git a/project.cpp b/project.cpp index 24179e4..fd17dc6 100644 --- a/project.cpp +++ b/project.cpp @@ -4,17 +4,23 @@ namespace fs = std::filesystem; using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { - constexpr std::array networkInterfaces = { + constexpr std::array networkInterfaces = { "interfaces/Crafter.Network", "interfaces/Crafter.Network-ClientTCP", "interfaces/Crafter.Network-ListenerTCP", "interfaces/Crafter.Network-ClientHTTP", "interfaces/Crafter.Network-ListenerHTTP", "interfaces/Crafter.Network-HTTP", - "interfaces/Crafter.Network-HTTP3", "interfaces/Crafter.Network-ClientQUIC", "interfaces/Crafter.Network-ListenerQUIC", - "interfaces/Crafter.Network-WebTransport", + }; + constexpr std::array networkImplementations = { + "implementations/Crafter.Network-ClientTCP", + "implementations/Crafter.Network-ListenerTCP", + "implementations/Crafter.Network-ClientHTTP", + "implementations/Crafter.Network-ListenerHTTP", + "implementations/Crafter.Network-ClientQUIC", + "implementations/Crafter.Network-ListenerQUIC", }; std::vector depArgs(args.begin(), args.end()); @@ -31,51 +37,6 @@ extern "C" Configuration CrafterBuildProject(std::span a ApplyStandardArgs(cfg, args); cfg.dependencies = { thread }; - // Browser path: any wasm32-* target gets the browser network stack - // (fetch + WebTransport via JS glue). msquic and the POSIX socket - // backends are skipped; the listener / TCP partitions stub to empty - // modules via #ifdef CRAFTER_NETWORK_BROWSER in their interface files. - // HTTP3 (varint / frame / QPACK codec) is dropped entirely — it threw - // exceptions for protocol errors, which the wasm build's -fno-exceptions - // forbids, and the browser's fetch() handles HTTP-layer framing itself. - bool browser = cfg.target.find("wasm") != std::string::npos; - if (browser) { - cfg.defines.push_back({"CRAFTER_NETWORK_BROWSER", ""}); - - std::array browserIfaces = { - "interfaces/Crafter.Network", - "interfaces/Crafter.Network-ClientTCP", - "interfaces/Crafter.Network-ListenerTCP", - "interfaces/Crafter.Network-ClientHTTP", - "interfaces/Crafter.Network-ListenerHTTP", - "interfaces/Crafter.Network-HTTP", - "interfaces/Crafter.Network-ClientQUIC", - "interfaces/Crafter.Network-ListenerQUIC", - "interfaces/Crafter.Network-WebTransport", - }; - std::array browserImpls = { - "implementations/Crafter.Network-ClientHTTP-Browser", - "implementations/Crafter.Network-ClientQUIC-Browser", - }; - cfg.GetInterfacesAndImplementations(browserIfaces, browserImpls); - - // JS glue shipped alongside the .wasm. The consuming executable's - // wasi-browser runtime merges this into the env import object - // before instantiation (mirrors Crafter.Graphics/dom-env.js). - cfg.files.emplace_back(fs::path("additional/network-env.js")); - return cfg; - } - - constexpr std::array networkImplementations = { - "implementations/Crafter.Network-ClientTCP", - "implementations/Crafter.Network-ListenerTCP", - "implementations/Crafter.Network-ClientHTTP", - "implementations/Crafter.Network-ListenerHTTP", - "implementations/Crafter.Network-ClientQUIC", - "implementations/Crafter.Network-ListenerQUIC", - "implementations/Crafter.Network-WebTransport", - }; - // msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC. // Cloned + built via CMake into the per-project external cache; no system // package required. Submodules (quictls / clog / etc.) come via the @@ -100,9 +61,9 @@ extern "C" Configuration CrafterBuildProject(std::span a // linker at the actual output location. msquic.libDirs = { "bin/Release" }; msquic.libs = { "msquic" }; - std::array ifaces; + std::array ifaces; std::ranges::copy(networkInterfaces, ifaces.begin()); - std::array impls; + std::array impls; std::ranges::copy(networkImplementations, impls.begin()); cfg.GetInterfacesAndImplementations(ifaces, impls); diff --git a/tests/ShouldEchoWebTransport/ShouldEchoWebTransport.cpp b/tests/ShouldEchoWebTransport/ShouldEchoWebTransport.cpp deleted file mode 100644 index d1daa22..0000000 --- a/tests/ShouldEchoWebTransport/ShouldEchoWebTransport.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* -Crafter® Build -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -// End-to-end WebTransport echo. Spins up a ListenerHTTP with a wtRoutes -// handler that echoes back whatever the peer sent on each new bidi -// stream, then drives a hand-rolled client side using raw ClientQUIC + -// HTTP3 framing (there's no ClientWebTransport class yet; that's Phase 4). -// Verifies: -// - extended CONNECT is accepted, 200 OK delivered without FIN -// - WT_STREAM bidi framing parses correctly on both sides -// - echoed payload round-trips byte-for-byte - -import Crafter.Network; -import Crafter.Thread; -import std; -using namespace Crafter; - -namespace { - // Helper: read one HTTP/3 frame off `stream` into a freshly-allocated - // buffer. Returns (frameType, payload). The peeked-but-unconsumed - // tail bytes (e.g. start of the next frame) are PrependReceived'd - // back onto the stream. - std::pair> ReadFrame(QUICStream& stream) { - std::vector buf; - // First varint = frame type. - std::uint64_t type = 0; std::size_t cn = 0; - while (true) { - const auto* p = reinterpret_cast(buf.data()); - if (HTTP3::DecodeVarint(p, buf.size(), type, cn)) break; - auto chunk = stream.RecieveSync(); - buf.insert(buf.end(), chunk.begin(), chunk.end()); - } - std::vector afterType(buf.begin() + cn, buf.end()); - buf = std::move(afterType); - - // Second varint = frame length. - std::uint64_t len = 0; std::size_t lc = 0; - while (true) { - const auto* p = reinterpret_cast(buf.data()); - if (HTTP3::DecodeVarint(p, buf.size(), len, lc)) break; - auto chunk = stream.RecieveSync(); - buf.insert(buf.end(), chunk.begin(), chunk.end()); - } - std::vector afterLen(buf.begin() + lc, buf.end()); - buf = std::move(afterLen); - - // Read enough for the full payload. - while (buf.size() < len) { - auto chunk = stream.RecieveSync(); - buf.insert(buf.end(), chunk.begin(), chunk.end()); - } - - std::vector payload(buf.begin(), buf.begin() + len); - std::vector tail(buf.begin() + len, buf.end()); - if (!tail.empty()) stream.PrependReceived(std::move(tail)); - return {type, std::move(payload)}; - } -} - -int main() { - ThreadPool::Start(); - - constexpr std::string_view kPayload = "hello-webtransport"; - constexpr std::uint16_t kPort = 8083; - - // ── Server ──────────────────────────────────────────────────────── - QUICServerCredentials serverCreds; - serverCreds.selfSigned = true; - - std::unordered_map> httpRoutes = {}; - std::unordered_map> wtRoutes = { - {"/echo", [](WebTransportSession& session) { - session.OnStream([](QUICStream peerStream) { - try { - auto bytes = peerStream.RecieveUntilCloseSync(); - peerStream.SendSync(bytes.data(), - static_cast(bytes.size()), - /*finish=*/true); - } catch (...) {} - }); - }}, - }; - ListenerAsyncHTTP listener(kPort, serverCreds, std::move(httpRoutes), std::move(wtRoutes)); - - try { - // ── Client (hand-rolled WT bring-up over raw ClientQUIC) ────── - QUICClientCredentials clientCreds; - clientCreds.insecureNoServerValidation = true; - ClientQUIC quic("localhost", kPort, std::string(HTTP3::kAlpn), clientCreds); - - // Drain peer-initiated unidi streams (its control + QPACK streams). - // Without this they'd back up and msquic might abort the connection. - quic.OnStream([](QUICStream stream) { - try { while (true) (void)stream.RecieveSync(); } catch (...) {} - }); - - // Our outgoing control stream + WT-aware SETTINGS prelude. - QUICStream controlStream = quic.OpenStream(/*unidirectional=*/true); - auto prelude = HTTP3::BuildWebTransportControlStreamPrelude(/*maxSessions=*/1); - controlStream.SendSync(prelude.data(), - static_cast(prelude.size()), - /*finish=*/false); - - // CONNECT request stream. Send HEADERS, do NOT FIN — the stream - // is the session-control stream and stays open for its lifetime. - QUICStream connectStream = quic.OpenStream(/*unidirectional=*/false); - std::vector> connectFields = { - {":method", "CONNECT"}, - {":scheme", "https"}, - {":authority", "localhost"}, - {":path", "/echo"}, - {":protocol", "webtransport"}, - }; - auto headerPayload = HTTP3::EncodeFieldSection(connectFields); - std::vector connectWire; - HTTP3::WriteFrame(connectWire, HTTP3::kFrameHeaders, - headerPayload.data(), headerPayload.size()); - connectStream.SendSync(connectWire.data(), - static_cast(connectWire.size()), - /*finish=*/false); - - // Read the response HEADERS frame. - auto [respType, respPayload] = ReadFrame(connectStream); - if (respType != HTTP3::kFrameHeaders) { - std::println("bad response frame type: {}", respType); - return 1; - } - auto respFields = HTTP3::DecodeFieldSection( - reinterpret_cast(respPayload.data()), - respPayload.size()); - std::string status; - for (auto& [k, v] : respFields) if (k == ":status") status = v; - if (status != "200") { - std::println("CONNECT rejected with status {}", status); - return 1; - } - - // Session is ready. session_id equals the CONNECT stream's QUIC - // stream id — same number on both ends of the wire. - std::uint64_t sessionId = connectStream.GetStreamId(); - - // Open a WT data bidi stream. Prefix: varint(0x41) varint(sessionId). - QUICStream wtStream = quic.OpenStream(/*unidirectional=*/false); - auto prefix = HTTP3::BuildWtBidiPrefix(sessionId); - - std::vector wire; - wire.insert(wire.end(), prefix.begin(), prefix.end()); - wire.insert(wire.end(), kPayload.begin(), kPayload.end()); - wtStream.SendSync(wire.data(), - static_cast(wire.size()), - /*finish=*/true); - - // Server echoes the payload (the prefix has already been stripped - // server-side; the bytes we read here are pure echo). - auto echoed = wtStream.RecieveUntilCloseSync(); - std::string got(echoed.begin(), echoed.end()); - if (got == kPayload) { - std::_Exit(0); - } - std::println("payload mismatch: expected '{}', got '{}'", kPayload, got); - return 1; - } catch (std::exception& e) { - std::println("client failed: {}", e.what()); - return 1; - } -} diff --git a/tests/ShouldRecieveHTTP/ShouldRecieveHTTP.cpp b/tests/ShouldRecieveHTTP/ShouldRecieveHTTP.cpp new file mode 100644 index 0000000..d405537 --- /dev/null +++ b/tests/ShouldRecieveHTTP/ShouldRecieveHTTP.cpp @@ -0,0 +1,43 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include +#include +import Crafter.Network; +import std; +using namespace Crafter; + +int main() { + bool success = false; + ListenerAsyncHTTP listener(8081, {{"/", [&](const HTTPRequest& request) { + success = true; + return CreateResponseHTTP("200 OK", "Hello World!"); + }}}); + try { + system("curl http://localhost:8081 > /dev/null 2>&1"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (success) { + return 0; + } + std::println("Did not receive"); + return 1; + } catch (std::exception& e) { + std::println("{}", e.what()); + return 1; + } +} \ No newline at end of file diff --git a/tests/ShouldEchoWebTransport/project.cpp b/tests/ShouldRecieveHTTP/project.cpp similarity index 72% rename from tests/ShouldEchoWebTransport/project.cpp rename to tests/ShouldRecieveHTTP/project.cpp index b0ca04d..c083256 100644 --- a/tests/ShouldEchoWebTransport/project.cpp +++ b/tests/ShouldRecieveHTTP/project.cpp @@ -5,16 +5,16 @@ using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span) { Configuration cfg; - cfg.path = "tests/ShouldEchoWebTransport/"; - cfg.name = "ShouldEchoWebTransport"; - cfg.outputName = "ShouldEchoWebTransport"; + cfg.path = "tests/ShouldRecieveHTTP/"; + cfg.name = "ShouldRecieveHTTP"; + cfg.outputName = "ShouldRecieveHTTP"; cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; cfg.dependencies = { ParentLib("crafter-network") }; cfg.linkFlags.push_back("-Wl,--export-dynamic"); cfg.linkFlags.push_back("-ldl"); std::array ifaces = {}; - std::array impls = { "ShouldEchoWebTransport" }; + std::array impls = { "ShouldRecieveHTTP" }; cfg.GetInterfacesAndImplementations(ifaces, impls); return cfg; } diff --git a/tests/ShouldSend/ShouldSend.cpp b/tests/ShouldSend/ShouldSend.cpp deleted file mode 100644 index c4f6799..0000000 --- a/tests/ShouldSend/ShouldSend.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* -Crafter® Build -Copyright (C) 2026 Catcrafts® -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ -import Crafter.Network; -import Crafter.Thread; -import std; -using namespace Crafter; - -// External-interop smoke test: connect to a public h3 endpoint, fetch /, and -// verify a 200 response with a non-empty body. Exercises: -// - real TLS chain validation against the system trust store -// - mandatory client control stream + SETTINGS prelude -// - peer's control + QPACK encoder/decoder unidi streams (drained) -// - QPACK Huffman decode on the response headers -// -// Targets cloudflare-quic.com (Cloudflare's public h3 demo). Network- -// dependent — if outbound UDP/443 is firewalled or the endpoint goes away, -// this will fail. -int main() { - ThreadPool::Start(); - try { - QUICClientCredentials creds; // default: validate against system trust - ClientHTTP client("cloudflare-quic.com", 443, creds); - HTTPResponse r = client.Send( - CreateRequestHTTP("GET", "/", "cloudflare-quic.com") - ); - std::cout << "status=" << r.status << " bodyBytes=" << r.body.size() << std::endl; - if (r.headers.count("server")) { - std::cout << "server=" << r.headers["server"] << std::endl; - } - if (r.body.size() > 0) { - auto preview = r.body.substr(0, std::min(80, r.body.size())); - std::cout << "preview: " << preview << std::endl; - } - if (r.status != "200" || r.body.empty()) { - std::cout << "unexpected response" << std::endl; - return 1; - } - std::cout.flush(); - std::_Exit(0); - } catch (std::exception& e) { - std::println("error: {}", e.what()); - return 1; - } -} diff --git a/tests/ShouldSendHTTP/ShouldSendHTTP.cpp b/tests/ShouldSendHTTP/ShouldSendHTTP.cpp new file mode 100644 index 0000000..51e32b2 --- /dev/null +++ b/tests/ShouldSendHTTP/ShouldSendHTTP.cpp @@ -0,0 +1,31 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +import Crafter.Network; +import std; +using namespace Crafter; + +int main() { + ClientHTTP client("httpbin.org", 80); + HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/get", "httpbin.org")); + if (response.status == "200 OK") { + return 0; + } + std::println("{}", response.body); + return 1; +} \ No newline at end of file diff --git a/tests/ShouldSend/project.cpp b/tests/ShouldSendHTTP/project.cpp similarity index 76% rename from tests/ShouldSend/project.cpp rename to tests/ShouldSendHTTP/project.cpp index 1dffb04..aa4b825 100644 --- a/tests/ShouldSend/project.cpp +++ b/tests/ShouldSendHTTP/project.cpp @@ -5,16 +5,16 @@ using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span) { Configuration cfg; - cfg.path = "tests/ShouldSend/"; - cfg.name = "ShouldSend"; - cfg.outputName = "ShouldSend"; + cfg.path = "tests/ShouldSendHTTP/"; + cfg.name = "ShouldSendHTTP"; + cfg.outputName = "ShouldSendHTTP"; cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; cfg.dependencies = { ParentLib("crafter-network") }; cfg.linkFlags.push_back("-Wl,--export-dynamic"); cfg.linkFlags.push_back("-ldl"); std::array ifaces = {}; - std::array impls = { "ShouldSend" }; + std::array impls = { "ShouldSendHTTP" }; cfg.GetInterfacesAndImplementations(ifaces, impls); return cfg; } diff --git a/tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp b/tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp index a626f67..e50ebef 100644 --- a/tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp +++ b/tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp @@ -16,34 +16,26 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include +#include import Crafter.Network; -import Crafter.Thread; import std; using namespace Crafter; int main() { - ThreadPool::Start(); - - QUICServerCredentials serverCreds; - serverCreds.selfSigned = true; - ListenerAsyncHTTP listener(8082, serverCreds, {{"/", [&](const HTTPRequest& request) { - return CreateResponseHTTP("200", "Hello World!"); + ListenerAsyncHTTP listener(8082, {{"/", [&](const HTTPRequest& request) { + return CreateResponseHTTP("200 OK", "Hello World!"); }}}); try { - QUICClientCredentials clientCreds; - clientCreds.insecureNoServerValidation = true; - ClientHTTP client("localhost", 8082, clientCreds); + ClientHTTP client("localhost", 8082); HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost")); - if (response.status == "200" && response.body == "Hello World!") { - // See ShouldSendRecieveQUICStream for rationale: msquic's - // RegistrationClose blocks on outstanding connections, so skip - // graceful teardown after the test logic succeeds. - std::_Exit(0); + if (response.status == "200 OK" && response.body == "Hello World!") { + return 0; } - std::println("{} {}", response.status, response.body); + std::println("{}{}", response.status, response.body); return 1; } catch (std::exception& e) { std::println("{}", e.what()); return 1; } -} +} \ No newline at end of file diff --git a/tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp b/tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp index 6a23889..742ef55 100644 --- a/tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp +++ b/tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp @@ -16,41 +16,33 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include +#include import Crafter.Network; -import Crafter.Thread; import std; using namespace Crafter; -// "Keep-alive" in HTTP/3 corresponds to the QUIC connection being multiplexed: -// successive client.Send() calls reuse the same connection and open new -// request streams within it. This test exercises that — two requests on one -// ClientHTTP must both succeed. int main() { - ThreadPool::Start(); - - QUICServerCredentials serverCreds; - serverCreds.selfSigned = true; - ListenerAsyncHTTP listener(8083, serverCreds, {{"/", [&](const HTTPRequest& request) { - return CreateResponseHTTP("200", "Hello World!"); + ListenerAsyncHTTP listener(8083, {{"/", [&](const HTTPRequest& request) { + return CreateResponseHTTP("200 OK", "Hello World!"); }}}); try { - QUICClientCredentials clientCreds; - clientCreds.insecureNoServerValidation = true; - ClientHTTP client("localhost", 8083, clientCreds); - + ClientHTTP client("localhost", 8083); HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost")); - if (response.status != "200" || response.body != "Hello World!") { - std::println("{} {}", response.status, response.body); + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (response.status != "200 OK" || response.body != "Hello World!") { + std::println("{}{}", response.status, response.body); return 1; } response = client.Send(CreateRequestHTTP("GET", "/", "localhost")); - if (response.status != "200" || response.body != "Hello World!") { - std::println("{} {}", response.status, response.body); + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (response.status != "200 OK" || response.body != "Hello World!") { + std::println("{}{}", response.status, response.body); return 1; } - std::_Exit(0); + return 0; } catch (std::exception& e) { std::println("{}", e.what()); return 1; } -} +} \ No newline at end of file diff --git a/tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp b/tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp index 5d0c265..7b6f003 100644 --- a/tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp +++ b/tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp @@ -16,31 +16,28 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include +#include import Crafter.Network; -import Crafter.Thread; import std; using namespace Crafter; int main() { - ThreadPool::Start(); - - QUICServerCredentials serverCreds; - serverCreds.selfSigned = true; - ListenerAsyncHTTP listener(8084, serverCreds, {{ "/", [&](const HTTPRequest& request) { - if (request.body.size() > 1'000'000) { - return CreateResponseHTTP("200", "Large request received: " + std::to_string(request.body.size()) + " bytes"); + ListenerAsyncHTTP listener(8084, {{ "/", [&](const HTTPRequest& request) { + if (request.body.size() > 1'000'000) { + return CreateResponseHTTP("200 OK", "Large request received: " + std::to_string(request.body.size()) + " bytes"); + } + return CreateResponseHTTP("200 OK", "Small request received"); } - return CreateResponseHTTP("200", "Small request received"); - }}}); + }}); try { - QUICClientCredentials clientCreds; - clientCreds.insecureNoServerValidation = true; - ClientHTTP client("localhost", 8084, clientCreds); + ClientHTTP client("localhost", 8084); std::string large_body(10 * 1024 * 1024, 'A'); HTTPResponse response = client.Send(CreateRequestHTTP("POST", "/", "localhost", large_body)); - if (response.status == "200" && response.body.find("Large request received") != std::string::npos) { - std::_Exit(0); + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (response.status == "200 OK" && response.body.find("Large request received") != std::string::npos) { + return 0; } std::println("Unexpected response: {} {}", response.status, response.body); return 1; @@ -48,4 +45,4 @@ int main() { std::println("{}", e.what()); return 1; } -} +} \ No newline at end of file