A ListenerQUIC hands each accepted connection to its callback as a heap-allocated ClientQUIC the callback owns, and nothing said when to delete it: the SHUTDOWN_COMPLETE handler closed the msquic handle of a connection the peer or the idle timeout ended, but the object stayed, so a server either leaked its peers or deleted them blind, and one still alive at exit holds msquic's registration open until its idle timeout. OnClosed(callback) is told once when the connection has shut down for any reason but this object's destruction: the peer closed it, the idle timeout or a transport error ended it, or Stop() was called. It runs on the msquic worker, in the SHUTDOWN_COMPLETE handler, after that handler has claimed and closed the handle, so deleting the ClientQUIC from inside it is safe: ~ClientQUIC then finds no handle to claim and waits for nothing. Delivering it there, rather than as a pool task, keeps it independent of a pool the host shares and possibly saturates; the cost is the usual worker rule, which the interface spells out (no blocking, no *Sync call, no deleting another ClientQUIC from inside it). A callback set after the close, which happens when the peer goes before a ListenerQUIC's connectCallback runs, fires as a pool task instead, never from inside OnClosed, where the caller is still using the object. Its state is a shared CloseNotice rather than part of the Impl, since the handler fires it after giving up the handle, when ~ClientQUIC on another thread no longer waits for the worker and may free the Impl. The rules are std::stop_callback's: ~ClientQUIC marks the owner gone first, after which the callback never starts; one already running on another thread is waited for, so the owner may free what it uses as soon as the destructor returns; one on the destructor's own thread, a callback deleting its connection, is not. The handler takes the handle and the notice under one lock, so the destructor cannot slip between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
11 KiB
C++
220 lines
11 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
// TCP, HTTP/1.1 and TLS: what every native build has.
|
|
constexpr std::array<std::string_view, 10> networkInterfaces = {
|
|
"interfaces/Crafter.Network",
|
|
"interfaces/Crafter.Network-ClientTCP",
|
|
"interfaces/Crafter.Network-ListenerTCP",
|
|
"interfaces/Crafter.Network-HTTP",
|
|
"interfaces/Crafter.Network-HTTP1",
|
|
"interfaces/Crafter.Network-Stream",
|
|
"interfaces/Crafter.Network-TLS",
|
|
"interfaces/Crafter.Network-URL",
|
|
"interfaces/Crafter.Network-ClientHTTP1",
|
|
"interfaces/Crafter.Network-ListenerHTTP1",
|
|
};
|
|
// HTTP/3, QUIC and WebTransport, over msquic. --no-quic leaves them out.
|
|
constexpr std::array<std::string_view, 6> quicInterfaces = {
|
|
"interfaces/Crafter.Network-ClientHTTP",
|
|
"interfaces/Crafter.Network-ListenerHTTP",
|
|
"interfaces/Crafter.Network-HTTP3",
|
|
"interfaces/Crafter.Network-ClientQUIC",
|
|
"interfaces/Crafter.Network-ListenerQUIC",
|
|
"interfaces/Crafter.Network-WebTransport",
|
|
};
|
|
|
|
// --no-quic is this project's own flag, so it stays out of the
|
|
// dependencies' args: Crafter.Thread would otherwise build a second copy
|
|
// of itself, hashed apart from the one the consumer imports.
|
|
std::vector<std::string> depArgs;
|
|
for (std::string_view arg : args) {
|
|
if (arg != "--no-quic") depArgs.emplace_back(arg);
|
|
}
|
|
// Crafter.Thread comes from forgejo, or with --local from the checkout
|
|
// next to this one. --local stays in depArgs, so Crafter.Thread resolves
|
|
// its own dependencies the same way.
|
|
const bool useLocal = std::ranges::find(args, std::string_view("--local")) != args.end();
|
|
Configuration* thread = useLocal
|
|
? LocalProject({ .projectFile = "../Crafter.Thread/project.cpp", .args = depArgs })
|
|
: GitProject({
|
|
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Thread.git" },
|
|
.args = depArgs,
|
|
});
|
|
|
|
Configuration cfg;
|
|
cfg.path = "./";
|
|
cfg.name = "crafter-network";
|
|
cfg.outputName = "crafter-network";
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
ArgQuery opts = ApplyStandardArgs(cfg, args);
|
|
cfg.dependencies = { thread };
|
|
|
|
// Browser path: any wasm32-* target gets the browser network stack
|
|
// (fetch + WebTransport via JS glue). msquic and the POSIX socket
|
|
// backends are skipped; the listener / TCP partitions stub to empty
|
|
// modules via #ifdef CRAFTER_NETWORK_BROWSER in their interface files.
|
|
// HTTP3 (varint / frame / QPACK codec) is dropped entirely — it threw
|
|
// exceptions for protocol errors, which the wasm build's -fno-exceptions
|
|
// forbids, and the browser's fetch() handles HTTP-layer framing itself.
|
|
bool browser = cfg.target.find("wasm") != std::string::npos;
|
|
if (browser) {
|
|
cfg.defines.push_back({"CRAFTER_NETWORK_BROWSER", ""});
|
|
|
|
std::array<fs::path, 9> browserIfaces = {
|
|
"interfaces/Crafter.Network",
|
|
"interfaces/Crafter.Network-ClientTCP",
|
|
"interfaces/Crafter.Network-ListenerTCP",
|
|
"interfaces/Crafter.Network-ClientHTTP",
|
|
"interfaces/Crafter.Network-ListenerHTTP",
|
|
"interfaces/Crafter.Network-HTTP",
|
|
"interfaces/Crafter.Network-ClientQUIC",
|
|
"interfaces/Crafter.Network-ListenerQUIC",
|
|
"interfaces/Crafter.Network-WebTransport",
|
|
};
|
|
std::array<fs::path, 2> browserImpls = {
|
|
"implementations/Crafter.Network-ClientHTTP-Browser",
|
|
"implementations/Crafter.Network-ClientQUIC-Browser",
|
|
};
|
|
cfg.GetInterfacesAndImplementations(browserIfaces, browserImpls);
|
|
|
|
// JS glue shipped alongside the .wasm. The consuming executable's
|
|
// wasi-browser runtime merges this into the env import object
|
|
// before instantiation (mirrors Crafter.Graphics/dom-env.js).
|
|
cfg.files.emplace_back(fs::path("additional/network-env.js"));
|
|
return cfg;
|
|
}
|
|
|
|
constexpr std::array<std::string_view, 6> networkImplementations = {
|
|
"implementations/Crafter.Network-ClientTCP",
|
|
"implementations/Crafter.Network-ListenerTCP",
|
|
"implementations/Crafter.Network-Stream",
|
|
"implementations/Crafter.Network-TLS",
|
|
"implementations/Crafter.Network-ClientHTTP1",
|
|
"implementations/Crafter.Network-ListenerHTTP1",
|
|
};
|
|
constexpr std::array<std::string_view, 5> quicImplementations = {
|
|
"implementations/Crafter.Network-ClientHTTP",
|
|
"implementations/Crafter.Network-ListenerHTTP",
|
|
"implementations/Crafter.Network-ClientQUIC",
|
|
"implementations/Crafter.Network-ListenerQUIC",
|
|
"implementations/Crafter.Network-WebTransport",
|
|
};
|
|
|
|
// --no-quic: TCP, HTTP/1.1 and TLS only, for consumers that need no more.
|
|
// msquic takes a recursive clone and a CMake build, so leaving it out is
|
|
// most of a fresh build. The browser build above has no msquic to leave
|
|
// out and ignores the flag.
|
|
const bool quic = !opts.Has("--no-quic");
|
|
if (quic) {
|
|
// msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC.
|
|
// Cloned + built via CMake into the per-project external cache; no system
|
|
// package required. Pinned to a release (v2.6.3): main moves, and its
|
|
// submodules (quictls / clog / etc.) with it. Crafter.Build checks the
|
|
// submodules out at the revisions the pinned commit records. We disable
|
|
// msquic's own tests, tools and perf binaries since we only need the
|
|
// library.
|
|
ExternalDependency& msquic = cfg.externalDependencies.emplace_back();
|
|
msquic.name = "msquic";
|
|
msquic.source.url = "https://github.com/microsoft/msquic.git";
|
|
msquic.source.commit = "16ca4d8d785d34fb22285e7c652e477ac16475a5";
|
|
msquic.builder = ExternalBuilder::CMake;
|
|
// TLS from the system's OpenSSL (3.5 or newer, which has the QUIC
|
|
// API), the same libssl the HTTP/1.1 TLS code links: one OpenSSL in
|
|
// the process. The quictls fork msquic otherwise builds and bundles is
|
|
// an older OpenSSL that still reads the system's openssl.cnf, and the
|
|
// Flatpak runtime's (an engine section for the runtime's own OpenSSL,
|
|
// config_diagnostics on) makes its initialisation fail: MsQuicOpen2
|
|
// returned QUIC_STATUS_TLS_ERROR (0x7e) there.
|
|
msquic.options = {
|
|
"-DQUIC_TLS_LIB=openssl",
|
|
"-DQUIC_USE_EXTERNAL_OPENSSL=ON",
|
|
"-DQUIC_BUILD_TEST=OFF",
|
|
"-DQUIC_BUILD_TOOLS=OFF",
|
|
"-DQUIC_BUILD_PERF=OFF",
|
|
"-DQUIC_BUILD_SHARED=ON",
|
|
};
|
|
msquic.includeDirs = { "src/inc" };
|
|
// msquic's CMakeLists overrides CMAKE_LIBRARY_OUTPUT_DIRECTORY with
|
|
// QUIC_OUTPUT_DIR (defaults to bin/$<CONFIG>), so libmsquic.so lands in
|
|
// a subdir of the cmake build dir rather than at its root. Point the
|
|
// linker at the actual output location.
|
|
msquic.libDirs = { "bin/Release" };
|
|
msquic.libs = { "msquic" };
|
|
} else {
|
|
cfg.defines.push_back({"CRAFTER_NETWORK_NO_QUIC", ""});
|
|
}
|
|
|
|
// libssl/libcrypto — the TLS transport behind :TLS, i.e. https:// on the
|
|
// HTTP/1.1 client and listener. A system package rather than a built
|
|
// external: OpenSSL 3 is on every platform we target, and building it here
|
|
// would mean shipping a second TLS stack alongside the one msquic already
|
|
// links (quictls, which keeps its symbols to itself inside libmsquic.so).
|
|
cfg.linkFlags.push_back("-lssl");
|
|
cfg.linkFlags.push_back("-lcrypto");
|
|
|
|
std::vector<fs::path> ifaces(networkInterfaces.begin(), networkInterfaces.end());
|
|
std::vector<fs::path> impls(networkImplementations.begin(), networkImplementations.end());
|
|
if (quic) {
|
|
ifaces.insert(ifaces.end(), quicInterfaces.begin(), quicInterfaces.end());
|
|
impls.insert(impls.end(), quicImplementations.begin(), quicImplementations.end());
|
|
}
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
|
|
// Linux-only: the POSIX socket backends, and msquic unless --no-quic.
|
|
// The browser path above returns early, so wasm builds skip these. Each
|
|
// test links the local crafter-network static lib via
|
|
// .Dependencies({ &cfg }). Run the suite with --no-quic as well. Any
|
|
// Linux triple: the toolchain's own is x86_64-unknown-linux-gnu.
|
|
if (cfg.target.find("linux") != std::string::npos) {
|
|
cfg.AddTest("ShouldConfigureTCP").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldRecieveTCP").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldConnectDualStack").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldReportConnectErrors").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldReportIoErrors").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldBindLoopbackEphemeral").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldTimeoutSilentHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldFollowRedirectsHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldParseURLs").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldInteropCurlHTTPS1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldRequireClientCertificateHTTPS1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveHTTPS1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveLargeHTTP1").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSurviveAbuseHTTP1").Dependencies({ &cfg });
|
|
if (quic) {
|
|
cfg.AddTest("ShouldBindLoopbackQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldHonourQUICSettings").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldNotifyClosedConnectionQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveHTTP").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveLargeHTTP").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveQUICDatagram").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldReportClosedQUICStream").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldReportListenerPortQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldReturnFromSendSyncQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldShareSelfSignedCertificateQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldTellPeerOfCloseQUIC").Dependencies({ &cfg });
|
|
} else {
|
|
// Only a --no-quic build can show that msquic stayed out.
|
|
cfg.AddTest("ShouldBuildWithoutQUIC").Dependencies({ &cfg });
|
|
}
|
|
}
|
|
|
|
return cfg;
|
|
}
|