2026-07-22 17:55:45 +02:00
|
|
|
//SPDX-License-Identifier: MIT
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
2026-05-19 02:53:50 +02:00
|
|
|
// Crafter.Network SimpleClient example.
|
|
|
|
|
//
|
|
|
|
|
// Browser build (wasm32-wasip1, default):
|
|
|
|
|
// crafter-build
|
|
|
|
|
// Serve bin/<wasm-out-dir>/ on a static HTTPS server, open index.html
|
|
|
|
|
// in Chrome and watch DevTools → Console.
|
|
|
|
|
//
|
|
|
|
|
// Native server build (e.g. x86_64-pc-linux-gnu):
|
|
|
|
|
// crafter-build --target=x86_64-pc-linux-gnu
|
|
|
|
|
// Runs a WebTransport echo server on port 4443 that the browser demo
|
|
|
|
|
// connects to at wt://localhost:4443/echo.
|
|
|
|
|
|
|
|
|
|
import Crafter.Network;
|
|
|
|
|
import std;
|
|
|
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
|
|
|
import Crafter.Thread;
|
|
|
|
|
#include <csignal>
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
#ifdef CRAFTER_NETWORK_BROWSER
|
|
|
|
|
void RunFetchDemo() {
|
|
|
|
|
// httpbin.org sets Access-Control-Allow-Origin: * and returns a
|
|
|
|
|
// small JSON echo, so it works as a smoke test from any origin.
|
|
|
|
|
// Swap host/port/path for your own service when you have one.
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] HTTP GET httpbin.org/get ...");
|
|
|
|
|
|
|
|
|
|
// Heap-allocated and intentionally leaked — fetch() resolves
|
|
|
|
|
// after main() returns and the JS event loop keeps the wasm
|
|
|
|
|
// instance alive. A real app would tie the lifetime to a session
|
|
|
|
|
// object that lives for as long as the page does.
|
|
|
|
|
auto* client = new ClientHTTP("httpbin.org", 443);
|
|
|
|
|
|
|
|
|
|
HTTPRequest req;
|
|
|
|
|
req.method = "GET";
|
|
|
|
|
req.path = "/get";
|
|
|
|
|
|
|
|
|
|
client->SendAsync(req,
|
|
|
|
|
[](HTTPResponse response) {
|
|
|
|
|
std::println(std::cout,
|
|
|
|
|
"[Crafter.Network] fetch OK — status {}, body {} bytes",
|
|
|
|
|
response.status, response.body.size());
|
|
|
|
|
if (!response.body.empty()) {
|
|
|
|
|
auto preview = response.body.substr(0, std::min<std::size_t>(80, response.body.size()));
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] body[0..80]: {}", preview);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[](std::string error) {
|
|
|
|
|
std::println(std::cerr, "[Crafter.Network] fetch ERROR: {}", error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse 64-char lowercase hex into a 32-byte digest. Returns all-zero
|
|
|
|
|
// bytes (treated as "no hash") if the input is malformed.
|
|
|
|
|
std::array<std::uint8_t, 32> ParseHexHash(std::string_view hex) {
|
|
|
|
|
std::array<std::uint8_t, 32> out{};
|
|
|
|
|
if (hex.size() < 64) return out;
|
|
|
|
|
auto nibble = [](char c) -> int {
|
|
|
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
|
|
|
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
|
|
|
|
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
|
|
|
|
return -1;
|
|
|
|
|
};
|
|
|
|
|
for (std::size_t i = 0; i < 32; ++i) {
|
|
|
|
|
int hi = nibble(hex[2 * i]);
|
|
|
|
|
int lo = nibble(hex[2 * i + 1]);
|
|
|
|
|
if (hi < 0 || lo < 0) return std::array<std::uint8_t, 32>{};
|
|
|
|
|
out[i] = static_cast<std::uint8_t>((hi << 4) | lo);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StartWebTransportEcho(std::array<std::uint8_t, 32> certHash) {
|
|
|
|
|
const char* wtHost = "localhost";
|
|
|
|
|
std::uint16_t wtPort = 4443;
|
|
|
|
|
const char* wtPath = "echo";
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] WebTransport connect {}:{}/{} ...", wtHost, wtPort, wtPath);
|
|
|
|
|
|
|
|
|
|
QUICClientCredentials creds;
|
|
|
|
|
creds.serverCertificateHash = certHash;
|
|
|
|
|
auto* conn = new ClientQUIC(wtHost, wtPort, wtPath, creds);
|
|
|
|
|
|
|
|
|
|
conn->OnDatagram([](std::vector<char> bytes) {
|
|
|
|
|
std::string text(bytes.begin(), bytes.end());
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] WT datagram echo: {} ({} bytes)", text, bytes.size());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Leak the stream so its handle survives past this function's
|
|
|
|
|
// return. Read fires after the echo server has sent the bytes
|
|
|
|
|
// back, which happens after the JS event loop runs. finish=true
|
|
|
|
|
// closes the send-side so the server's RecieveUntilCloseSync
|
|
|
|
|
// returns and the echo handler runs.
|
|
|
|
|
static QUICStream s = conn->OpenStream(/*unidirectional=*/false);
|
|
|
|
|
constexpr std::string_view payload = "hello from crafter.network";
|
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
s.SendAsync(payload.data(), static_cast<std::uint32_t>(payload.size()), /*finish=*/true, [](bool sent) {
|
|
|
|
|
if (!sent) {
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] WT stream write failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-19 02:53:50 +02:00
|
|
|
std::println(std::cout, "[Crafter.Network] WT stream write OK");
|
|
|
|
|
s.RecieveUntilCloseAsync([](std::vector<char> bytes) {
|
|
|
|
|
std::string text(bytes.begin(), bytes.end());
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] WT stream echo: {} ({} bytes)", text, bytes.size());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RunWebTransportDemo() {
|
|
|
|
|
// Chrome refuses self-signed WebTransport certs unless we pass their
|
|
|
|
|
// SHA-256 via `serverCertificateHashes`. Our native server writes the
|
|
|
|
|
// hex digest to ./cert-hash.txt; we fetch it from the same origin as
|
|
|
|
|
// this wasm (`ClientHTTP("", 0)` is the same-origin sentinel). Serve
|
|
|
|
|
// the wasm from the directory the server is running in so the file
|
|
|
|
|
// is reachable, then refresh.
|
|
|
|
|
static ClientHTTP origin("", 0);
|
|
|
|
|
HTTPRequest req;
|
|
|
|
|
req.method = "GET";
|
|
|
|
|
req.path = "/cert-hash.txt";
|
|
|
|
|
origin.SendAsync(req,
|
|
|
|
|
[](HTTPResponse resp) {
|
|
|
|
|
if (resp.status != "200") {
|
|
|
|
|
std::println(std::cerr,
|
|
|
|
|
"[Crafter.Network] /cert-hash.txt → HTTP {} — start the native server "
|
|
|
|
|
"from this directory so the hash file is reachable",
|
|
|
|
|
resp.status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
auto hash = ParseHexHash(resp.body);
|
|
|
|
|
bool zero = true;
|
|
|
|
|
for (auto b : hash) if (b) { zero = false; break; }
|
|
|
|
|
if (zero) {
|
|
|
|
|
std::println(std::cerr,
|
|
|
|
|
"[Crafter.Network] /cert-hash.txt is empty or malformed; expected 64 hex chars");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] using cert hash from /cert-hash.txt");
|
|
|
|
|
StartWebTransportEcho(hash);
|
|
|
|
|
},
|
|
|
|
|
[](std::string err) {
|
|
|
|
|
std::println(std::cerr,
|
|
|
|
|
"[Crafter.Network] could not fetch /cert-hash.txt: {} — is the static server "
|
|
|
|
|
"serving the directory the native server runs in?", err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#else // native server
|
|
|
|
|
|
|
|
|
|
static std::atomic<bool> gRunning{true};
|
|
|
|
|
|
|
|
|
|
void RunEchoServer() {
|
|
|
|
|
ThreadPool::Start();
|
|
|
|
|
|
|
|
|
|
QUICServerCredentials creds;
|
|
|
|
|
creds.selfSigned = true;
|
|
|
|
|
|
|
|
|
|
// Compute the SHA-256 of the self-signed cert so the browser peer
|
|
|
|
|
// can pass it via WebTransport's serverCertificateHashes option.
|
|
|
|
|
// Chrome won't accept self-signed certs without this. We also write
|
|
|
|
|
// the hash hex to ./cert-hash.txt alongside the binary so a static
|
|
|
|
|
// file server serving the wasm can hand it back to the page.
|
|
|
|
|
std::string certPath = GetSelfSignedCertificatePath();
|
|
|
|
|
auto hash = ComputeCertificateHashSHA256(certPath);
|
|
|
|
|
std::string hashHex;
|
|
|
|
|
for (auto b : hash) {
|
|
|
|
|
constexpr char hex[] = "0123456789abcdef";
|
|
|
|
|
hashHex.push_back(hex[b >> 4]);
|
|
|
|
|
hashHex.push_back(hex[b & 0xf]);
|
|
|
|
|
}
|
|
|
|
|
std::ofstream("cert-hash.txt") << hashHex;
|
|
|
|
|
|
|
|
|
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> httpRoutes = {
|
|
|
|
|
{"/health", [](const HTTPRequest&) {
|
|
|
|
|
HTTPResponse r;
|
|
|
|
|
r.status = "200";
|
|
|
|
|
r.body = "ok";
|
|
|
|
|
return r;
|
|
|
|
|
}},
|
|
|
|
|
};
|
|
|
|
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes = {
|
|
|
|
|
{"/echo", [](WebTransportSession& session) {
|
|
|
|
|
session.OnStream([](QUICStream stream) {
|
|
|
|
|
try {
|
|
|
|
|
auto bytes = stream.RecieveUntilCloseSync();
|
|
|
|
|
stream.SendSync(bytes.data(),
|
|
|
|
|
static_cast<std::uint32_t>(bytes.size()),
|
|
|
|
|
/*finish=*/true);
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
});
|
|
|
|
|
}},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ListenerAsyncHTTP server(4443, creds, std::move(httpRoutes), std::move(wtRoutes));
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] echo server listening on port 4443");
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] WebTransport /echo — bidi streams echoed back");
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] HTTP GET /health — returns 200 ok");
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] cert SHA-256: {}", hashHex);
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] (also written to ./cert-hash.txt for the browser to fetch)");
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] Press Ctrl-C to stop");
|
|
|
|
|
|
|
|
|
|
std::signal(SIGINT, [](int) { gRunning = false; });
|
|
|
|
|
std::signal(SIGTERM, [](int) { gRunning = false; });
|
|
|
|
|
while (gRunning) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
|
|
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] shutting down");
|
|
|
|
|
server.Stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
#ifdef CRAFTER_NETWORK_BROWSER
|
|
|
|
|
// Full-buffer stdout means async-callback prints never reach the
|
|
|
|
|
// console after main() returns. unitbuf flushes after every insert
|
|
|
|
|
// so logs show up live.
|
|
|
|
|
std::cout << std::unitbuf;
|
|
|
|
|
std::cerr << std::unitbuf;
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] SimpleClient starting (browser)");
|
|
|
|
|
RunFetchDemo();
|
|
|
|
|
RunWebTransportDemo();
|
|
|
|
|
std::println(std::cout, "[Crafter.Network] main() returning (async work continues in JS event loop)");
|
|
|
|
|
#else
|
|
|
|
|
RunEchoServer();
|
|
|
|
|
#endif
|
|
|
|
|
return 0;
|
|
|
|
|
}
|