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