Crafter.Network/implementations/Crafter.Network-ClientQUIC-Browser.cpp
2026-05-19 02:53:50 +02:00

443 lines
18 KiB
C++

/*
Crafter®.Network
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
// CRAFTER_NETWORK_BROWSER implementation of ClientQUIC / QUICStream backed
// by the browser's WebTransport API. WebTransport is HTTP/3-based and
// expects a URL — we use https://${host}:${port}/${alpn}. Native msquic
// is not linked into the browser build.
//
// Async-only: synchronous send/receive methods on QUICStream are not
// compiled (gated out in the interface). Everything goes through the
// existing *Async / OnStream / OnDatagram callbacks. The JS bridge
// (additional/network-env.js) runs the WebTransport reader loops and
// dispatches each chunk back through the wasm exports declared below.
module;
module Crafter.Network:ClientQUIC_impl;
import :ClientQUIC;
import std;
using namespace Crafter;
namespace Crafter::NetworkBrowserBindings {
__attribute__((import_module("env"), import_name("crafterNetworkWtConnect")))
std::int32_t crafterNetworkWtConnect(
const char* host, std::int32_t hostLen,
std::int32_t port,
const char* alpn, std::int32_t alpnLen,
const std::uint8_t* certHash, std::int32_t certHashLen);
__attribute__((import_module("env"), import_name("crafterNetworkWtClose")))
void crafterNetworkWtClose(std::int32_t handle);
__attribute__((import_module("env"), import_name("crafterNetworkWtOpenStream")))
std::int32_t crafterNetworkWtOpenStream(std::int32_t handle, std::int32_t unidirectional);
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamWrite")))
void crafterNetworkWtStreamWrite(std::int32_t streamId,
const char* buf, std::int32_t bufLen,
std::int32_t finish,
std::int32_t callbackId);
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamStop")))
void crafterNetworkWtStreamStop(std::int32_t streamId);
__attribute__((import_module("env"), import_name("crafterNetworkWtSendDatagram")))
void crafterNetworkWtSendDatagram(std::int32_t handle,
const char* buf, std::int32_t bufLen);
}
namespace Crafter::NetworkBrowser {
// ─── Receive state machine per stream ─────────────────────────────────
//
// The JS reader loop pushes every chunk it sees through
// CrafterNetworkOnWtStreamChunk. We buffer them until the user calls
// one of the Receive*Async variants — at which point we either
// dispatch immediately (chunks already queued) or wait for the next
// chunk to arrive. A FIN signal marks the end of the peer's send-
// side; further chunks after FIN do not arrive.
//
// StreamState / ConnectionState live in a named namespace (not
// anonymous) because they are referenced from the QUICStream::Impl
// and ClientQUIC::Impl definitions below — private nested types of
// exported classes can't have TU-local member types without a
// diagnostic.
enum class RecvMode { None, Once, UntilClose, UntilFull };
struct StreamState {
// 0 means destroyed / closed.
std::int32_t handle = 0;
// Buffered chunks not yet delivered to a pending callback.
std::vector<char> buffer;
bool finReceived = false;
bool closed = false;
// Pending one-shot receive.
RecvMode mode = RecvMode::None;
std::uint32_t target = 0;
std::function<void(std::vector<char>)> cb;
ClientQUIC* connection = nullptr;
};
struct ConnectionState {
std::int32_t handle = 0;
bool ready = false;
bool closed = false;
std::function<void(QUICStream)> onStream;
std::function<void(std::vector<char>)> onDatagram;
};
// Handle → state. Allocated on the heap so the pointer is stable
// across QUICStream / ClientQUIC moves (each holds a unique_ptr<Impl>
// that wraps a pointer into these maps via its handle).
inline std::unordered_map<std::int32_t, StreamState*>& Streams() {
static std::unordered_map<std::int32_t, StreamState*> m;
return m;
}
inline std::unordered_map<std::int32_t, ConnectionState*>& Connections() {
static std::unordered_map<std::int32_t, ConnectionState*> m;
return m;
}
inline std::unordered_map<std::int32_t, std::function<void()>>& WriteCallbacks() {
static std::unordered_map<std::int32_t, std::function<void()>> m;
return m;
}
inline std::int32_t NextWriteCallbackId() {
static std::int32_t counter = 0;
return ++counter;
}
inline void TryDispatchRecv(StreamState& s) {
if (s.mode == RecvMode::None) return;
if (s.mode == RecvMode::Once) {
if (!s.buffer.empty()) {
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
} else if (s.finReceived || s.closed) {
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb({});
}
return;
}
if (s.mode == RecvMode::UntilClose) {
if (s.finReceived || s.closed) {
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
}
return;
}
if (s.mode == RecvMode::UntilFull) {
if (s.buffer.size() >= s.target) {
std::vector<char> chunk(s.buffer.begin(), s.buffer.begin() + s.target);
s.buffer.erase(s.buffer.begin(), s.buffer.begin() + s.target);
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
} else if (s.finReceived || s.closed) {
// Peer closed before we got the requested byte count —
// deliver whatever's left. Mirrors the native variant's
// "throws if peer closes early" only loosely (we have no
// exception channel from JS).
auto chunk = std::move(s.buffer);
s.buffer.clear();
auto cb = std::move(s.cb);
s.mode = RecvMode::None;
if (cb) cb(std::move(chunk));
}
}
}
}
// All implementation-private state types live in Crafter::NetworkBrowser
// (above). Pull them in unqualified for readability.
using Crafter::NetworkBrowser::StreamState;
using Crafter::NetworkBrowser::ConnectionState;
using Crafter::NetworkBrowser::RecvMode;
using Crafter::NetworkBrowser::Streams;
using Crafter::NetworkBrowser::Connections;
using Crafter::NetworkBrowser::WriteCallbacks;
using Crafter::NetworkBrowser::NextWriteCallbackId;
using Crafter::NetworkBrowser::TryDispatchRecv;
// ─── QUICStream::Impl ────────────────────────────────────────────────────
struct QUICStream::Impl {
StreamState state;
};
QUICStream::QUICStream() : impl(std::make_unique<Impl>()) {}
QUICStream::QUICStream(std::int32_t streamHandle, ClientQUIC* conn,
bool canSendArg, bool canReceiveArg)
: canSend(canSendArg), canReceive(canReceiveArg),
impl(std::make_unique<Impl>())
{
connection = conn;
impl->state.handle = streamHandle;
impl->state.connection = conn;
Streams()[streamHandle] = &impl->state;
}
QUICStream::QUICStream(QUICStream&&) noexcept = default;
QUICStream& QUICStream::operator=(QUICStream&&) noexcept = default;
QUICStream::~QUICStream() {
if (impl && impl->state.handle != 0) {
Streams().erase(impl->state.handle);
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
}
}
void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish,
std::function<void()> onSent) {
if (!impl || impl->state.handle == 0 || impl->state.closed) {
if (onSent) onSent();
return;
}
std::int32_t cbId = 0;
if (onSent) {
cbId = NextWriteCallbackId();
WriteCallbacks()[cbId] = std::move(onSent);
}
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamWrite(
impl->state.handle,
static_cast<const char*>(buffer),
static_cast<std::int32_t>(size),
finish ? 1 : 0,
cbId);
}
void QUICStream::RecieveAsync(std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::Once;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::UntilClose;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize,
std::function<void(std::vector<char>)> cb) {
if (!impl) { if (cb) cb({}); return; }
impl->state.mode = RecvMode::UntilFull;
impl->state.target = bufferSize;
impl->state.cb = std::move(cb);
TryDispatchRecv(impl->state);
}
void QUICStream::Stop() {
if (impl && impl->state.handle != 0) {
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
Streams().erase(impl->state.handle);
impl->state.handle = 0;
impl->state.closed = true;
}
}
// ─── ClientQUIC::Impl ────────────────────────────────────────────────────
struct ClientQUIC::Impl {
ConnectionState state;
};
namespace {
QUICStream MakeStreamFromHandle(std::int32_t streamId, ClientQUIC* conn,
bool canSend, bool canReceive) {
return QUICStream{streamId, conn, canSend, canReceive};
}
}
ClientQUIC::ClientQUIC(const char* host, std::uint16_t port, std::string alpnArg,
QUICClientCredentials creds)
: alpn(std::move(alpnArg)), impl(std::make_unique<Impl>()) {
// Zeroed hash means "no pinning" — JS passes an empty array, browser
// falls back to its trust store. A non-zero hash is forwarded as a
// serverCertificateHashes entry (Chrome only, < 14 day cert validity).
const std::uint8_t* hashPtr = nullptr;
std::int32_t hashLen = 0;
for (std::uint8_t b : creds.serverCertificateHash) {
if (b != 0) { hashPtr = creds.serverCertificateHash.data(); hashLen = 32; break; }
}
std::string hostStr = host;
impl->state.handle = Crafter::NetworkBrowserBindings::crafterNetworkWtConnect(
hostStr.data(), static_cast<std::int32_t>(hostStr.size()),
static_cast<std::int32_t>(port),
alpn.data(), static_cast<std::int32_t>(alpn.size()),
hashPtr, hashLen);
// wasm builds run with -fno-exceptions; a failed JS-side allocation
// leaves the connection in a closed state and the first operation will
// produce a sentinel result (OpenStream returns a default-constructed
// QUICStream, SendDatagram silently drops, OnStream/OnDatagram never
// fire). The constructor cannot signal the failure synchronously.
if (impl->state.handle == 0) {
impl->state.closed = true;
} else {
Connections()[impl->state.handle] = &impl->state;
}
}
ClientQUIC::ClientQUIC(std::string host, std::uint16_t port, std::string alpnArg,
QUICClientCredentials creds)
: ClientQUIC(host.c_str(), port, std::move(alpnArg), std::move(creds)) {}
ClientQUIC::ClientQUIC(ClientQUIC&&) noexcept = default;
ClientQUIC::~ClientQUIC() {
if (impl && impl->state.handle != 0) {
Connections().erase(impl->state.handle);
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
}
}
QUICStream ClientQUIC::OpenStream(bool unidirectional) {
if (!impl || impl->state.handle == 0 || impl->state.closed) {
return QUICStream{}; // default-constructed: closed sentinel
}
std::int32_t streamId = Crafter::NetworkBrowserBindings::crafterNetworkWtOpenStream(
impl->state.handle, unidirectional ? 1 : 0);
if (streamId == 0) {
return QUICStream{};
}
return MakeStreamFromHandle(streamId, this,
/*canSend=*/true,
/*canReceive=*/!unidirectional);
}
void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) {
if (!impl || impl->state.handle == 0 || impl->state.closed) return;
Crafter::NetworkBrowserBindings::crafterNetworkWtSendDatagram(
impl->state.handle,
static_cast<const char*>(buffer),
static_cast<std::int32_t>(size));
}
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
if (impl) impl->state.onStream = std::move(cb);
}
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
if (impl) impl->state.onDatagram = std::move(cb);
}
void ClientQUIC::Stop() {
if (impl && impl->state.handle != 0) {
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
Connections().erase(impl->state.handle);
impl->state.handle = 0;
impl->state.closed = true;
}
}
// ─── WASM exports the JS bridge dispatches back through ──────────────────
extern "C" {
__attribute__((export_name("CrafterNetworkOnWtReady")))
void CrafterNetworkOnWtReady(std::int32_t connectionHandle) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end()) return;
it->second->ready = true;
}
__attribute__((export_name("CrafterNetworkOnWtClosed")))
void CrafterNetworkOnWtClosed(std::int32_t connectionHandle,
char* messagePtr, std::int32_t /*messageLen*/) {
std::free(messagePtr);
auto it = Connections().find(connectionHandle);
if (it == Connections().end()) return;
it->second->closed = true;
it->second->ready = false;
// The JS bridge dispatches CrafterNetworkOnWtStreamChunk(_, nullptr, 0,
// fin=1) for each stream that belonged to this connection so pending
// receivers terminate. We don't iterate stream state here.
}
__attribute__((export_name("CrafterNetworkOnWtIncomingStream")))
void CrafterNetworkOnWtIncomingStream(std::int32_t connectionHandle,
std::int32_t streamId,
std::int32_t bidirectional) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end() || !it->second->onStream) {
// No registered handler — close the stream JS-side to free
// resources. Mirrors what the native msquic backend does
// when a stream arrives before OnStream is registered (it
// queues, but in the browser we don't have a backing buffer
// to queue against without leaking).
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(streamId);
return;
}
QUICStream stream = MakeStreamFromHandle(streamId, /*conn=*/nullptr,
/*canSend=*/bidirectional != 0,
/*canReceive=*/true);
it->second->onStream(std::move(stream));
}
__attribute__((export_name("CrafterNetworkOnWtStreamChunk")))
void CrafterNetworkOnWtStreamChunk(std::int32_t streamId,
char* dataPtr, std::int32_t dataLen,
std::int32_t fin) {
auto it = Streams().find(streamId);
if (it == Streams().end()) { std::free(dataPtr); return; }
StreamState& s = *it->second;
if (dataPtr && dataLen > 0) {
s.buffer.insert(s.buffer.end(), dataPtr, dataPtr + dataLen);
}
std::free(dataPtr);
if (fin) s.finReceived = true;
TryDispatchRecv(s);
}
__attribute__((export_name("CrafterNetworkOnWtStreamWriteComplete")))
void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId) {
auto it = WriteCallbacks().find(callbackId);
if (it == WriteCallbacks().end()) return;
auto cb = std::move(it->second);
WriteCallbacks().erase(it);
if (cb) cb();
}
__attribute__((export_name("CrafterNetworkOnWtDatagram")))
void CrafterNetworkOnWtDatagram(std::int32_t connectionHandle,
char* dataPtr, std::int32_t dataLen) {
auto it = Connections().find(connectionHandle);
if (it == Connections().end() || !it->second->onDatagram) {
std::free(dataPtr);
return;
}
std::vector<char> data(dataPtr, dataPtr + dataLen);
std::free(dataPtr);
it->second->onDatagram(std::move(data));
}
}