Crafter.Network/project.cpp
catbot 9c22cbe09e feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack
HTTP/1.1 was plaintext-only, which left `https://` to either an HTTP/3
listener or a terminating proxy in front. Neither helps the callers this
stack exists for — curl scripts, CI tooling, old proxies — so wrap the
transport in libssl instead.

Two new partitions:

  :Stream  a ByteStream with per-call deadlines on both directions, plus
           the plaintext socket implementation. The HTTP/1.1 client and
           listener now hold a ByteStream& and never learn which
           transport they have, which is what lets one code path serve
           both schemes.
  :TLS     TLSContext/TLSStream over OpenSSL 3, with credentials for both
           roles: chain and hostname verification on by default, private
           trust anchors, client certificates, mutual TLS, ALPN, and an
           in-process self-signed certificate for development.

Both descriptors go non-blocking and every read and write is driven by
poll() against a deadline. That is required for TLS — a blocking
descriptor cannot express a handshake timeout — and it means a plaintext
write can now time out too, instead of parking forever against a peer
that stopped reading.

ClientHTTP1 and ListenerHTTP1 gain credential-taking constructors; the
existing ones still speak http://. The listener handshakes on the
connection's own thread, so a peer that stalls mid-handshake costs one
thread rather than the accept loop, and a failed handshake is counted
rather than logged — on a public port it is ordinary traffic.

MessageParser gains SetDefaultScheme so origin-form targets report the
scheme the transport actually used; handlers shared with ListenerHTTP now
see the same "https" they would over HTTP/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:13:30 +00:00

152 lines
7.1 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) {
constexpr std::array<std::string_view, 15> networkInterfaces = {
"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-HTTP1",
"interfaces/Crafter.Network-Stream",
"interfaces/Crafter.Network-TLS",
"interfaces/Crafter.Network-ClientHTTP1",
"interfaces/Crafter.Network-ListenerHTTP1",
"interfaces/Crafter.Network-HTTP3",
"interfaces/Crafter.Network-ClientQUIC",
"interfaces/Crafter.Network-ListenerQUIC",
"interfaces/Crafter.Network-WebTransport",
};
std::vector<std::string> depArgs(args.begin(), args.end());
Configuration* thread = 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;
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, 11> networkImplementations = {
"implementations/Crafter.Network-ClientTCP",
"implementations/Crafter.Network-ListenerTCP",
"implementations/Crafter.Network-ClientHTTP",
"implementations/Crafter.Network-ListenerHTTP",
"implementations/Crafter.Network-Stream",
"implementations/Crafter.Network-TLS",
"implementations/Crafter.Network-ClientHTTP1",
"implementations/Crafter.Network-ListenerHTTP1",
"implementations/Crafter.Network-ClientQUIC",
"implementations/Crafter.Network-ListenerQUIC",
"implementations/Crafter.Network-WebTransport",
};
// msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC.
// Cloned + built via CMake into the per-project external cache; no system
// package required. Submodules (quictls / clog / etc.) come via the
// recursive clone Crafter.Build performs. 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.branch = "main";
msquic.builder = ExternalBuilder::CMake;
msquic.options = {
"-DQUIC_TLS_LIB=quictls",
"-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" };
// 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::array<fs::path, 15> ifaces;
std::ranges::copy(networkInterfaces, ifaces.begin());
std::array<fs::path, 11> impls;
std::ranges::copy(networkImplementations, impls.begin());
cfg.GetInterfacesAndImplementations(ifaces, impls);
// Linux-only: msquic + POSIX socket backends. The browser path above
// returns early, so wasm builds skip these. Each test links the local
// crafter-network static lib via .Dependencies({ &cfg }).
if (cfg.target == "x86_64-pc-linux-gnu") {
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP1").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("ShouldSurviveAbuseHTTP1").Dependencies({ &cfg });
}
return cfg;
}