// Crafter.Network SimpleClient example. // // Browser build (wasm32-wasip1, default): // crafter-build // Serve bin// 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 #include #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(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 ParseHexHash(std::string_view hex) { std::array 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{}; out[i] = static_cast((hi << 4) | lo); } return out; } void StartWebTransportEcho(std::array 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 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"; s.SendAsync(payload.data(), static_cast(payload.size()), /*finish=*/true, []{ std::println(std::cout, "[Crafter.Network] WT stream write OK"); s.RecieveUntilCloseAsync([](std::vector 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 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> httpRoutes = { {"/health", [](const HTTPRequest&) { HTTPResponse r; r.status = "200"; r.body = "ok"; return r; }}, }; std::unordered_map> wtRoutes = { {"/echo", [](WebTransportSession& session) { session.OnStream([](QUICStream stream) { try { auto bytes = stream.RecieveUntilCloseSync(); stream.SendSync(bytes.data(), static_cast(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; }