227 lines
9.5 KiB
C++
227 lines
9.5 KiB
C++
|
|
// 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;
|
||
|
|
}
|