new tests

This commit is contained in:
Jorijn van der Graaf 2026-05-28 16:48:55 +02:00
commit f5f142f993
15 changed files with 13 additions and 140 deletions

View file

@ -0,0 +1,182 @@
/*
Crafter® Build
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 version 3.0 as published by the Free Software Foundation;
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
*/
// End-to-end WebTransport echo. Spins up a ListenerHTTP with a wtRoutes
// handler that echoes back whatever the peer sent on each new bidi
// stream, then drives a hand-rolled client side using raw ClientQUIC +
// HTTP3 framing (there's no ClientWebTransport class yet; that's Phase 4).
// Verifies:
// - extended CONNECT is accepted, 200 OK delivered without FIN
// - WT_STREAM bidi framing parses correctly on both sides
// - echoed payload round-trips byte-for-byte
import Crafter.Network;
import Crafter.Thread;
import std;
using namespace Crafter;
namespace {
// Helper: read one HTTP/3 frame off `stream` into a freshly-allocated
// buffer. Returns (frameType, payload). The peeked-but-unconsumed
// tail bytes (e.g. start of the next frame) are PrependReceived'd
// back onto the stream.
std::pair<std::uint64_t, std::vector<char>> ReadFrame(QUICStream& stream) {
std::vector<char> buf;
// First varint = frame type.
std::uint64_t type = 0; std::size_t cn = 0;
while (true) {
const auto* p = reinterpret_cast<const std::uint8_t*>(buf.data());
if (HTTP3::DecodeVarint(p, buf.size(), type, cn)) break;
auto chunk = stream.RecieveSync();
buf.insert(buf.end(), chunk.begin(), chunk.end());
}
std::vector<char> afterType(buf.begin() + cn, buf.end());
buf = std::move(afterType);
// Second varint = frame length.
std::uint64_t len = 0; std::size_t lc = 0;
while (true) {
const auto* p = reinterpret_cast<const std::uint8_t*>(buf.data());
if (HTTP3::DecodeVarint(p, buf.size(), len, lc)) break;
auto chunk = stream.RecieveSync();
buf.insert(buf.end(), chunk.begin(), chunk.end());
}
std::vector<char> afterLen(buf.begin() + lc, buf.end());
buf = std::move(afterLen);
// Read enough for the full payload.
while (buf.size() < len) {
auto chunk = stream.RecieveSync();
buf.insert(buf.end(), chunk.begin(), chunk.end());
}
std::vector<char> payload(buf.begin(), buf.begin() + len);
std::vector<char> tail(buf.begin() + len, buf.end());
if (!tail.empty()) stream.PrependReceived(std::move(tail));
return {type, std::move(payload)};
}
}
int main() {
ThreadPool::Start();
constexpr std::string_view kPayload = "hello-webtransport";
constexpr std::uint16_t kPort = 8083;
// ── Server ────────────────────────────────────────────────────────
QUICServerCredentials serverCreds;
serverCreds.selfSigned = true;
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> httpRoutes = {};
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes = {
{"/echo", [](WebTransportSession& session) {
session.OnStream([](QUICStream peerStream) {
try {
auto bytes = peerStream.RecieveUntilCloseSync();
peerStream.SendSync(bytes.data(),
static_cast<std::uint32_t>(bytes.size()),
/*finish=*/true);
} catch (...) {}
});
}},
};
ListenerAsyncHTTP listener(kPort, serverCreds, std::move(httpRoutes), std::move(wtRoutes));
try {
// ── Client (hand-rolled WT bring-up over raw ClientQUIC) ──────
QUICClientCredentials clientCreds;
clientCreds.insecureNoServerValidation = true;
ClientQUIC quic("localhost", kPort, std::string(HTTP3::kAlpn), clientCreds);
// Drain peer-initiated unidi streams (its control + QPACK streams).
// Without this they'd back up and msquic might abort the connection.
quic.OnStream([](QUICStream stream) {
try { while (true) (void)stream.RecieveSync(); } catch (...) {}
});
// Our outgoing control stream + WT-aware SETTINGS prelude.
QUICStream controlStream = quic.OpenStream(/*unidirectional=*/true);
auto prelude = HTTP3::BuildWebTransportControlStreamPrelude(/*maxSessions=*/1);
controlStream.SendSync(prelude.data(),
static_cast<std::uint32_t>(prelude.size()),
/*finish=*/false);
// CONNECT request stream. Send HEADERS, do NOT FIN — the stream
// is the session-control stream and stays open for its lifetime.
QUICStream connectStream = quic.OpenStream(/*unidirectional=*/false);
std::vector<std::pair<std::string, std::string>> connectFields = {
{":method", "CONNECT"},
{":scheme", "https"},
{":authority", "localhost"},
{":path", "/echo"},
{":protocol", "webtransport"},
};
auto headerPayload = HTTP3::EncodeFieldSection(connectFields);
std::vector<std::uint8_t> connectWire;
HTTP3::WriteFrame(connectWire, HTTP3::kFrameHeaders,
headerPayload.data(), headerPayload.size());
connectStream.SendSync(connectWire.data(),
static_cast<std::uint32_t>(connectWire.size()),
/*finish=*/false);
// Read the response HEADERS frame.
auto [respType, respPayload] = ReadFrame(connectStream);
if (respType != HTTP3::kFrameHeaders) {
std::println("bad response frame type: {}", respType);
return 1;
}
auto respFields = HTTP3::DecodeFieldSection(
reinterpret_cast<const std::uint8_t*>(respPayload.data()),
respPayload.size());
std::string status;
for (auto& [k, v] : respFields) if (k == ":status") status = v;
if (status != "200") {
std::println("CONNECT rejected with status {}", status);
return 1;
}
// Session is ready. session_id equals the CONNECT stream's QUIC
// stream id — same number on both ends of the wire.
std::uint64_t sessionId = connectStream.GetStreamId();
// Open a WT data bidi stream. Prefix: varint(0x41) varint(sessionId).
QUICStream wtStream = quic.OpenStream(/*unidirectional=*/false);
auto prefix = HTTP3::BuildWtBidiPrefix(sessionId);
std::vector<std::uint8_t> wire;
wire.insert(wire.end(), prefix.begin(), prefix.end());
wire.insert(wire.end(), kPayload.begin(), kPayload.end());
wtStream.SendSync(wire.data(),
static_cast<std::uint32_t>(wire.size()),
/*finish=*/true);
// Server echoes the payload (the prefix has already been stripped
// server-side; the bytes we read here are pure echo).
auto echoed = wtStream.RecieveUntilCloseSync();
std::string got(echoed.begin(), echoed.end());
if (got == kPayload) {
std::_Exit(0);
}
std::println("payload mismatch: expected '{}', got '{}'", kPayload, got);
return 1;
} catch (std::exception& e) {
std::println("client failed: {}", e.what());
return 1;
}
}