/* 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 ClientHTTP. Each SendAsync // hands its request to the JS bridge (additional/network-env.js), which // runs a fetch() and dispatches the result back through the // CrafterNetworkOnFetchComplete / CrafterNetworkOnFetchError wasm exports. module; module Crafter.Network:ClientHTTP_impl; import :ClientHTTP; import :HTTP; import std; using namespace Crafter; namespace Crafter::NetworkBrowserBindings { // External linkage so the import_module/import_name attributes wire up. __attribute__((import_module("env"), import_name("crafterNetworkFetch"))) void crafterNetworkFetch( const char* method, std::int32_t methodLen, const char* url, std::int32_t urlLen, const char* headers, std::int32_t headersLen, const char* body, std::int32_t bodyLen, std::int32_t callbackId); } namespace { struct FetchCallbacks { std::function onSuccess; std::function onError; }; // JS dispatches back into wasm via a stable id we mint here. The id // counter is monotone — wraparound at 2 billion fetches is not a // realistic concern. The map is touched only from the JS event loop // (single-threaded in the browser), so no synchronisation is needed. std::unordered_map& Callbacks() { static std::unordered_map m; return m; } std::int32_t NextId() { static std::int32_t counter = 0; return ++counter; } // Serialise headers as newline-separated "name: value" pairs. The JS // side splits on '\n' and the first ": " for header construction. std::string SerialiseHeaders(const std::unordered_map& headers) { std::string out; bool first = true; for (const auto& [name, value] : headers) { if (!first) out += '\n'; first = false; out += name; out += ": "; out += value; } return out; } // Parse a "name: value\nname2: value2" blob into the HTTPResponse map. // Names are kept verbatim (fetch surfaces them lowercase already on the // browser side via response.headers.forEach). void ParseHeaders(std::string_view raw, HTTPResponse& response) { std::size_t pos = 0; while (pos < raw.size()) { std::size_t end = raw.find('\n', pos); std::string_view line = raw.substr(pos, end == std::string_view::npos ? raw.size() - pos : end - pos); std::size_t sep = line.find(": "); if (sep != std::string_view::npos) { response.headers.emplace(std::string(line.substr(0, sep)), std::string(line.substr(sep + 2))); } if (end == std::string_view::npos) break; pos = end + 1; } } } struct ClientHTTP::Impl {}; ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials) : host(host), port(port), impl(std::make_unique()) {} ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds) : ClientHTTP(host.c_str(), port, std::move(creds)) {} ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default; ClientHTTP::~ClientHTTP() = default; void ClientHTTP::SendAsync(const HTTPRequest& request, std::function onSuccess, std::function onError) { std::int32_t id = NextId(); Callbacks().emplace(id, FetchCallbacks{std::move(onSuccess), std::move(onError)}); std::string method = request.method.empty() ? std::string("GET") : request.method; std::string path = request.path.empty() ? std::string("/") : request.path; // Sentinel: a ClientHTTP constructed with an empty host fetches against // the page's own origin. fetch(url) in JS handles a leading-slash path // by resolving it against window.location, so we just hand the path // straight through. Useful for fetching files served by whatever static // server is hosting the wasm (e.g. ./cert-hash.txt for WebTransport). std::string url; if (host.empty()) { url = path; } else { std::string scheme = request.scheme.empty() ? std::string("https") : request.scheme; std::string authority = request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority; url = scheme + "://" + authority + path; } std::string headerStr = SerialiseHeaders(request.headers); Crafter::NetworkBrowserBindings::crafterNetworkFetch( method.data(), static_cast(method.size()), url.data(), static_cast(url.size()), headerStr.data(), static_cast(headerStr.size()), request.body.data(), static_cast(request.body.size()), id); } extern "C" { // JS allocates `headersPtr` and `bodyPtr` via WasmAlloc, copies the // response into them, then transfers ownership across this call. We // free the buffers after copying into the HTTPResponse. __attribute__((export_name("CrafterNetworkOnFetchComplete"))) void CrafterNetworkOnFetchComplete(std::int32_t callbackId, std::int32_t status, char* headersPtr, std::int32_t headersLen, char* bodyPtr, std::int32_t bodyLen) { auto& callbacks = Callbacks(); auto it = callbacks.find(callbackId); if (it == callbacks.end()) { std::free(headersPtr); std::free(bodyPtr); return; } HTTPResponse response; response.status = std::to_string(status); if (headersPtr && headersLen > 0) { ParseHeaders(std::string_view(headersPtr, static_cast(headersLen)), response); } if (bodyPtr && bodyLen > 0) { response.body.assign(bodyPtr, static_cast(bodyLen)); } std::free(headersPtr); std::free(bodyPtr); auto onSuccess = std::move(it->second.onSuccess); callbacks.erase(it); if (onSuccess) onSuccess(std::move(response)); } __attribute__((export_name("CrafterNetworkOnFetchError"))) void CrafterNetworkOnFetchError(std::int32_t callbackId, char* messagePtr, std::int32_t messageLen) { auto& callbacks = Callbacks(); auto it = callbacks.find(callbackId); if (it == callbacks.end()) { std::free(messagePtr); return; } std::string msg(messagePtr ? messagePtr : "", static_cast(messageLen)); std::free(messagePtr); auto onError = std::move(it->second.onError); callbacks.erase(it); if (onError) onError(std::move(msg)); } // WasmAlloc / WasmFree are the buffer-marshalling primitives the JS // bridge calls into. Crafter.Graphics's Dom backend defines the same // pair; we mark ours weak so the two libraries can coexist in one // executable without a duplicate-symbol error. __attribute__((export_name("WasmAlloc"), weak)) void* WasmAlloc(std::int32_t size) { return std::malloc(static_cast(size)); } __attribute__((export_name("WasmFree"), weak)) void WasmFree(void* ptr) { std::free(ptr); } }