2026-07-22 17:55:45 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
2026-05-06 01:06:05 +02:00
|
|
|
import std;
|
|
|
|
|
import Crafter.Build;
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
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
|
|
|
constexpr std::array<std::string_view, 15> networkInterfaces = {
|
2026-05-06 01:06:05 +02:00
|
|
|
"interfaces/Crafter.Network",
|
|
|
|
|
"interfaces/Crafter.Network-ClientTCP",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerTCP",
|
|
|
|
|
"interfaces/Crafter.Network-ClientHTTP",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerHTTP",
|
|
|
|
|
"interfaces/Crafter.Network-HTTP",
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
"interfaces/Crafter.Network-HTTP1",
|
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
|
|
|
"interfaces/Crafter.Network-Stream",
|
|
|
|
|
"interfaces/Crafter.Network-TLS",
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
"interfaces/Crafter.Network-ClientHTTP1",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerHTTP1",
|
2026-05-07 00:06:44 +02:00
|
|
|
"interfaces/Crafter.Network-HTTP3",
|
2026-05-06 04:06:17 +02:00
|
|
|
"interfaces/Crafter.Network-ClientQUIC",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerQUIC",
|
2026-05-19 02:53:50 +02:00
|
|
|
"interfaces/Crafter.Network-WebTransport",
|
2026-05-06 01:06:05 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 = "./";
|
2026-05-06 04:06:17 +02:00
|
|
|
cfg.name = "crafter-network";
|
|
|
|
|
cfg.outputName = "crafter-network";
|
2026-05-06 01:06:05 +02:00
|
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
|
|
|
ApplyStandardArgs(cfg, args);
|
|
|
|
|
cfg.dependencies = { thread };
|
|
|
|
|
|
2026-05-19 02:53:50 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
constexpr std::array<std::string_view, 11> networkImplementations = {
|
2026-05-19 02:53:50 +02:00
|
|
|
"implementations/Crafter.Network-ClientTCP",
|
|
|
|
|
"implementations/Crafter.Network-ListenerTCP",
|
|
|
|
|
"implementations/Crafter.Network-ClientHTTP",
|
|
|
|
|
"implementations/Crafter.Network-ListenerHTTP",
|
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
|
|
|
"implementations/Crafter.Network-Stream",
|
|
|
|
|
"implementations/Crafter.Network-TLS",
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
"implementations/Crafter.Network-ClientHTTP1",
|
|
|
|
|
"implementations/Crafter.Network-ListenerHTTP1",
|
2026-05-19 02:53:50 +02:00
|
|
|
"implementations/Crafter.Network-ClientQUIC",
|
|
|
|
|
"implementations/Crafter.Network-ListenerQUIC",
|
|
|
|
|
"implementations/Crafter.Network-WebTransport",
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-06 04:06:17 +02:00
|
|
|
// 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",
|
2026-05-06 01:06:05 +02:00
|
|
|
};
|
2026-05-06 04:06:17 +02:00
|
|
|
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" };
|
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
|
|
|
|
|
|
|
|
// 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;
|
2026-05-06 04:06:17 +02:00
|
|
|
std::ranges::copy(networkInterfaces, ifaces.begin());
|
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
|
|
|
std::array<fs::path, 11> impls;
|
2026-05-06 04:06:17 +02:00
|
|
|
std::ranges::copy(networkImplementations, impls.begin());
|
|
|
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
2026-05-06 01:06:05 +02:00
|
|
|
|
2026-05-28 16:48:55 +02:00
|
|
|
// 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 });
|
test(http): replay one fallback route table over both listeners
The point of the feature is that a URL means the same thing over either
protocol, so the test states it that way: one route map plus one
fallback, registered with ListenerHTTP1 and ListenerHTTP, asked the same
eight questions, asserting identical answers.
Covers exact routes beating the fallback, query strings still routing to
the bare path, the fallback seeing the full target including the query,
the fallback choosing its own status (404 and 303), a throwing fallback
becoming a 500, and an unset fallback still producing the listener's own
synthetic 404.
Verified against a broken build both ways: dropping the fallback lookup
fails 9 checks, dropping the query-strip fails 2.
2026-07-28 19:42:16 +00:00
|
|
|
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
|
2026-05-29 16:56:07 +02:00
|
|
|
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
|
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies
and CI tooling still speak nothing but HTTP/1.1. This adds that path
using the request/response types the HTTP/3 stack already uses, so a
route handler or call site moves between the two protocols by changing
the class name.
- :HTTP1 — transport-free wire format. Serialisation with the framing
headers owned by the serialiser, and an incremental parser that takes
arbitrary socket chunks and yields one message at a time: keep-alive,
pipelining, content-length and chunked bodies (with trailers),
read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and
Expect: 100-continue. Ambiguous framing is rejected rather than
guessed at (content-length with transfer-encoding, disagreeing
content-lengths, whitespace before a colon), and CR/LF in a value we
are asked to serialise is refused.
- ClientHTTP1 — persistent connection, redialling once when a pooled
connection turns out to have been closed by the peer, which is the
race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a
response byte has arrived.
- ListenerHTTP1 — one thread per connection (keep-alive connections are
idle most of their life and would pin every ThreadPool thread),
automatic Date, HEAD, 100-continue, handler-requested close, idle and
request timeouts, and 400/404/500 responses. Routes fall back to the
query-stripped path so `/thing?x=1` reaches the handler for `/thing`.
No TLS: this is `http://` only. Encrypted traffic still goes over
HTTP/3, or through a TLS-terminating proxy.
Tests: codec unit tests including the malformed inputs above, a
client/server round-trip, keep-alive and stale-connection recovery, a
10 MiB body both ways, and interop both directions against curl and
python3's http.server (skipped when those are not installed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveLargeHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
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 });
|
fix(http1): close a finished connection instead of holding it until reap
A connection's socket was owned by the registry entry and only released
when the next accept() reaped it, so a peer we had finished with — after
a 408, a 400, or a `connection: close` — never saw EOF and sat waiting
for a server that was done talking. On a server that goes quiet it also
held every descriptor from the last burst indefinitely.
The connection thread now closes its own socket the moment Serve()
returns, under the registry lock so Stop()'s shutdown() can never name a
descriptor that has already been released, and Stop() waits on a
condition variable for the last thread rather than assuming the vector
it moved out is quiescent. Adopt() is also fully guarded: it runs on
ListenerTCP's accept loop, which has no handler, so anything escaping it
would abort the process.
Found by ShouldSurviveAbuseHTTP1, added here: 24 concurrent keep-alive
clients, peers that vanish mid-request or send garbage, and a peer that
stalls forever — the server must keep serving and still stop promptly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:56:53 +00:00
|
|
|
cfg.AddTest("ShouldSurviveAbuseHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:06:05 +02:00
|
|
|
return cfg;
|
|
|
|
|
}
|