browser wasm

This commit is contained in:
Jorijn van der Graaf 2026-05-19 02:53:50 +02:00
commit e8630528af
24 changed files with 2490 additions and 100 deletions

View file

@ -32,6 +32,12 @@ namespace Crafter {
//
// 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;
@ -44,8 +50,18 @@ namespace Crafter {
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<void(HTTPResponse)> onSuccess,
std::function<void(std::string)> onError);
private:
struct Impl;

View file

@ -19,7 +19,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#ifndef CRAFTER_NETWORK_BROWSER
#include <msquic.h>
#endif
export module Crafter.Network:ClientQUIC;
import std;
@ -45,9 +47,16 @@ namespace Crafter {
};
// Client-side credential validation. By default we require a real cert.
// insecureNoServerValidation disables peer cert checks — only for dev.
// 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.
export struct QUICClientCredentials {
bool insecureNoServerValidation = false;
std::array<std::uint8_t, 32> serverCertificateHash{};
};
export class ClientQUIC;
@ -60,8 +69,10 @@ namespace Crafter {
// 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;
@ -72,12 +83,22 @@ namespace Crafter {
bool canReceive = true;
QUICStream();
#ifndef CRAFTER_NETWORK_BROWSER
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.
@ -93,12 +114,36 @@ namespace Crafter {
// Read exactly bufferSize bytes; throws if the peer closes early.
std::vector<char> RecieveUntilFullSync(std::uint32_t bufferSize);
#endif
// Async variants: dispatched on Crafter.Thread's ThreadPool.
// 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<void()> onSent);
// Async receive variants. Dispatched on Crafter.Thread's ThreadPool
// (native) or driven by a per-stream JS reader loop (browser).
void RecieveAsync(std::function<void(std::vector<char>)> recieveCallback);
void RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback);
void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> 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<char> 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();
@ -109,8 +154,9 @@ namespace Crafter {
};
// A QUIC connection. On the client side, constructing one initiates the
// handshake and blocks until it succeeds (or throws on failure). On the
// server side, ListenerQUIC instantiates these for accepted peers.
// handshake and (on native) 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
@ -120,10 +166,28 @@ 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
@ -133,10 +197,12 @@ 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;
@ -162,15 +228,19 @@ namespace Crafter {
// msquic worker; copy/queue and return promptly.
void OnDatagram(std::function<void(std::vector<char>)> callback);
#ifndef CRAFTER_NETWORK_BROWSER
// Block the caller until the next datagram arrives; returns it.
// Throws QUICClosedException if the connection closes first.
std::vector<char> 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;

View file

@ -18,6 +18,7 @@ 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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
@ -31,9 +32,11 @@ module;
#include <netdb.h>
#include <strings.h>
#include <cerrno>
#endif
export module Crafter.Network:ClientTCP;
import std;
#ifndef CRAFTER_NETWORK_BROWSER
namespace Crafter {
export class SocketClosedException : public std::exception {
public:
@ -68,4 +71,5 @@ namespace Crafter {
hostent* host;
sockaddr_in serv_addr;
};
}
}
#endif

View file

@ -43,11 +43,33 @@ namespace Crafter::HTTP3 {
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 {
@ -575,4 +597,55 @@ namespace Crafter::HTTP3 {
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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> BuildWtBidiPrefix(std::uint64_t sessionId) {
std::vector<std::uint8_t> 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<std::uint8_t> BuildWtUnidiPrefix(std::uint64_t sessionId) {
std::vector<std::uint8_t> out;
EncodeVarint(kStreamWt, out);
EncodeVarint(sessionId, out);
return out;
}
}

View file

@ -23,7 +23,9 @@ import std;
import :HTTP;
import :ListenerQUIC;
import :ClientQUIC;
import :WebTransport;
#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
@ -33,6 +35,13 @@ namespace Crafter {
// 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,
@ -40,12 +49,20 @@ namespace Crafter {
// and owned by this Impl so that move construction/destruction is
// straightforward.
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes;
std::string alpn;
ListenerHTTP(std::uint16_t port,
QUICServerCredentials creds,
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
// 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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes);
~ListenerHTTP();
ListenerHTTP(const ListenerHTTP&) = delete;
ListenerHTTP(ListenerHTTP&&) noexcept;
@ -71,7 +88,15 @@ namespace Crafter {
ListenerAsyncHTTP(std::uint16_t port,
QUICServerCredentials creds,
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
// WT-aware overload.
ListenerAsyncHTTP(std::uint16_t port,
QUICServerCredentials creds,
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes);
~ListenerAsyncHTTP();
void Stop();
};
}
#endif

View file

@ -19,11 +19,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#ifndef CRAFTER_NETWORK_BROWSER
#include <msquic.h>
#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
@ -72,4 +75,17 @@ namespace Crafter {
std::unique_ptr<Impl> 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<std::uint8_t, 32> 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

View file

@ -22,6 +22,7 @@ export module Crafter.Network:ListenerTCP;
import std;
import :ClientTCP;
#ifndef CRAFTER_NETWORK_BROWSER
namespace Crafter {
export class ListenerTCP {
public:
@ -40,4 +41,5 @@ namespace Crafter {
std::uint32_t totalClientCounter = 0;
int s;
};
}
}
#endif

View file

@ -0,0 +1,105 @@
/*
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<void(QUICStream)> 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<void(std::vector<char>)> 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> 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

View file

@ -26,4 +26,12 @@ export import :ClientHTTP;
export import :ListenerHTTP;
export import :HTTP;
export import :ClientQUIC;
export import :ListenerQUIC;
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