ShouldShareSelfSignedCertificateQUIC: six processes, forked before any thread or msquic exists and released together, each start two threads that create a selfSigned listener at once against an empty cache (XDG_CACHE_HOME pointed at a fresh directory) and with no openssl on PATH, and connect to both. All twelve listeners serve, and all six processes report the same certificate hash and a path under XDG_CACHE_HOME. The directory is 0700 and the key 0600, nothing temporary is left behind, a process started later serves the same certificate, and a mismatched key planted on disk, what the race used to leave, is replaced by the next process rather than loaded. Against the previous implementation every child fails (`openssl: command not found`), as do the location and permission checks. The parent starts no thread, so forking stays safe, and its watchdog is alarm(55); each child's is alarm(40). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
217 lines
11 KiB
C++
217 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("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("ShouldShareSelfSignedCertificateQUIC").Dependencies({ &cfg });
|
|
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
|
|
} else {
|
|
// Only a --no-quic build can show that msquic stayed out.
|
|
cfg.AddTest("ShouldBuildWithoutQUIC").Dependencies({ &cfg });
|
|
}
|
|
}
|
|
|
|
return cfg;
|
|
}
|