browser wasm
This commit is contained in:
parent
28fab2509b
commit
e8630528af
24 changed files with 2490 additions and 100 deletions
227
examples/SimpleClient/main.cpp
Normal file
227
examples/SimpleClient/main.cpp
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue