C++ multipurpose networking library for linux
  • C++ 97%
  • JavaScript 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
catbot 9c22cbe09e feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack
HTTP/1.1 was plaintext-only, which left `https://` to either an HTTP/3
listener or a terminating proxy in front. Neither helps the callers this
stack exists for — curl scripts, CI tooling, old proxies — so wrap the
transport in libssl instead.

Two new partitions:

  :Stream  a ByteStream with per-call deadlines on both directions, plus
           the plaintext socket implementation. The HTTP/1.1 client and
           listener now hold a ByteStream& and never learn which
           transport they have, which is what lets one code path serve
           both schemes.
  :TLS     TLSContext/TLSStream over OpenSSL 3, with credentials for both
           roles: chain and hostname verification on by default, private
           trust anchors, client certificates, mutual TLS, ALPN, and an
           in-process self-signed certificate for development.

Both descriptors go non-blocking and every read and write is driven by
poll() against a deadline. That is required for TLS — a blocking
descriptor cannot express a handshake timeout — and it means a plaintext
write can now time out too, instead of parking forever against a peer
that stopped reading.

ClientHTTP1 and ListenerHTTP1 gain credential-taking constructors; the
existing ones still speak http://. The listener handshakes on the
connection's own thread, so a peer that stalls mid-handshake costs one
thread rather than the accept loop, and a failed handshake is counted
rather than logged — on a public port it is ordinary traffic.

MessageParser gains SetDefaultScheme so origin-form targets report the
scheme the transport actually used; handlers shared with ListenerHTTP now
see the same "https" they would over HTTP/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:13:30 +00:00
additional SPDX license 2026-07-22 17:55:45 +02:00
examples/SimpleClient SPDX license 2026-07-22 17:55:45 +02:00
implementations feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack 2026-07-28 20:13:30 +00:00
interfaces feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack 2026-07-28 20:13:30 +00:00
tests test(http): replay one fallback route table over both listeners 2026-07-28 19:42:16 +00:00
.gitignore initial commit 2025-11-02 15:00:53 +01:00
LICENSE SPDX license 2026-07-22 17:55:45 +02:00
LICENSE.GPL SPDX license 2026-07-22 17:55:45 +02:00
project.cpp feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack 2026-07-28 20:13:30 +00:00
README.md docs: document fallback routing for both listeners 2026-07-28 19:42:16 +00:00

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, 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.
  • 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 poolbased 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.

Architecture

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, constructors, and PathWithoutQueryHTTP, shared by every HTTP version
  • Crafter.Network:ClientHTTP1: HTTP/1.1 client over TCP (native only)
  • Crafter.Network:ListenerHTTP1: HTTP/1.1 server over TCP (native only)
  • Crafter.Network:HTTP1: HTTP/1.1 wire format — serialisation plus an incremental parser (RFC 9112). Transport-free; usable on its own to speak HTTP/1.1 over some other byte stream. Native only, for the same reason as :HTTP3
  • 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).

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 and then on the query-stripped path; anything still unmatched goes to fallback if one is set and returns a synthetic 404 otherwise.

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 (or fallback, see 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.

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
}

Routes that cannot be enumerated

The route map only answers paths known when the listener is built. /shop/<slug>, /order/<token>, /posts/<id> cannot be pre-registered — the token space is unbounded and the product set changes while the server runs. Both listeners therefore take an optional fallback, called for any request the route map missed, with the full target still in request.path:

auto router = [](const Crafter::HTTPRequest& request) {
    auto path = Crafter::PathWithoutQueryHTTP(request.path);   // everything up to '?'
    if (path.starts_with("/shop/")) return RenderProduct(path.substr(6));
    return Crafter::CreateResponseHTTP("404", "Not Found");
};

Crafter::ListenerAsyncHTTP1 listener(8080, std::move(routes), router);      // HTTP/1.1
Crafter::ListenerAsyncHTTP  quic(4443, creds, std::move(routes), router);   // same handler over HTTP/3

Dispatch precedence is the same on both: exact path, then the query-stripped path, then fallback, then a synthetic 404. So a fallback only ever sees what the route map did not claim, and leaving it unset keeps the previous behaviour exactly.

This is deliberately a hook rather than a pattern-matching syntax. Consumers that already have a router — one shared between a wasm frontend and the server, say, so a URL cannot mean different things to a crawler and to the app — keep using it, and there is no second route table to disagree with the first. A throwing fallback becomes a 500, like any other handler. On ListenerHTTP it applies to routes only; an unmatched WebTransport CONNECT is still a 404, since a WT handler has a different signature.

fallback is a plain public member on ListenerHTTP/ListenerHTTP1 and may be assigned before Listen(). The ListenerAsync* wrappers start accepting inside their constructor, so there they have to be passed as the trailing constructor argument shown above — assigning afterwards races the accept loop.

WebTransport Components

ListenerHTTP has a WT-aware constructor overload that takes a second route map keyed by :path. When the map is non-empty the listener advertises both draft-02 (SETTINGS_ENABLE_WEBTRANSPORT = 0x2b603742) and draft-07+ (SETTINGS_WT_MAX_SESSIONS = 0xc671706a) identifiers in its SETTINGS frame so current browsers connect. An extended-CONNECT request (:method=CONNECT, :protocol=webtransport) whose :path matches a registered route is accepted with a 200 (no FIN), upgraded into a WebTransportSession, and dispatched on the ThreadPool. Plain HTTP/3 routes and WT routes coexist on the same listener and port.

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_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. So are ClientHTTP1 / ListenerHTTP1 / the HTTP1 codec: a page cannot open a raw TCP socket anyway, and fetch() behind ClientHTTP already negotiates whatever HTTP version the server offers.
  • 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/. 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; defines CRAFTER_NETWORK_BROWSER, drops msquic.
  • crafter-build test [globs] — build and run tests under tests/.

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
  • 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) — curl against ListenerHTTP1 (keep-alive reuse, chunked upload, Expect: 100-continue, HEAD) and ClientHTTP1 against python3's http.server, which answers HTTP/1.0 with Connection: close
  • HTTP/1.1 under abuse (ShouldSurviveAbuseHTTP1) — 24 concurrent keep-alive clients, peers that vanish mid-request or send garbage, and a stalled peer that must be timed out
  • Fallback routing (ShouldFallbackUnknownRoutes) — one route map plus a fallback replayed over both ListenerHTTP1 and ListenerHTTP, asserting identical answers: exact routes win, query strings still route to the bare path, the fallback sees the full target, a throwing fallback is a 500, and an unset fallback still means a synthetic 404

The external-interop test requires outbound UDP/443; if your network blocks it the test will fail. ShouldInteropCurlHTTP1 skips whichever half is unavailable when curl or python3 is not installed, so it passes on a bare machine — install both to actually exercise it.

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.
    • 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 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-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

#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 (C) 2026 Catcrafts® Catcrafts.net