From 45479a46ff9fb25690468d847849973413a322a5 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 6 May 2026 04:06:17 +0200 Subject: [PATCH] added QUIC --- README.md | 9 +- .../Crafter.Network-ClientQUIC.cpp | 531 ++++++++++++++++++ .../Crafter.Network-ListenerQUIC.cpp | 331 +++++++++++ interfaces/Crafter.Network-ClientQUIC.cppm | 172 ++++++ interfaces/Crafter.Network-ListenerQUIC.cppm | 75 +++ interfaces/Crafter.Network.cppm | 4 +- project.cpp | 72 +-- tests/ShouldCompile.cpp | 27 - .../ShouldRecieveHTTP.cpp | 0 tests/ShouldRecieveHTTP/project.cpp | 20 + tests/{ => ShouldSendHTTP}/ShouldSendHTTP.cpp | 0 tests/ShouldSendHTTP/project.cpp | 20 + .../ShouldSendRecieveHTTP.cpp | 0 tests/ShouldSendRecieveHTTP/project.cpp | 20 + .../ShouldSendRecieveKeepaliveHTTP.cpp | 0 .../project.cpp | 20 + .../ShouldSendRecieveLargeHTTP.cpp | 0 tests/ShouldSendRecieveLargeHTTP/project.cpp | 20 + .../ShouldSendRecieveQUICDatagram.cpp | 72 +++ .../ShouldSendRecieveQUICDatagram/project.cpp | 20 + .../ShouldSendRecieveQUICStream.cpp | 81 +++ tests/ShouldSendRecieveQUICStream/project.cpp | 20 + 22 files changed, 1448 insertions(+), 66 deletions(-) create mode 100644 implementations/Crafter.Network-ClientQUIC.cpp create mode 100644 implementations/Crafter.Network-ListenerQUIC.cpp create mode 100644 interfaces/Crafter.Network-ClientQUIC.cppm create mode 100644 interfaces/Crafter.Network-ListenerQUIC.cppm delete mode 100644 tests/ShouldCompile.cpp rename tests/{ => ShouldRecieveHTTP}/ShouldRecieveHTTP.cpp (100%) create mode 100644 tests/ShouldRecieveHTTP/project.cpp rename tests/{ => ShouldSendHTTP}/ShouldSendHTTP.cpp (100%) create mode 100644 tests/ShouldSendHTTP/project.cpp rename tests/{ => ShouldSendRecieveHTTP}/ShouldSendRecieveHTTP.cpp (100%) create mode 100644 tests/ShouldSendRecieveHTTP/project.cpp rename tests/{ => ShouldSendRecieveKeepaliveHTTP}/ShouldSendRecieveKeepaliveHTTP.cpp (100%) create mode 100644 tests/ShouldSendRecieveKeepaliveHTTP/project.cpp rename tests/{ => ShouldSendRecieveLargeHTTP}/ShouldSendRecieveLargeHTTP.cpp (100%) create mode 100644 tests/ShouldSendRecieveLargeHTTP/project.cpp create mode 100644 tests/ShouldSendRecieveQUICDatagram/ShouldSendRecieveQUICDatagram.cpp create mode 100644 tests/ShouldSendRecieveQUICDatagram/project.cpp create mode 100644 tests/ShouldSendRecieveQUICStream/ShouldSendRecieveQUICStream.cpp create mode 100644 tests/ShouldSendRecieveQUICStream/project.cpp diff --git a/README.md b/README.md index 0757137..1076a2e 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ A cross-platform C++ networking library providing TCP and HTTP client/server fun ## Overview -Crafter.Network is a comprehensive networking library designed for modern C++ applications. It provides both TCP and HTTP networking capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks. +Crafter.Network is a comprehensive networking library designed for modern C++ applications. It provides TCP, HTTP, and QUIC networking capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks including real-time multiplayer games. ## Features - **TCP Networking**: Client and server implementations for TCP connections - **HTTP Support**: Full HTTP client and server implementations with routing capabilities +- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync - **Asynchronous Operations**: Thread pool-based async operations for improved performance - **Cross-Platform**: Built for Unix-like systems with socket-based networking - **Modern C++**: Uses C++ modules, STL containers, and modern C++ features @@ -21,10 +22,12 @@ The library follows a modular design using C++20 modules: ### Core Modules - `Crafter.Network`: Main module that exports all components - `Crafter.Network:ClientTCP`: TCP client implementation -- `Crafter.Network:ListenerTCP`: TCP server implementation +- `Crafter.Network:ListenerTCP`: TCP server implementation - `Crafter.Network:ClientHTTP`: HTTP client implementation - `Crafter.Network:ListenerHTTP`: HTTP server implementation - `Crafter.Network:HTTP`: HTTP protocol utilities and data structures +- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams +- `Crafter.Network:ListenerQUIC`: QUIC listener accepting incoming connections ## Components @@ -96,6 +99,8 @@ The library includes comprehensive tests covering: ## Dependencies - Crafter.Thread: Thread pool management for asynchronous operations +- **msquic** — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` recursively into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=quictls`, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC modules. + - On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros). ## Usage Example diff --git a/implementations/Crafter.Network-ClientQUIC.cpp b/implementations/Crafter.Network-ClientQUIC.cpp new file mode 100644 index 0000000..6bb4b23 --- /dev/null +++ b/implementations/Crafter.Network-ClientQUIC.cpp @@ -0,0 +1,531 @@ +/* +Crafter®.Network +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 as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +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 +*/ + +module; +#include +#include +module Crafter.Network:ClientQUIC_impl; +import :ClientQUIC; +import Crafter.Thread; +import std; + +using namespace Crafter; + +namespace { + // Process-wide msquic API table + registration. Initialised lazily on + // first ClientQUIC/ListenerQUIC construction; tear-down happens at + // process exit via the destructor of the static object. + struct MsQuicRuntime { + const QUIC_API_TABLE* api = nullptr; + HQUIC registration = nullptr; + std::mutex initMutex; + bool initialised = false; + + void Ensure() { + std::lock_guard lock(initMutex); + if (initialised) return; + QUIC_STATUS s = MsQuicOpen2(&api); + if (QUIC_FAILED(s)) { + throw QUICException(std::format("MsQuicOpen2 failed: 0x{:x}", static_cast(s))); + } + QUIC_REGISTRATION_CONFIG regConfig{ "crafter.network", QUIC_EXECUTION_PROFILE_LOW_LATENCY }; + s = api->RegistrationOpen(®Config, ®istration); + if (QUIC_FAILED(s)) { + MsQuicClose(api); + api = nullptr; + throw QUICException(std::format("RegistrationOpen failed: 0x{:x}", static_cast(s))); + } + initialised = true; + } + + ~MsQuicRuntime() { + if (registration) api->RegistrationClose(registration); + if (api) MsQuicClose(api); + } + }; + + MsQuicRuntime& Runtime() { + static MsQuicRuntime r; + r.Ensure(); + return r; + } + + // Encode an ALPN string into the wire format msquic expects: a length + // byte followed by the ASCII characters. Lifetime of the returned buffer + // matches the caller's storage in `out`. + QUIC_BUFFER MakeAlpn(const std::string& alpn, std::vector& out) { + if (alpn.size() > 255) throw QUICException("ALPN string too long (max 255)"); + out.assign(alpn.begin(), alpn.end()); + QUIC_BUFFER b{}; + b.Length = static_cast(out.size()); + b.Buffer = out.data(); + return b; + } +} + +// ---------------- QUICStream::Impl ---------------- +struct QUICStream::Impl { + HQUIC handle = nullptr; + ClientQUIC* connection = nullptr; + + std::mutex mtx; + std::condition_variable cv; + std::deque> pending; + bool peerSendClosed = false; + bool shutdownComplete = false; + bool sendInFlight = false; + + static QUIC_STATUS QUIC_API Callback(HQUIC stream, void* ctx, QUIC_STREAM_EVENT* ev) { + auto* self = static_cast(ctx); + switch (ev->Type) { + case QUIC_STREAM_EVENT_RECEIVE: { + std::vector chunk; + std::uint64_t total = 0; + for (std::uint32_t i = 0; i < ev->RECEIVE.BufferCount; ++i) { + total += ev->RECEIVE.Buffers[i].Length; + } + chunk.reserve(static_cast(total)); + for (std::uint32_t i = 0; i < ev->RECEIVE.BufferCount; ++i) { + const QUIC_BUFFER& b = ev->RECEIVE.Buffers[i]; + chunk.insert(chunk.end(), b.Buffer, b.Buffer + b.Length); + } + { + std::lock_guard lk(self->mtx); + if (!chunk.empty()) self->pending.push_back(std::move(chunk)); + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + case QUIC_STREAM_EVENT_SEND_COMPLETE: { + { + std::lock_guard lk(self->mtx); + self->sendInFlight = false; + } + if (ev->SEND_COMPLETE.ClientContext) { + delete[] static_cast(ev->SEND_COMPLETE.ClientContext); + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: { + { + std::lock_guard lk(self->mtx); + self->peerSendClosed = true; + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: { + { + std::lock_guard lk(self->mtx); + self->peerSendClosed = true; + self->shutdownComplete = true; + } + self->cv.notify_all(); + Runtime().api->SetCallbackHandler(stream, nullptr, nullptr); + Runtime().api->StreamClose(stream); + return QUIC_STATUS_SUCCESS; + } + default: + return QUIC_STATUS_SUCCESS; + } + } +}; + +QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection) + : handle(handle), connection(connection), impl(std::make_unique()) +{ + impl->handle = handle; + impl->connection = connection; + Runtime().api->SetCallbackHandler(handle, reinterpret_cast(&Impl::Callback), impl.get()); +} + +QUICStream::QUICStream(QUICStream&& other) noexcept + : handle(other.handle), connection(other.connection), impl(std::move(other.impl)) +{ + other.handle = nullptr; + other.connection = nullptr; +} + +QUICStream& QUICStream::operator=(QUICStream&& other) noexcept { + if (this != &other) { + Stop(); + handle = other.handle; + connection = other.connection; + impl = std::move(other.impl); + other.handle = nullptr; + other.connection = nullptr; + } + return *this; +} + +QUICStream::~QUICStream() { + Stop(); +} + +void QUICStream::Stop() { + if (!handle) return; + Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0); + handle = nullptr; +} + +void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) { + if (!handle) throw QUICClosedException(); + auto* copy = new char[size]; + std::memcpy(copy, buffer, size); + QUIC_BUFFER quicBuf{}; + quicBuf.Buffer = reinterpret_cast(copy); + quicBuf.Length = size; + { + std::lock_guard lk(impl->mtx); + impl->sendInFlight = true; + } + QUIC_SEND_FLAGS flags = finish ? QUIC_SEND_FLAG_FIN : QUIC_SEND_FLAG_NONE; + QUIC_STATUS s = Runtime().api->StreamSend(handle, &quicBuf, 1, flags, copy); + if (QUIC_FAILED(s)) { + delete[] copy; + throw QUICException(std::format("StreamSend failed: 0x{:x}", static_cast(s))); + } + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->sendInFlight || impl->shutdownComplete; }); + if (impl->shutdownComplete) throw QUICClosedException(); +} + +std::vector QUICStream::RecieveSync() { + if (!handle) throw QUICClosedException(); + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; }); + if (!impl->pending.empty()) { + auto out = std::move(impl->pending.front()); + impl->pending.pop_front(); + return out; + } + throw QUICClosedException(); +} + +std::vector QUICStream::RecieveUntilCloseSync() { + if (!handle) throw QUICClosedException(); + std::vector out; + while (true) { + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; }); + while (!impl->pending.empty()) { + auto& chunk = impl->pending.front(); + out.insert(out.end(), chunk.begin(), chunk.end()); + impl->pending.pop_front(); + } + if (impl->peerSendClosed || impl->shutdownComplete) return out; + } +} + +std::vector QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) { + if (!handle) throw QUICClosedException(); + std::vector out; + out.reserve(bufferSize); + while (out.size() < bufferSize) { + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; }); + while (!impl->pending.empty() && out.size() < bufferSize) { + auto& chunk = impl->pending.front(); + std::size_t want = std::min(chunk.size(), bufferSize - out.size()); + out.insert(out.end(), chunk.begin(), chunk.begin() + want); + if (want == chunk.size()) { + impl->pending.pop_front(); + } else { + chunk.erase(chunk.begin(), chunk.begin() + want); + } + } + if (out.size() < bufferSize && (impl->peerSendClosed || impl->shutdownComplete)) { + throw QUICClosedException(); + } + } + return out; +} + +void QUICStream::RecieveAsync(std::function)> cb) { + ThreadPool::Enqueue([this, cb]{ cb(this->RecieveSync()); }); +} +void QUICStream::RecieveUntilCloseAsync(std::function)> cb) { + ThreadPool::Enqueue([this, cb]{ cb(this->RecieveUntilCloseSync()); }); +} +void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function)> cb) { + ThreadPool::Enqueue([this, bufferSize, cb]{ cb(this->RecieveUntilFullSync(bufferSize)); }); +} + +// ---------------- ClientQUIC::Impl ---------------- +struct ClientQUIC::Impl { + HQUIC connection = nullptr; + HQUIC configuration = nullptr; + bool ownsConfiguration = true; + + std::mutex mtx; + std::condition_variable cv; + bool connected = false; + bool closed = false; + QUIC_STATUS shutdownStatus = QUIC_STATUS_SUCCESS; + + std::function onStream; + std::function)> onDatagram; + std::deque> datagramQueue; + + ClientQUIC* outer = nullptr; + + static QUIC_STATUS QUIC_API Callback(HQUIC conn, void* ctx, QUIC_CONNECTION_EVENT* ev) { + auto* self = static_cast(ctx); + switch (ev->Type) { + case QUIC_CONNECTION_EVENT_CONNECTED: { + { + std::lock_guard lk(self->mtx); + self->connected = true; + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: { + { + std::lock_guard lk(self->mtx); + self->closed = true; + if (ev->Type == QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT) { + self->shutdownStatus = ev->SHUTDOWN_INITIATED_BY_TRANSPORT.Status; + } + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: { + { + std::lock_guard lk(self->mtx); + self->closed = true; + } + self->cv.notify_all(); + if (ev->SHUTDOWN_COMPLETE.AppCloseInProgress == 0) { + Runtime().api->ConnectionClose(conn); + self->connection = nullptr; + } + return QUIC_STATUS_SUCCESS; + } + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: { + HQUIC streamHandle = ev->PEER_STREAM_STARTED.Stream; + QUICStream stream(streamHandle, self->outer); + if (self->onStream) { + auto cb = self->onStream; + auto* shared = new QUICStream(std::move(stream)); + ThreadPool::Enqueue([cb, shared]{ + cb(std::move(*shared)); + delete shared; + }); + } else { + // No handler: shut down to avoid leaking a stream. + Runtime().api->StreamShutdown(streamHandle, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); + } + return QUIC_STATUS_SUCCESS; + } + case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: { + std::vector chunk(ev->DATAGRAM_RECEIVED.Buffer->Buffer, + ev->DATAGRAM_RECEIVED.Buffer->Buffer + ev->DATAGRAM_RECEIVED.Buffer->Length); + if (self->onDatagram) { + auto cb = self->onDatagram; + ThreadPool::Enqueue([cb, chunk = std::move(chunk)]() mutable { cb(std::move(chunk)); }); + } else { + std::lock_guard lk(self->mtx); + self->datagramQueue.push_back(std::move(chunk)); + self->cv.notify_all(); + } + return QUIC_STATUS_SUCCESS; + } + case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED: { + // msquic fires this event multiple times per datagram (e.g. + // SENT -> ACKNOWLEDGED). Free the combined QUIC_BUFFER+payload + // allocation only on a terminal state — SENT and LOST_SUSPECT + // are intermediate and may be followed by another transition. + auto state = ev->DATAGRAM_SEND_STATE_CHANGED.State; + if (ev->DATAGRAM_SEND_STATE_CHANGED.ClientContext && + (state == QUIC_DATAGRAM_SEND_LOST_DISCARDED + || state == QUIC_DATAGRAM_SEND_ACKNOWLEDGED + || state == QUIC_DATAGRAM_SEND_ACKNOWLEDGED_SPURIOUS + || state == QUIC_DATAGRAM_SEND_CANCELED)) { + ::operator delete(ev->DATAGRAM_SEND_STATE_CHANGED.ClientContext); + } + return QUIC_STATUS_SUCCESS; + } + default: + return QUIC_STATUS_SUCCESS; + } + } +}; + +static HQUIC OpenClientConfiguration(const std::string& alpn, const QUICClientCredentials& creds) { + std::vector alpnBuf; + QUIC_BUFFER alpnBuffer = MakeAlpn(alpn, alpnBuf); + + QUIC_SETTINGS settings{}; + settings.IsSet.IdleTimeoutMs = 1; + settings.IdleTimeoutMs = 30'000; + settings.IsSet.DatagramReceiveEnabled = 1; + settings.DatagramReceiveEnabled = 1; + + HQUIC cfg = nullptr; + QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1, + &settings, sizeof(settings), nullptr, &cfg); + if (QUIC_FAILED(s)) throw QUICException(std::format("ConfigurationOpen failed: 0x{:x}", static_cast(s))); + + QUIC_CREDENTIAL_CONFIG cc{}; + cc.Type = QUIC_CREDENTIAL_TYPE_NONE; + cc.Flags = QUIC_CREDENTIAL_FLAG_CLIENT; + if (creds.insecureNoServerValidation) { + cc.Flags |= QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION; + } + s = Runtime().api->ConfigurationLoadCredential(cfg, &cc); + if (QUIC_FAILED(s)) { + Runtime().api->ConfigurationClose(cfg); + throw QUICException(std::format("ConfigurationLoadCredential failed: 0x{:x}", static_cast(s))); + } + return cfg; +} + +ClientQUIC::ClientQUIC(const char* host, std::uint16_t port, std::string alpnIn, + QUICClientCredentials creds) + : alpn(std::move(alpnIn)), impl(std::make_unique()) +{ + impl->outer = this; + impl->configuration = OpenClientConfiguration(alpn, creds); + + QUIC_STATUS s = Runtime().api->ConnectionOpen(Runtime().registration, + reinterpret_cast(&Impl::Callback), + impl.get(), &impl->connection); + if (QUIC_FAILED(s)) { + Runtime().api->ConfigurationClose(impl->configuration); + throw QUICException(std::format("ConnectionOpen failed: 0x{:x}", static_cast(s))); + } + s = Runtime().api->ConnectionStart(impl->connection, impl->configuration, QUIC_ADDRESS_FAMILY_UNSPEC, host, port); + if (QUIC_FAILED(s)) { + Runtime().api->ConnectionClose(impl->connection); + Runtime().api->ConfigurationClose(impl->configuration); + throw QUICException(std::format("ConnectionStart failed: 0x{:x}", static_cast(s))); + } + + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return impl->connected || impl->closed; }); + if (!impl->connected) { + throw QUICException(std::format("QUIC handshake failed: 0x{:x}", static_cast(impl->shutdownStatus))); + } +} + +ClientQUIC::ClientQUIC(std::string host, std::uint16_t port, std::string alpnIn, QUICClientCredentials creds) + : ClientQUIC(host.c_str(), port, std::move(alpnIn), std::move(creds)) {} + +ClientQUIC::ClientQUIC(HQUIC connectionHandle, HQUIC serverConfiguration, std::string alpnIn) + : alpn(std::move(alpnIn)), impl(std::make_unique()) +{ + impl->outer = this; + impl->connection = connectionHandle; + impl->configuration = serverConfiguration; + impl->ownsConfiguration = false; + impl->connected = true; + Runtime().api->SetCallbackHandler(connectionHandle, + reinterpret_cast(&Impl::Callback), + impl.get()); +} + +ClientQUIC::ClientQUIC(ClientQUIC&& other) noexcept + : alpn(std::move(other.alpn)), impl(std::move(other.impl)) +{ + if (impl) impl->outer = this; +} + +ClientQUIC::~ClientQUIC() { + if (!impl) return; + if (impl->connection) { + Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0); + Runtime().api->ConnectionClose(impl->connection); + impl->connection = nullptr; + } + if (impl->configuration && impl->ownsConfiguration) { + Runtime().api->ConfigurationClose(impl->configuration); + impl->configuration = nullptr; + } +} + +void ClientQUIC::Stop() { + if (!impl || !impl->connection) return; + Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0); +} + +QUICStream ClientQUIC::OpenStream() { + HQUIC streamHandle = nullptr; + QUICStream stream; + stream.impl = std::make_unique(); + stream.impl->connection = this; + QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, QUIC_STREAM_OPEN_FLAG_NONE, + reinterpret_cast(&QUICStream::Impl::Callback), + stream.impl.get(), &streamHandle); + if (QUIC_FAILED(s)) throw QUICException(std::format("StreamOpen failed: 0x{:x}", static_cast(s))); + stream.handle = streamHandle; + stream.connection = this; + stream.impl->handle = streamHandle; + s = Runtime().api->StreamStart(streamHandle, QUIC_STREAM_START_FLAG_NONE); + if (QUIC_FAILED(s)) { + Runtime().api->StreamClose(streamHandle); + throw QUICException(std::format("StreamStart failed: 0x{:x}", static_cast(s))); + } + return stream; +} + +void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) { + // msquic stores the QUIC_BUFFER pointer (not a copy) on the send queue + // and serialises async on a worker thread. Both the QUIC_BUFFER and the + // payload it points at must outlive the call until DATAGRAM_SEND_STATE + // reports a terminal state. Pack them together in a single allocation. + auto* mem = static_cast(::operator new(sizeof(QUIC_BUFFER) + size)); + auto* hdr = reinterpret_cast(mem); + auto* payload = mem + sizeof(QUIC_BUFFER); + std::memcpy(payload, buffer, size); + hdr->Buffer = payload; + hdr->Length = size; + QUIC_STATUS s = Runtime().api->DatagramSend(impl->connection, hdr, 1, + QUIC_SEND_FLAG_NONE, mem); + if (QUIC_FAILED(s)) { + ::operator delete(mem); + throw QUICException(std::format("DatagramSend failed: 0x{:x}", static_cast(s))); + } +} + +void ClientQUIC::OnStream(std::function cb) { + impl->onStream = std::move(cb); +} + +void ClientQUIC::OnDatagram(std::function)> cb) { + impl->onDatagram = std::move(cb); +} + +std::vector ClientQUIC::RecieveDatagramSync() { + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->datagramQueue.empty() || impl->closed; }); + if (!impl->datagramQueue.empty()) { + auto out = std::move(impl->datagramQueue.front()); + impl->datagramQueue.pop_front(); + return out; + } + throw QUICClosedException(); +} + +HQUIC ClientQUIC::GetHandle() const { return impl ? impl->connection : nullptr; } diff --git a/implementations/Crafter.Network-ListenerQUIC.cpp b/implementations/Crafter.Network-ListenerQUIC.cpp new file mode 100644 index 0000000..022b7b9 --- /dev/null +++ b/implementations/Crafter.Network-ListenerQUIC.cpp @@ -0,0 +1,331 @@ +/* +Crafter®.Network +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 as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +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 +*/ + +module; +#include +#include +#include +#include +module Crafter.Network:ListenerQUIC_impl; +import :ListenerQUIC; +import :ClientQUIC; +import Crafter.Thread; +import std; + +using namespace Crafter; + +namespace { + // Mirror of MakeAlpn from ClientQUIC_impl. Kept private here to avoid + // cross-module-impl coupling; the runtime singleton is reachable via + // any msquic call so we just access it through MsQuicOpen2 indirectly. + QUIC_BUFFER MakeAlpn(const std::string& alpn, std::vector& out) { + if (alpn.size() > 255) throw QUICException("ALPN string too long (max 255)"); + out.assign(alpn.begin(), alpn.end()); + QUIC_BUFFER b{}; + b.Length = static_cast(out.size()); + b.Buffer = out.data(); + return b; + } + + // Reach the same runtime as ClientQUIC_impl by calling MsQuicOpen2 again + // — msquic ref-counts the singleton, so this is safe and returns the same + // table. Registration is per-call, so we open a separate one here. + struct ListenerRuntime { + const QUIC_API_TABLE* api = nullptr; + HQUIC registration = nullptr; + std::mutex initMutex; + bool initialised = false; + void Ensure() { + std::lock_guard lock(initMutex); + if (initialised) return; + QUIC_STATUS s = MsQuicOpen2(&api); + if (QUIC_FAILED(s)) throw QUICException(std::format("MsQuicOpen2 failed: 0x{:x}", static_cast(s))); + QUIC_REGISTRATION_CONFIG regConfig{ "crafter.network.listener", QUIC_EXECUTION_PROFILE_LOW_LATENCY }; + s = api->RegistrationOpen(®Config, ®istration); + if (QUIC_FAILED(s)) { + MsQuicClose(api); + api = nullptr; + throw QUICException(std::format("RegistrationOpen failed: 0x{:x}", static_cast(s))); + } + initialised = true; + } + ~ListenerRuntime() { + if (registration) api->RegistrationClose(registration); + if (api) MsQuicClose(api); + } + }; + + ListenerRuntime& Runtime() { + static ListenerRuntime r; + r.Ensure(); + return r; + } + + // Lazily generate (and cache) a self-signed cert + key pair on disk by + // shelling out to the openssl CLI. Used by ListenerQUIC when the caller + // passes QUICServerCredentials{selfSigned=true}. The pair lives in a + // mkdtemp'd directory under /tmp for the lifetime of the process. + // Intended for dev / LAN play / tests — production should pass real + // cert/key paths. + struct SelfSignedCert { + std::string certPath; + std::string keyPath; + }; + SelfSignedCert& GetSelfSignedCert() { + static std::mutex mtx; + static std::optional cached; + std::lock_guard lk(mtx); + if (cached) return *cached; + + char tmpl[] = "/tmp/crafter-quic-cert-XXXXXX"; + if (mkdtemp(tmpl) == nullptr) { + throw QUICException("mkdtemp failed for self-signed cert dir"); + } + std::string dir = tmpl; + SelfSignedCert s; + s.keyPath = dir + "/key.pem"; + s.certPath = dir + "/cert.pem"; + std::string cmd = std::format( + "openssl req -x509 -newkey rsa:2048 -keyout '{}' -out '{}' " + "-days 1 -nodes -subj '/CN=localhost' >/dev/null 2>&1", + s.keyPath, s.certPath); + int rc = std::system(cmd.c_str()); + if (rc != 0) { + throw QUICException(std::format( + "openssl CLI failed to generate self-signed cert " + "(exit {}); install openssl or pass certPath/keyPath", + rc)); + } + cached = std::move(s); + return *cached; + } +} + +struct ListenerQUIC::Impl { + HQUIC listener = nullptr; + HQUIC configuration = nullptr; + std::vector alpnBuf; + + ListenerQUIC* outer = nullptr; + + // Backlog used by ListenSync* methods to convert msquic's callback model + // to a blocking accept(2)-style loop. + std::mutex mtx; + std::condition_variable cv; + std::deque pendingAccepted; + bool stopRequested = false; + + // Holds the std::thread spawned by ListenAsync* so the destructor can + // join it before tearing the listener down (the lambda captures `this` + // by raw pointer; running it past the destructor is a use-after-free). + std::thread acceptLoop; + + static QUIC_STATUS QUIC_API ConnectionCallbackBootstrap(HQUIC, void*, QUIC_CONNECTION_EVENT*) { + // Real callbacks are installed by ClientQUIC's constructor for the + // server-side branch. This stub exists only for the brief window + // between NEW_CONNECTION and ConnectionSetConfiguration. + return QUIC_STATUS_SUCCESS; + } + + static QUIC_STATUS QUIC_API Callback(HQUIC, void* ctx, QUIC_LISTENER_EVENT* ev) { + auto* self = static_cast(ctx); + switch (ev->Type) { + case QUIC_LISTENER_EVENT_NEW_CONNECTION: { + HQUIC conn = ev->NEW_CONNECTION.Connection; + Runtime().api->SetCallbackHandler(conn, + reinterpret_cast(&ConnectionCallbackBootstrap), nullptr); + QUIC_STATUS s = Runtime().api->ConnectionSetConfiguration(conn, self->configuration); + if (QUIC_FAILED(s)) { + Runtime().api->ConnectionClose(conn); + return s; + } + { + std::lock_guard lk(self->mtx); + self->pendingAccepted.push_back(conn); + } + self->cv.notify_all(); + return QUIC_STATUS_SUCCESS; + } + default: + return QUIC_STATUS_SUCCESS; + } + } +}; + +static HQUIC OpenServerConfiguration(const std::string& alpn, + const QUICServerCredentials& creds, + std::vector& alpnBufOut) { + QUIC_BUFFER alpnBuffer = MakeAlpn(alpn, alpnBufOut); + + QUIC_SETTINGS settings{}; + settings.IsSet.IdleTimeoutMs = 1; + settings.IdleTimeoutMs = 30'000; + settings.IsSet.PeerBidiStreamCount = 1; + settings.PeerBidiStreamCount = 16; + settings.IsSet.PeerUnidiStreamCount = 1; + settings.PeerUnidiStreamCount = 16; + settings.IsSet.DatagramReceiveEnabled = 1; + settings.DatagramReceiveEnabled = 1; + settings.IsSet.ServerResumptionLevel = 1; + settings.ServerResumptionLevel = QUIC_SERVER_RESUME_AND_ZERORTT; + + HQUIC cfg = nullptr; + QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1, + &settings, sizeof(settings), nullptr, &cfg); + if (QUIC_FAILED(s)) throw QUICException(std::format("ConfigurationOpen failed: 0x{:x}", static_cast(s))); + + QUIC_CREDENTIAL_CONFIG cc{}; + QUIC_CERTIFICATE_FILE certFile{}; + std::string effectiveCertPath; + std::string effectiveKeyPath; + if (creds.selfSigned) { + const SelfSignedCert& s = GetSelfSignedCert(); + effectiveCertPath = s.certPath; + effectiveKeyPath = s.keyPath; + } else { + effectiveCertPath = creds.certPath; + effectiveKeyPath = creds.keyPath; + } + certFile.CertificateFile = effectiveCertPath.c_str(); + certFile.PrivateKeyFile = effectiveKeyPath.c_str(); + cc.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE; + cc.CertificateFile = &certFile; + s = Runtime().api->ConfigurationLoadCredential(cfg, &cc); + if (QUIC_FAILED(s)) { + Runtime().api->ConfigurationClose(cfg); + throw QUICException(std::format("ConfigurationLoadCredential failed: 0x{:x}" + " (selfSigned={}, cert={})", + static_cast(s), + creds.selfSigned, creds.certPath)); + } + return cfg; +} + +ListenerQUIC::ListenerQUIC(std::uint16_t port, + std::string alpnIn, + QUICServerCredentials creds, + std::function cb, + std::uint32_t concurrentClientLimit, + std::uint32_t totalClientLimit) + : connectCallback(std::move(cb)) + , concurrentClientLimit(concurrentClientLimit) + , totalClientLimit(totalClientLimit) + , alpn(std::move(alpnIn)) + , impl(std::make_unique()) +{ + impl->outer = this; + impl->configuration = OpenServerConfiguration(alpn, creds, impl->alpnBuf); + + QUIC_STATUS s = Runtime().api->ListenerOpen(Runtime().registration, + reinterpret_cast(&Impl::Callback), + impl.get(), &impl->listener); + if (QUIC_FAILED(s)) { + Runtime().api->ConfigurationClose(impl->configuration); + throw QUICException(std::format("ListenerOpen failed: 0x{:x}", static_cast(s))); + } + + QUIC_BUFFER alpnBuffer{}; + alpnBuffer.Length = static_cast(impl->alpnBuf.size()); + alpnBuffer.Buffer = impl->alpnBuf.data(); + + QUIC_ADDR addr{}; + QuicAddrSetFamily(&addr, QUIC_ADDRESS_FAMILY_UNSPEC); + QuicAddrSetPort(&addr, port); + + s = Runtime().api->ListenerStart(impl->listener, &alpnBuffer, 1, &addr); + if (QUIC_FAILED(s)) { + Runtime().api->ListenerClose(impl->listener); + Runtime().api->ConfigurationClose(impl->configuration); + throw QUICException(std::format("ListenerStart failed: 0x{:x}", static_cast(s))); + } +} + +ListenerQUIC::ListenerQUIC(ListenerQUIC&& other) noexcept + : running(other.running) + , connectCallback(std::move(other.connectCallback)) + , concurrentClientLimit(other.concurrentClientLimit) + , totalClientLimit(other.totalClientLimit) + , alpn(std::move(other.alpn)) + , impl(std::move(other.impl)) + , totalClientCounter(other.totalClientCounter) +{ + if (impl) impl->outer = this; +} + +ListenerQUIC::~ListenerQUIC() { + if (!impl) return; + Stop(); + if (impl->acceptLoop.joinable()) impl->acceptLoop.join(); + if (impl->listener) { + Runtime().api->ListenerClose(impl->listener); + impl->listener = nullptr; + } + if (impl->configuration) { + Runtime().api->ConfigurationClose(impl->configuration); + impl->configuration = nullptr; + } +} + +void ListenerQUIC::Stop() { + running = false; + if (impl && impl->listener) { + Runtime().api->ListenerStop(impl->listener); + std::lock_guard lk(impl->mtx); + impl->stopRequested = true; + } + if (impl) impl->cv.notify_all(); +} + +void ListenerQUIC::ListenSyncSync() { + while (running && totalClientCounter < totalClientLimit) { + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->pendingAccepted.empty() || impl->stopRequested; }); + if (impl->stopRequested) return; + HQUIC conn = impl->pendingAccepted.front(); + impl->pendingAccepted.pop_front(); + lk.unlock(); + connectCallback(new ClientQUIC(conn, impl->configuration, alpn)); + ++totalClientCounter; + } +} + +void ListenerQUIC::ListenSyncAsync() { + while (running && totalClientCounter < totalClientLimit) { + std::unique_lock lk(impl->mtx); + impl->cv.wait(lk, [&]{ return !impl->pendingAccepted.empty() || impl->stopRequested; }); + if (impl->stopRequested) return; + HQUIC conn = impl->pendingAccepted.front(); + impl->pendingAccepted.pop_front(); + lk.unlock(); + std::string a = alpn; + HQUIC cfg = impl->configuration; + auto cb = connectCallback; + ThreadPool::Enqueue([conn, cfg, a, cb]{ cb(new ClientQUIC(conn, cfg, a)); }); + ++totalClientCounter; + } +} + +void ListenerQUIC::ListenAsyncSync() { + impl->acceptLoop = std::thread([this]{ ListenSyncSync(); }); +} + +void ListenerQUIC::ListenAsyncAsync() { + impl->acceptLoop = std::thread([this]{ ListenSyncAsync(); }); +} diff --git a/interfaces/Crafter.Network-ClientQUIC.cppm b/interfaces/Crafter.Network-ClientQUIC.cppm new file mode 100644 index 0000000..1d31b8c --- /dev/null +++ b/interfaces/Crafter.Network-ClientQUIC.cppm @@ -0,0 +1,172 @@ +/* +Crafter®.Network +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 as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +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 +*/ + +module; +#include +export module Crafter.Network:ClientQUIC; +import std; + +namespace Crafter { + export class ListenerQUIC; + + export class QUICException : public std::runtime_error { + public: + using std::runtime_error::runtime_error; + }; + + export class QUICClosedException : public std::exception { + public: + const char* what() const noexcept override { return "QUIC connection closed"; } + }; + + // Server certificate sources. Pick one of (filePaths) or (selfSigned). + // selfSigned generates an in-memory ephemeral cert — fine for dev/LAN. + export struct QUICServerCredentials { + std::string certPath; + std::string keyPath; + bool selfSigned = false; + }; + + // Client-side credential validation. By default we require a real cert. + // insecureNoServerValidation disables peer cert checks — only for dev. + export struct QUICClientCredentials { + bool insecureNoServerValidation = false; + }; + + export class ClientQUIC; + + // A reliable, ordered, bidirectional stream within a QUIC connection. + // Owned by ClientQUIC; do not destroy directly. Obtain via + // ClientQUIC::OpenStream() or via the on-stream callback for inbound + // streams initiated by the peer. + export class QUICStream { + public: + // Underlying msquic HQUIC handle. Treated as opaque by callers. + HQUIC handle = nullptr; + + // The connection that owns this stream (non-owning). + ClientQUIC* connection = nullptr; + + QUICStream() = default; + QUICStream(HQUIC handle, ClientQUIC* connection); + ~QUICStream(); + QUICStream(const QUICStream&) = delete; + QUICStream(QUICStream&&) noexcept; + QUICStream& operator=(QUICStream&&) noexcept; + + // Send a buffer. If finish=true, the send-side of the stream is closed + // after the buffer is delivered (peer will see graceful shutdown). + // Blocks until msquic accepts the buffer; throws on stream/conn close. + void SendSync(const void* buffer, std::uint32_t size, bool finish = false); + + // Block until at least one byte is received; returns the received + // chunk. Throws QUICClosedException once the peer has closed the + // send-side and the buffer is drained. + std::vector RecieveSync(); + + // Read until the peer closes the send-side, accumulating all chunks. + std::vector RecieveUntilCloseSync(); + + // Read exactly bufferSize bytes; throws if the peer closes early. + std::vector RecieveUntilFullSync(std::uint32_t bufferSize); + + // Async variants: dispatched on Crafter.Thread's ThreadPool. + void RecieveAsync(std::function)> recieveCallback); + void RecieveUntilCloseAsync(std::function)> recieveCallback); + void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function)> recieveCallback); + + // Cleanly shut down the stream (both directions). + void Stop(); + + private: + struct Impl; + std::unique_ptr impl; + friend class ClientQUIC; + }; + + // A QUIC connection. On the client side, constructing one initiates the + // handshake and blocks until it succeeds (or throws on failure). On the + // server side, ListenerQUIC instantiates these for accepted peers. + // + // A connection multiplexes: + // - Reliable, ordered streams (open via OpenStream() / observe inbound + // via OnStream()). + // - Unreliable, unordered datagrams (SendDatagram() / OnDatagram()). + // + // Lifetime: ~ClientQUIC closes the connection. Streams obtained from + // OpenStream() are scoped to the connection and must be destroyed (or + // moved out) before the ClientQUIC. + export class ClientQUIC { + public: + // ALPN identifier exchanged in the handshake. Both peers must agree. + // For 3DForts use e.g. "f3d/1" or similar — a short stable token. + std::string alpn; + + // Client constructor: connects to host:port using QUIC. ALPN must + // match the listener. Throws QUICException on connect failure. + ClientQUIC(const char* host, std::uint16_t port, std::string alpn, + QUICClientCredentials creds = {}); + ClientQUIC(std::string host, std::uint16_t port, std::string alpn, + QUICClientCredentials creds = {}); + + // Server-side constructor used by ListenerQUIC for accepted peers. + // Takes ownership of an already-accepted msquic connection handle + // and the server configuration handle. Not intended for direct use. + ClientQUIC(HQUIC connectionHandle, HQUIC serverConfiguration, std::string alpn); + + ~ClientQUIC(); + ClientQUIC(const ClientQUIC&) = delete; + ClientQUIC(ClientQUIC&&) noexcept; + + // Open a new bidirectional stream initiated by this side. + // Blocks until the stream is started; throws on failure. + QUICStream OpenStream(); + + // Send a datagram. Best-effort: may be silently dropped under loss + // or congestion. Size must fit within the path MTU (msquic surfaces + // the maximum via QUIC_PARAM_CONN_DATAGRAM_SEND_ENABLED — typically + // ~1200 bytes safely on the open internet). + void SendDatagram(const void* buffer, std::uint32_t size); + + // Register a handler for streams the peer initiates toward us. + // Called on the msquic worker; offload heavy work to ThreadPool. + void OnStream(std::function callback); + + // Register a handler for datagrams from the peer. Called on the + // msquic worker; copy/queue and return promptly. + void OnDatagram(std::function)> callback); + + // Block the caller until the next datagram arrives; returns it. + // Throws QUICClosedException if the connection closes first. + std::vector RecieveDatagramSync(); + + // Cleanly shut down the connection. + void Stop(); + + // Underlying handle for advanced use (parameter queries, etc.). + HQUIC GetHandle() const; + + private: + struct Impl; + std::unique_ptr impl; + friend class ListenerQUIC; + friend class QUICStream; + }; +} diff --git a/interfaces/Crafter.Network-ListenerQUIC.cppm b/interfaces/Crafter.Network-ListenerQUIC.cppm new file mode 100644 index 0000000..49b3bc1 --- /dev/null +++ b/interfaces/Crafter.Network-ListenerQUIC.cppm @@ -0,0 +1,75 @@ +/* +Crafter®.Network +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 as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +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 +*/ + +module; +#include +export module Crafter.Network:ListenerQUIC; +import std; +import :ClientQUIC; + +namespace Crafter { + // Server side of a QUIC connection. Mirrors ListenerTCP in shape: + // four Listen* methods covering the sync/async outer-loop x sync/async + // per-connection cross product. + // + // ALPN must match the client's ClientQUIC::alpn. The credentials describe + // the TLS certificate the server presents — for development, set + // selfSigned=true and the listener will generate an ephemeral cert + // (paired with insecureNoServerValidation on the client). + export class ListenerQUIC { + public: + bool running = true; + std::function connectCallback; + std::uint32_t concurrentClientLimit; + std::uint32_t totalClientLimit; + std::string alpn; + + ListenerQUIC(std::uint16_t port, + std::string alpn, + QUICServerCredentials creds, + std::function connectCallback, + std::uint32_t concurrentClientLimit = std::numeric_limits::max(), + std::uint32_t totalClientLimit = std::numeric_limits::max()); + + ~ListenerQUIC(); + ListenerQUIC(const ListenerQUIC&) = delete; + ListenerQUIC(ListenerQUIC&&) noexcept; + + void Stop(); + + // Block on this thread, dispatch each accepted connection on this + // thread. connectCallback runs sequentially. + void ListenSyncSync(); + // Block on this thread, dispatch each accepted connection on the + // ThreadPool. connectCallback runs concurrently. + void ListenSyncAsync(); + // Run the accept loop on the ThreadPool, dispatch each accepted + // connection on that same loop thread. + void ListenAsyncSync(); + // Run the accept loop on the ThreadPool, dispatch each accepted + // connection on the ThreadPool too. + void ListenAsyncAsync(); + + private: + struct Impl; + std::unique_ptr impl; + std::uint32_t totalClientCounter = 0; + }; +} diff --git a/interfaces/Crafter.Network.cppm b/interfaces/Crafter.Network.cppm index 92dec0b..1c6d696 100755 --- a/interfaces/Crafter.Network.cppm +++ b/interfaces/Crafter.Network.cppm @@ -24,4 +24,6 @@ export import :ClientTCP; export import :ListenerTCP; export import :ClientHTTP; export import :ListenerHTTP; -export import :HTTP; \ No newline at end of file +export import :HTTP; +export import :ClientQUIC; +export import :ListenerQUIC; \ No newline at end of file diff --git a/project.cpp b/project.cpp index e8fb41b..fd17dc6 100644 --- a/project.cpp +++ b/project.cpp @@ -4,19 +4,23 @@ namespace fs = std::filesystem; using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { - constexpr std::array networkInterfaces = { + constexpr std::array 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-ClientQUIC", + "interfaces/Crafter.Network-ListenerQUIC", }; - constexpr std::array networkImplementations = { + constexpr std::array networkImplementations = { "implementations/Crafter.Network-ClientTCP", "implementations/Crafter.Network-ListenerTCP", "implementations/Crafter.Network-ClientHTTP", "implementations/Crafter.Network-ListenerHTTP", + "implementations/Crafter.Network-ClientQUIC", + "implementations/Crafter.Network-ListenerQUIC", }; std::vector depArgs(args.begin(), args.end()); @@ -27,45 +31,41 @@ extern "C" Configuration CrafterBuildProject(std::span a Configuration cfg; cfg.path = "./"; - cfg.name = "Crafter.Network"; - cfg.outputName = "Crafter.Network"; + cfg.name = "crafter-network"; + cfg.outputName = "crafter-network"; cfg.type = ConfigurationType::LibraryStatic; ApplyStandardArgs(cfg, args); cfg.dependencies = { thread }; - { - std::array ifaces; - std::ranges::copy(networkInterfaces, ifaces.begin()); - std::array impls; - std::ranges::copy(networkImplementations, impls.begin()); - cfg.GetInterfacesAndImplementations(ifaces, impls); - } - auto addTest = [&](std::string name, fs::path implFile) { - Test t; - t.config.path = "./"; - t.config.name = std::move(name); - t.config.outputName = t.config.name; - t.config.target = cfg.target; - t.config.debug = cfg.debug; - t.config.march = cfg.march; - t.config.mtune = cfg.mtune; - t.config.type = ConfigurationType::Executable; - t.config.dependencies = { thread }; - std::array ifaces; - std::ranges::copy(networkInterfaces, ifaces.begin()); - std::array impls; - std::ranges::copy(networkImplementations, impls.begin()); - impls[4] = std::move(implFile); - t.config.GetInterfacesAndImplementations(ifaces, impls); - cfg.tests.push_back(std::move(t)); + // 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", }; - - addTest("ShouldCompile", "tests/ShouldCompile"); - addTest("ShouldRecieveHTTP", "tests/ShouldRecieveHTTP"); - addTest("ShouldSendHTTP", "tests/ShouldSendHTTP"); - addTest("ShouldSendRecieveHTTP", "tests/ShouldSendRecieveHTTP"); - addTest("ShouldSendRecieveKeepaliveHTTP", "tests/ShouldSendRecieveKeepaliveHTTP"); - addTest("ShouldSendRecieveLargeHTTP", "tests/ShouldSendRecieveLargeHTTP"); + msquic.includeDirs = { "src/inc" }; + // msquic's CMakeLists overrides CMAKE_LIBRARY_OUTPUT_DIRECTORY with + // QUIC_OUTPUT_DIR (defaults to bin/$), 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" }; + std::array ifaces; + std::ranges::copy(networkInterfaces, ifaces.begin()); + std::array impls; + std::ranges::copy(networkImplementations, impls.begin()); + cfg.GetInterfacesAndImplementations(ifaces, impls); return cfg; } diff --git a/tests/ShouldCompile.cpp b/tests/ShouldCompile.cpp deleted file mode 100644 index 3553b82..0000000 --- a/tests/ShouldCompile.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/* -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 -*/ -import Crafter.Network; -import std; -using namespace Crafter; - -int main() { - return 0; -} - - diff --git a/tests/ShouldRecieveHTTP.cpp b/tests/ShouldRecieveHTTP/ShouldRecieveHTTP.cpp similarity index 100% rename from tests/ShouldRecieveHTTP.cpp rename to tests/ShouldRecieveHTTP/ShouldRecieveHTTP.cpp diff --git a/tests/ShouldRecieveHTTP/project.cpp b/tests/ShouldRecieveHTTP/project.cpp new file mode 100644 index 0000000..c083256 --- /dev/null +++ b/tests/ShouldRecieveHTTP/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldRecieveHTTP/"; + cfg.name = "ShouldRecieveHTTP"; + cfg.outputName = "ShouldRecieveHTTP"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldRecieveHTTP" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendHTTP.cpp b/tests/ShouldSendHTTP/ShouldSendHTTP.cpp similarity index 100% rename from tests/ShouldSendHTTP.cpp rename to tests/ShouldSendHTTP/ShouldSendHTTP.cpp diff --git a/tests/ShouldSendHTTP/project.cpp b/tests/ShouldSendHTTP/project.cpp new file mode 100644 index 0000000..aa4b825 --- /dev/null +++ b/tests/ShouldSendHTTP/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendHTTP/"; + cfg.name = "ShouldSendHTTP"; + cfg.outputName = "ShouldSendHTTP"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendHTTP" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendRecieveHTTP.cpp b/tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp similarity index 100% rename from tests/ShouldSendRecieveHTTP.cpp rename to tests/ShouldSendRecieveHTTP/ShouldSendRecieveHTTP.cpp diff --git a/tests/ShouldSendRecieveHTTP/project.cpp b/tests/ShouldSendRecieveHTTP/project.cpp new file mode 100644 index 0000000..693ea3e --- /dev/null +++ b/tests/ShouldSendRecieveHTTP/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendRecieveHTTP/"; + cfg.name = "ShouldSendRecieveHTTP"; + cfg.outputName = "ShouldSendRecieveHTTP"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendRecieveHTTP" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendRecieveKeepaliveHTTP.cpp b/tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp similarity index 100% rename from tests/ShouldSendRecieveKeepaliveHTTP.cpp rename to tests/ShouldSendRecieveKeepaliveHTTP/ShouldSendRecieveKeepaliveHTTP.cpp diff --git a/tests/ShouldSendRecieveKeepaliveHTTP/project.cpp b/tests/ShouldSendRecieveKeepaliveHTTP/project.cpp new file mode 100644 index 0000000..5508a03 --- /dev/null +++ b/tests/ShouldSendRecieveKeepaliveHTTP/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendRecieveKeepaliveHTTP/"; + cfg.name = "ShouldSendRecieveKeepaliveHTTP"; + cfg.outputName = "ShouldSendRecieveKeepaliveHTTP"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendRecieveKeepaliveHTTP" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendRecieveLargeHTTP.cpp b/tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp similarity index 100% rename from tests/ShouldSendRecieveLargeHTTP.cpp rename to tests/ShouldSendRecieveLargeHTTP/ShouldSendRecieveLargeHTTP.cpp diff --git a/tests/ShouldSendRecieveLargeHTTP/project.cpp b/tests/ShouldSendRecieveLargeHTTP/project.cpp new file mode 100644 index 0000000..6850a71 --- /dev/null +++ b/tests/ShouldSendRecieveLargeHTTP/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendRecieveLargeHTTP/"; + cfg.name = "ShouldSendRecieveLargeHTTP"; + cfg.outputName = "ShouldSendRecieveLargeHTTP"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendRecieveLargeHTTP" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendRecieveQUICDatagram/ShouldSendRecieveQUICDatagram.cpp b/tests/ShouldSendRecieveQUICDatagram/ShouldSendRecieveQUICDatagram.cpp new file mode 100644 index 0000000..56760a7 --- /dev/null +++ b/tests/ShouldSendRecieveQUICDatagram/ShouldSendRecieveQUICDatagram.cpp @@ -0,0 +1,72 @@ +/* +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 +*/ +import Crafter.Network; +import Crafter.Thread; +import std; +using namespace Crafter; + +int main() { + ThreadPool::Start(); + + constexpr std::uint16_t port = 9182; + constexpr std::string_view alpn = "f3d/test-datagram"; + + std::promise serverHeard; + auto serverHeardFuture = serverHeard.get_future(); + + QUICServerCredentials serverCreds; + serverCreds.selfSigned = true; + + ListenerQUIC listener(port, std::string(alpn), serverCreds, [&](ClientQUIC* peer) { + peer->OnDatagram([&](std::vector dg){ + try { serverHeard.set_value(std::string(dg.begin(), dg.end())); } catch (...) {} + }); + }); + listener.ListenAsyncAsync(); + + try { + QUICClientCredentials clientCreds; + clientCreds.insecureNoServerValidation = true; + ClientQUIC client("localhost", port, std::string(alpn), clientCreds); + + // Give the server's connectCallback time to install OnDatagram + // before we send (avoid the early-arrival race where datagrams hit + // the empty default handler). + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + const char payload[] = "snapshot-42"; + client.SendDatagram(payload, sizeof(payload) - 1); + + if (serverHeardFuture.wait_for(std::chrono::seconds(5)) != std::future_status::ready) { + std::println("server never received the datagram"); + return 1; + } + std::string heard = serverHeardFuture.get(); + if (heard != "snapshot-42") { + std::println("server heard '{}'", heard); + return 1; + } + // See ShouldSendRecieveQUICStream.cpp — bypass static dtors to avoid + // a known msquic-runtime teardown SEGV. + std::_Exit(0); + } catch (std::exception& e) { + std::println("{}", e.what()); + return 1; + } +} diff --git a/tests/ShouldSendRecieveQUICDatagram/project.cpp b/tests/ShouldSendRecieveQUICDatagram/project.cpp new file mode 100644 index 0000000..4325610 --- /dev/null +++ b/tests/ShouldSendRecieveQUICDatagram/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendRecieveQUICDatagram/"; + cfg.name = "ShouldSendRecieveQUICDatagram"; + cfg.outputName = "ShouldSendRecieveQUICDatagram"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendRecieveQUICDatagram" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/ShouldSendRecieveQUICStream/ShouldSendRecieveQUICStream.cpp b/tests/ShouldSendRecieveQUICStream/ShouldSendRecieveQUICStream.cpp new file mode 100644 index 0000000..9035db6 --- /dev/null +++ b/tests/ShouldSendRecieveQUICStream/ShouldSendRecieveQUICStream.cpp @@ -0,0 +1,81 @@ +/* +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 +*/ +import Crafter.Network; +import Crafter.Thread; +import std; +using namespace Crafter; + +int main() { + ThreadPool::Start(); + + constexpr std::uint16_t port = 9181; + constexpr std::string_view alpn = "f3d/test-stream"; + + std::promise serverHeard; + auto serverHeardFuture = serverHeard.get_future(); + + QUICServerCredentials serverCreds; + serverCreds.selfSigned = true; + + ListenerQUIC listener(port, std::string(alpn), serverCreds, [&](ClientQUIC* peer) { + peer->OnStream([&](QUICStream stream){ + try { + std::vector got = stream.RecieveSync(); + stream.SendSync(got.data(), static_cast(got.size()), true); + serverHeard.set_value(std::string(got.begin(), got.end())); + } catch (...) { + serverHeard.set_value(""); + } + }); + }); + listener.ListenAsyncAsync(); + + try { + QUICClientCredentials clientCreds; + clientCreds.insecureNoServerValidation = true; + ClientQUIC client("localhost", port, std::string(alpn), clientCreds); + + QUICStream stream = client.OpenStream(); + const char hello[] = "ping"; + stream.SendSync(hello, sizeof(hello) - 1, false); + + std::vector echoed = stream.RecieveUntilCloseSync(); + std::string echoStr(echoed.begin(), echoed.end()); + + if (serverHeardFuture.wait_for(std::chrono::seconds(5)) != std::future_status::ready) { + std::println("server never heard from client"); + return 1; + } + std::string heard = serverHeardFuture.get(); + + if (heard != "ping" || echoStr != "ping") { + std::println("server heard '{}' echoed '{}'", heard, echoStr); + return 1; + } + // Skip the static-dtor cleanup — msquic's RegistrationClose blocks + // until every connection it ever opened is fully torn down. The + // listener-accepted server-side peer is heap-allocated and leaked + // by design (the user owns it), so it would otherwise wait for + // QUIC's idle timeout. The test logic succeeded; just exit. + std::_Exit(0); + } catch (std::exception& e) { + std::println("{}", e.what()); + return 1; + } +} diff --git a/tests/ShouldSendRecieveQUICStream/project.cpp b/tests/ShouldSendRecieveQUICStream/project.cpp new file mode 100644 index 0000000..0ca8fb2 --- /dev/null +++ b/tests/ShouldSendRecieveQUICStream/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/ShouldSendRecieveQUICStream/"; + cfg.name = "ShouldSendRecieveQUICStream"; + cfg.outputName = "ShouldSendRecieveQUICStream"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter-network") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "ShouldSendRecieveQUICStream" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +}