https #6
1 changed files with 55 additions and 10 deletions
docs: describe HTTPS, the TLS layer, and its system dependency
The HTTP/1.1 section promised the opposite of what the code now does — a "No TLS" heading stating there was no plan to link a TLS stack into this path. Replace it with what to actually pass, and lead with the part that gets deployed wrong: verifying the chain without the hostname is not a check, and a private trust anchor is the answer for a self-signed peer rather than insecureNoServerValidation. Also document :Stream and :TLS as modules in their own right — TLSStream is a ByteStream over any descriptor, not something only HTTP can use — and record libssl as a system dependency, including why it is not vendored the way msquic is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit
331cf0c66c
65
README.md
65
README.md
|
|
@ -11,7 +11,7 @@ Crafter.Network is a C++ networking library designed for modern C++ applications
|
||||||
- **TCP Networking**: Client and server implementations for raw TCP connections (native only).
|
- **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.
|
- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync.
|
||||||
- **HTTP/3**: Client and server implementations on top of QUIC. Uses ALPN `h3`, QUIC bidi streams for requests/responses, the mandatory unidirectional control stream + SETTINGS frame (RFC 9114 §6.2.1), the (empty) QPACK encoder + decoder unidi streams required by stricter peers like Chromium, and a built-in QPACK codec (RFC 9204) with the full static table, Huffman *decoding* (RFC 7541), and literal-only emission. The QPACK dynamic table is unused. The client is interoperable with mainstream public h3 endpoints (cloudflare, nghttp3-based servers, etc.).
|
- **HTTP/3**: Client and server implementations on top of QUIC. Uses ALPN `h3`, QUIC bidi streams for requests/responses, the mandatory unidirectional control stream + SETTINGS frame (RFC 9114 §6.2.1), the (empty) QPACK encoder + decoder unidi streams required by stricter peers like Chromium, and a built-in QPACK codec (RFC 9204) with the full static table, Huffman *decoding* (RFC 7541), and literal-only emission. The QPACK dynamic table is unused. The client is interoperable with mainstream public h3 endpoints (cloudflare, nghttp3-based servers, etc.).
|
||||||
- **HTTP/1.1**: Client and server over plain TCP (RFC 9112), for the large part of the world that is not ready for HTTP/3 — old proxies, CI tooling, load balancers, `curl` scripts. Shares the `HTTPRequest`/`HTTPResponse` types and the route-map API — including the `fallback` hook for paths that cannot be enumerated — with the HTTP/3 stack, so a handler or call site moves between the two by changing the class name. Keep-alive and pipelining, `content-length` and `chunked` bodies with trailers, `Expect: 100-continue`, HEAD, automatic `Date`, and per-connection timeouts. Plaintext only — see [HTTP/1.1 Components](#http11-components).
|
- **HTTP/1.1**: Client and server over plain TCP (RFC 9112), for the large part of the world that is not ready for HTTP/3 — old proxies, CI tooling, load balancers, `curl` scripts. Shares the `HTTPRequest`/`HTTPResponse` types and the route-map API — including the `fallback` hook for paths that cannot be enumerated — with the HTTP/3 stack, so a handler or call site moves between the two by changing the class name. Keep-alive and pipelining, `content-length` and `chunked` bodies with trailers, `Expect: 100-continue`, HEAD, automatic `Date`, and per-connection timeouts. `https://` on both sides via libssl, including ALPN and mutual TLS — see [HTTP/1.1 Components](#http11-components).
|
||||||
- **WebTransport (server)**: `ListenerHTTP` accepts extended-CONNECT sessions (`:method=CONNECT, :protocol=webtransport`) negotiated on the existing h3 listener — no separate port or alternate stack. Both draft-02 and draft-07+ identifier sets are advertised in SETTINGS so current Chrome/Edge browsers connect out of the box. Per-route handlers receive a `WebTransportSession&` and can multiplex bidirectional streams over the session.
|
- **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.
|
- **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).
|
- **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).
|
||||||
|
|
@ -29,8 +29,10 @@ The library follows a modular design using C++20 modules:
|
||||||
- `Crafter.Network:ClientHTTP`: HTTP/3 client (ALPN `h3`). On browser builds this maps to `fetch()`.
|
- `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:ListenerHTTP`: HTTP/3 + WebTransport server (ALPN `h3`, native only)
|
||||||
- `Crafter.Network:HTTP`: HTTP request/response types, constructors, and `PathWithoutQueryHTTP`, shared by every HTTP version
|
- `Crafter.Network:HTTP`: HTTP request/response types, constructors, and `PathWithoutQueryHTTP`, shared by every HTTP version
|
||||||
- `Crafter.Network:ClientHTTP1`: HTTP/1.1 client over TCP (native only)
|
- `Crafter.Network:ClientHTTP1`: HTTP/1.1 client over TCP, plaintext or TLS (native only)
|
||||||
- `Crafter.Network:ListenerHTTP1`: HTTP/1.1 server over TCP (native only)
|
- `Crafter.Network:ListenerHTTP1`: HTTP/1.1 server over TCP, plaintext or TLS (native only)
|
||||||
|
- `Crafter.Network:Stream`: `ByteStream` — a reliable byte stream with deadlines on both directions — plus `PlainStream`, the plaintext socket implementation. The seam that lets one HTTP/1.1 implementation serve `http://` and `https://` (native only)
|
||||||
|
- `Crafter.Network:TLS`: TLS over a connected socket via libssl (OpenSSL 3): `TLSContext`, `TLSStream`, and the credential types for both roles. Protocol-agnostic — usable over any descriptor, not just HTTP (native only)
|
||||||
- `Crafter.Network:HTTP1`: HTTP/1.1 wire format — serialisation plus an incremental parser (RFC 9112). Transport-free; usable on its own to speak HTTP/1.1 over some other byte stream. Native only, for the same reason as `:HTTP3`
|
- `Crafter.Network:HTTP1`: HTTP/1.1 wire format — serialisation plus an incremental parser (RFC 9112). Transport-free; usable on its own to speak HTTP/1.1 over some other byte stream. Native only, for the same reason as `:HTTP3`
|
||||||
- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams. On browser builds this maps to the `WebTransport` JS API.
|
- `Crafter.Network: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:ListenerQUIC`: QUIC listener accepting incoming connections (native only). Also exports `ComputeCertificateHashSHA256()` and `GetSelfSignedCertificatePath()` for browser-peer cert pinning.
|
||||||
|
|
@ -95,7 +97,7 @@ The `HTTPRequest` exposes the four HTTP/3 pseudo-headers (`method`, `scheme`, `a
|
||||||
|
|
||||||
### HTTP/1.1 Components
|
### HTTP/1.1 Components
|
||||||
|
|
||||||
HTTP/3 is not something every peer can be made to speak. `ClientHTTP1` and `ListenerHTTP1` provide the same API over plain TCP, using the same `HTTPRequest`/`HTTPResponse` types and the same route-map shape, so a handler can be registered with both and served over either protocol.
|
HTTP/3 is not something every peer can be made to speak. `ClientHTTP1` and `ListenerHTTP1` provide the same API over TCP — with or without TLS — using the same `HTTPRequest`/`HTTPResponse` types and the same route-map shape, so a handler can be registered with both and served over either protocol.
|
||||||
|
|
||||||
#### ClientHTTP1
|
#### ClientHTTP1
|
||||||
```cpp
|
```cpp
|
||||||
|
|
@ -106,7 +108,7 @@ Crafter::HTTPResponse response = client.Send(
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
The connection is persistent: the first `Send()` dials, and later calls reuse the socket unless the peer asked for it to be closed. A pooled connection that the peer closed while it looked idle — the race HTTP/1.1 keep-alive cannot avoid — is redialled once and the request replayed; nothing is replayed once a response byte has arrived, and a freshly dialled connection is never retried on, so a genuinely broken server surfaces as an exception rather than a retry loop. `authority` defaults to the host:port the client was constructed with (the port is elided when it is 80).
|
The connection is persistent: the first `Send()` dials, and later calls reuse the socket unless the peer asked for it to be closed. A pooled connection that the peer closed while it looked idle — the race HTTP/1.1 keep-alive cannot avoid — is redialled once and the request replayed; nothing is replayed once a response byte has arrived, and a freshly dialled connection is never retried on, so a genuinely broken server surfaces as an exception rather than a retry loop. `authority` defaults to the host:port the client was constructed with (the port is elided when it is the scheme default — 443 under TLS, 80 without).
|
||||||
|
|
||||||
#### ListenerHTTP1
|
#### ListenerHTTP1
|
||||||
```cpp
|
```cpp
|
||||||
|
|
@ -123,13 +125,52 @@ Each accepted connection gets its own thread and is served sequentially until th
|
||||||
|
|
||||||
Routing matches `path` exactly and then falls back to the query-stripped path, so `/thing?x=1` reaches the handler registered for `/thing` while the handler still sees the full target in `request.path`. A handler that throws becomes a 500; an unknown path a 404 (or `fallback`, see [Routes that cannot be enumerated](#routes-that-cannot-be-enumerated)); a request we refuse to parse a 400. `Date` is stamped automatically unless the handler set one, HEAD returns the headers a GET would have produced with no body, and a handler can end the connection by answering with a `connection: close` header.
|
Routing matches `path` exactly and then falls back to the query-stripped path, so `/thing?x=1` reaches the handler registered for `/thing` while the handler still sees the full target in `request.path`. A handler that throws becomes a 500; an unknown path a 404 (or `fallback`, see [Routes that cannot be enumerated](#routes-that-cannot-be-enumerated)); a request we refuse to parse a 400. `Date` is stamped automatically unless the handler set one, HEAD returns the headers a GET would have produced with no body, and a handler can end the connection by answering with a `connection: close` header.
|
||||||
|
|
||||||
Implemented: keep-alive, pipelining, `content-length` and `chunked` request bodies with trailers, `Expect: 100-continue`, absolute-form request targets, obs-fold, and HTTP/1.0 peers (which only get connection reuse when they ask for it). Not implemented: TLS, CONNECT tunnels, `Upgrade`, and chunked *responses* — handlers return a complete body, so responses are always `content-length` framed.
|
Implemented: TLS (see [HTTPS](#https)), keep-alive, pipelining, `content-length` and `chunked` request bodies with trailers, `Expect: 100-continue`, absolute-form request targets, obs-fold, and HTTP/1.0 peers (which only get connection reuse when they ask for it). Not implemented: CONNECT tunnels, `Upgrade`, and chunked *responses* — handlers return a complete body, so responses are always `content-length` framed.
|
||||||
|
|
||||||
Ambiguous framing is rejected rather than guessed at, because guessing is how request smuggling happens (RFC 9112 §11.2): `Content-Length` together with `Transfer-Encoding`, disagreeing duplicate `Content-Length` values, and whitespace between a field name and its colon are all 400s. CR/LF in a header value we are asked to *send* throws instead of splitting the message.
|
Ambiguous framing is rejected rather than guessed at, because guessing is how request smuggling happens (RFC 9112 §11.2): `Content-Length` together with `Transfer-Encoding`, disagreeing duplicate `Content-Length` values, and whitespace between a field name and its colon are all 400s. CR/LF in a header value we are asked to *send* throws instead of splitting the message.
|
||||||
|
|
||||||
#### No TLS
|
#### HTTPS
|
||||||
|
|
||||||
`ClientHTTP1`/`ListenerHTTP1` speak `http://` only. There is no `https://` support and no plan to link a TLS stack into this path: for encrypted traffic use `ClientHTTP`/`ListenerHTTP` (HTTP/3 over QUIC, which is always encrypted), or terminate TLS in a proxy in front of the HTTP/1.1 endpoint. Do not expose an HTTP/1.1 listener directly to the internet.
|
`https://` is a constructor argument, not a different class. Pass credentials and every byte goes through libssl (OpenSSL 3); leave them out and the transport is plaintext. Nothing above the transport changes — same routes, same keep-alive and replay rules, same framing.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Server. selfSigned mints an ephemeral development certificate; in
|
||||||
|
// production set certPath/keyPath (or certPem/keyPem) instead.
|
||||||
|
Crafter::ListenerAsyncHTTP1 listener(8443, std::move(routes),
|
||||||
|
Crafter::TLSServerCredentials{ .selfSigned = true });
|
||||||
|
|
||||||
|
// Client. The default verifies the chain against the system trust store
|
||||||
|
// *and* the hostname — a chain check alone accepts any valid certificate
|
||||||
|
// for any name, which is not a check.
|
||||||
|
Crafter::TLSClientCredentials credentials;
|
||||||
|
Crafter::ClientHTTP1 client("example.com", 443, credentials);
|
||||||
|
```
|
||||||
|
|
||||||
|
To verify a self-signed or privately issued server without giving up verification, hand the client the certificate as a trust anchor (`caPath` for a file or hashed directory, `caPem` for the bytes) rather than reaching for `insecureNoServerValidation` — that switch accepts an attacker's certificate too, and exists for development only.
|
||||||
|
|
||||||
|
ALPN is negotiated: the listener advertises `http/1.1` and a client offering only something else (say `h2`) is refused with `no_application_protocol` rather than being served an HTTP/1.1 response it cannot parse. `alpnProtocols` on either side changes what is offered or accepted. Requests reach handlers with `scheme` set to `https`, so a handler shared with `ListenerHTTP` sees the same thing over both protocols.
|
||||||
|
|
||||||
|
Mutual TLS: set `requireClientCertificate` with a `clientCaPath`, and a peer presenting no certificate — or one that does not chain to that CA — is rejected during the handshake. `HandshakeFailureCount()` counts connections dropped that way; on a public port those are ordinary traffic (scanners, misconfigured callers) rather than something to alert on. Client certificates are supplied by `certPath`/`keyPath` on `TLSClientCredentials`.
|
||||||
|
|
||||||
|
The handshake runs on the connection's own thread, so a peer that stalls halfway through it costs one thread rather than the accept loop. `handshakeTimeout` (default 15 s) bounds it on both sides.
|
||||||
|
|
||||||
|
For an encrypted transport with better properties than TLS-over-TCP, `ClientHTTP`/`ListenerHTTP` speak HTTP/3 over QUIC, which is always encrypted.
|
||||||
|
|
||||||
|
#### TLS on its own
|
||||||
|
|
||||||
|
`Crafter::TLSContext` and `Crafter::TLSStream` are transport-agnostic: anything holding a connected descriptor can put a record layer over it, HTTP or not. `TLSStream` implements `Crafter::ByteStream` — the same interface `PlainStream` implements and the HTTP/1.1 endpoints are written against — so code that reads and writes through a `ByteStream&` works over either.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Crafter::ClientTCP socket("example.com", 443);
|
||||||
|
auto context = Crafter::TLSContext::Client({});
|
||||||
|
auto stream = Crafter::TLSStream::Connect(socket.socketid, context,
|
||||||
|
"example.com", std::chrono::seconds(10));
|
||||||
|
stream->Write(request.data(), request.size(), std::chrono::seconds(10));
|
||||||
|
```
|
||||||
|
|
||||||
|
The descriptor stays owned by its `ClientTCP`; the stream only adds the record layer. Both factories complete the handshake before returning, so a stream you hold is one you can write to, and both throw `Crafter::TLSException` — with the specific certificate error, not just "handshake failed" — when they cannot. Reads and writes are driven by `poll()` against a deadline, which is what makes the timeouts real; the descriptor is put into non-blocking mode to allow it.
|
||||||
|
|
||||||
|
`GetSelfSignedCertificatePem()` returns the process-wide development certificate (ECDSA P-256, `CN=localhost`, SANs for `localhost`/`127.0.0.1`/`::1`, 10 days) so it can be written out for a peer process or handed to a client as a trust anchor. It is generated in-process and regenerated on every start — no peer has any reason to trust it.
|
||||||
|
|
||||||
#### HTTP/1.1 wire format on its own
|
#### HTTP/1.1 wire format on its own
|
||||||
|
|
||||||
|
|
@ -242,8 +283,11 @@ The library includes tests covering:
|
||||||
- HTTP/1.1 interop (`ShouldInteropCurlHTTP1`) — `curl` against `ListenerHTTP1` (keep-alive reuse, chunked upload, `Expect: 100-continue`, HEAD) and `ClientHTTP1` against python3's `http.server`, which answers HTTP/1.0 with `Connection: close`
|
- HTTP/1.1 interop (`ShouldInteropCurlHTTP1`) — `curl` against `ListenerHTTP1` (keep-alive reuse, chunked upload, `Expect: 100-continue`, HEAD) and `ClientHTTP1` against python3's `http.server`, which answers HTTP/1.0 with `Connection: close`
|
||||||
- HTTP/1.1 under abuse (`ShouldSurviveAbuseHTTP1`) — 24 concurrent keep-alive clients, peers that vanish mid-request or send garbage, and a stalled peer that must be timed out
|
- HTTP/1.1 under abuse (`ShouldSurviveAbuseHTTP1`) — 24 concurrent keep-alive clients, peers that vanish mid-request or send garbage, and a stalled peer that must be timed out
|
||||||
- Fallback routing (`ShouldFallbackUnknownRoutes`) — one route map plus a `fallback` replayed over both `ListenerHTTP1` and `ListenerHTTP`, asserting identical answers: exact routes win, query strings still route to the bare path, the fallback sees the full target, a throwing fallback is a 500, and an unset fallback still means a synthetic 404
|
- Fallback routing (`ShouldFallbackUnknownRoutes`) — one route map plus a `fallback` replayed over both `ListenerHTTP1` and `ListenerHTTP`, asserting identical answers: exact routes win, query strings still route to the bare path, the fallback sees the full target, a throwing fallback is a 500, and an unset fallback still means a synthetic 404
|
||||||
|
- HTTPS round-trip (`ShouldSendRecieveHTTPS1`) — the plaintext round-trip replayed over TLS, so a transport regression surfaces as an HTTP failure, plus what only exists under TLS: ALPN, `scheme=https` reaching handlers, a body spanning many records, verification failing for an untrusted certificate *and* for a trusted certificate presented under the wrong name, and a plaintext peer on the TLS port being counted and shrugged off
|
||||||
|
- HTTPS interop (`ShouldInteropCurlHTTPS1`) — `curl` verifying our certificate with `--cacert` (not `--insecure`): keep-alive reuse, POST, HEAD, negotiated version, and an h2-only client being refused rather than mis-served; then `ClientHTTP1` against python3's `http.server` behind `ssl.wrap_socket`, whose HTTP/1.0 `Connection: close` frames the body by `close_notify`
|
||||||
|
- Mutual TLS and the raw stream (`ShouldRequireClientCertificateHTTPS1`) — a client certificate the listener's CA vouches for is served and one absent is refused; `TLSStream` is also driven directly with hand-written HTTP/1.1 to keep `:TLS` usable without `:ClientHTTP1` on top
|
||||||
|
|
||||||
The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. `ShouldInteropCurlHTTP1` skips whichever half is unavailable when `curl` or `python3` is not installed, so it passes on a bare machine — install both to actually exercise it.
|
The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. `ShouldInteropCurlHTTP1` and `ShouldInteropCurlHTTPS1` skip whichever half is unavailable when `curl` or `python3` is not installed, and the mutual-TLS half of `ShouldRequireClientCertificateHTTPS1` skips without the `openssl` CLI (a *client* certificate needs the `clientAuth` extended key usage, which the built-in development certificate does not carry) — so all three pass on a bare machine. Install `curl`, `python3` and `openssl` to actually exercise them.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -251,7 +295,8 @@ The external-interop test requires outbound UDP/443; if your network blocks it t
|
||||||
- **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.
|
- **msquic** (native target only) — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` recursively into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=quictls`, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC and HTTP/3 modules. Skipped entirely on browser builds.
|
||||||
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
|
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
|
||||||
- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`. The same path also produces the cert hash that browser peers need for `serverCertificateHashes`.
|
- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`. The same path also produces the cert hash that browser peers need for `serverCertificateHashes`.
|
||||||
- The HTTP/1.1 stack has no dependencies beyond POSIX sockets — no TLS library, no msquic.
|
- **libssl / libcrypto** (OpenSSL 3, native target only) — a *system* package, unlike msquic, linked via `-lssl -lcrypto`. Backs `Crafter.Network:TLS`, i.e. `https://` on the HTTP/1.1 client and listener. It is not built here on purpose: OpenSSL 3 is present on every platform this targets, and vendoring it would mean shipping a second TLS stack next to the one msquic already links (quictls, whose symbols stay inside `libmsquic.so`). Install your distro's OpenSSL development package (`openssl` on Arch, `libssl-dev` on Debian/Ubuntu). Certificate generation for `TLSServerCredentials{selfSigned = true}` happens in-process through this library — no `openssl` CLI needed.
|
||||||
|
- Plaintext HTTP/1.1 needs nothing beyond POSIX sockets: `ClientHTTP1`/`ListenerHTTP1` built without credentials pull in no msquic and touch no TLS code path.
|
||||||
- **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`.
|
- **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
|
## Usage Example
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue