Crafter.Network/examples/SimpleClient/main.cpp

227 lines
9.5 KiB
C++
Raw Normal View History

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";
s.SendAsync(payload.data(), static_cast<std::uint32_t>(payload.size()), /*finish=*/true, []{
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;
}