199 lines
8.2 KiB
C++
199 lines
8.2 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 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<void(HTTPResponse)> onSuccess;
|
||
|
|
std::function<void(std::string)> 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<std::int32_t, FetchCallbacks>& Callbacks() {
|
||
|
|
static std::unordered_map<std::int32_t, FetchCallbacks> 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<std::string, std::string>& 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<Impl>()) {}
|
||
|
|
|
||
|
|
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<void(HTTPResponse)> onSuccess,
|
||
|
|
std::function<void(std::string)> 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<std::int32_t>(method.size()),
|
||
|
|
url.data(), static_cast<std::int32_t>(url.size()),
|
||
|
|
headerStr.data(), static_cast<std::int32_t>(headerStr.size()),
|
||
|
|
request.body.data(), static_cast<std::int32_t>(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<std::size_t>(headersLen)), response);
|
||
|
|
}
|
||
|
|
if (bodyPtr && bodyLen > 0) {
|
||
|
|
response.body.assign(bodyPtr, static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(size)); }
|
||
|
|
|
||
|
|
__attribute__((export_name("WasmFree"), weak))
|
||
|
|
void WasmFree(void* ptr) { std::free(ptr); }
|
||
|
|
}
|