added QUIC
This commit is contained in:
parent
de2073422c
commit
45479a46ff
22 changed files with 1448 additions and 66 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
531
implementations/Crafter.Network-ClientQUIC.cpp
Normal file
531
implementations/Crafter.Network-ClientQUIC.cpp
Normal file
|
|
@ -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 <msquic.h>
|
||||
#include <cstring>
|
||||
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<unsigned>(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<unsigned>(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<std::uint8_t>& 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<std::uint32_t>(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<std::vector<char>> 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<Impl*>(ctx);
|
||||
switch (ev->Type) {
|
||||
case QUIC_STREAM_EVENT_RECEIVE: {
|
||||
std::vector<char> 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<std::size_t>(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<char*>(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>())
|
||||
{
|
||||
impl->handle = handle;
|
||||
impl->connection = connection;
|
||||
Runtime().api->SetCallbackHandler(handle, reinterpret_cast<void*>(&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<std::uint8_t*>(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<unsigned>(s)));
|
||||
}
|
||||
std::unique_lock lk(impl->mtx);
|
||||
impl->cv.wait(lk, [&]{ return !impl->sendInFlight || impl->shutdownComplete; });
|
||||
if (impl->shutdownComplete) throw QUICClosedException();
|
||||
}
|
||||
|
||||
std::vector<char> 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<char> QUICStream::RecieveUntilCloseSync() {
|
||||
if (!handle) throw QUICClosedException();
|
||||
std::vector<char> 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<char> QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) {
|
||||
if (!handle) throw QUICClosedException();
|
||||
std::vector<char> 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<std::size_t>(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<void(std::vector<char>)> cb) {
|
||||
ThreadPool::Enqueue([this, cb]{ cb(this->RecieveSync()); });
|
||||
}
|
||||
void QUICStream::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> cb) {
|
||||
ThreadPool::Enqueue([this, cb]{ cb(this->RecieveUntilCloseSync()); });
|
||||
}
|
||||
void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> 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<void(QUICStream)> onStream;
|
||||
std::function<void(std::vector<char>)> onDatagram;
|
||||
std::deque<std::vector<char>> datagramQueue;
|
||||
|
||||
ClientQUIC* outer = nullptr;
|
||||
|
||||
static QUIC_STATUS QUIC_API Callback(HQUIC conn, void* ctx, QUIC_CONNECTION_EVENT* ev) {
|
||||
auto* self = static_cast<Impl*>(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<char> 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<std::uint8_t> 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<unsigned>(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<unsigned>(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>())
|
||||
{
|
||||
impl->outer = this;
|
||||
impl->configuration = OpenClientConfiguration(alpn, creds);
|
||||
|
||||
QUIC_STATUS s = Runtime().api->ConnectionOpen(Runtime().registration,
|
||||
reinterpret_cast<QUIC_CONNECTION_CALLBACK_HANDLER>(&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<unsigned>(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<unsigned>(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<unsigned>(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>())
|
||||
{
|
||||
impl->outer = this;
|
||||
impl->connection = connectionHandle;
|
||||
impl->configuration = serverConfiguration;
|
||||
impl->ownsConfiguration = false;
|
||||
impl->connected = true;
|
||||
Runtime().api->SetCallbackHandler(connectionHandle,
|
||||
reinterpret_cast<void*>(&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<QUICStream::Impl>();
|
||||
stream.impl->connection = this;
|
||||
QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, QUIC_STREAM_OPEN_FLAG_NONE,
|
||||
reinterpret_cast<QUIC_STREAM_CALLBACK_HANDLER>(&QUICStream::Impl::Callback),
|
||||
stream.impl.get(), &streamHandle);
|
||||
if (QUIC_FAILED(s)) throw QUICException(std::format("StreamOpen failed: 0x{:x}", static_cast<unsigned>(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<unsigned>(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<std::uint8_t*>(::operator new(sizeof(QUIC_BUFFER) + size));
|
||||
auto* hdr = reinterpret_cast<QUIC_BUFFER*>(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<unsigned>(s)));
|
||||
}
|
||||
}
|
||||
|
||||
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
|
||||
impl->onStream = std::move(cb);
|
||||
}
|
||||
|
||||
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
|
||||
impl->onDatagram = std::move(cb);
|
||||
}
|
||||
|
||||
std::vector<char> 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; }
|
||||
331
implementations/Crafter.Network-ListenerQUIC.cpp
Normal file
331
implementations/Crafter.Network-ListenerQUIC.cpp
Normal file
|
|
@ -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 <msquic.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
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<std::uint8_t>& 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<std::uint32_t>(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<unsigned>(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<unsigned>(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<SelfSignedCert> 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<std::uint8_t> 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<HQUIC> 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<Impl*>(ctx);
|
||||
switch (ev->Type) {
|
||||
case QUIC_LISTENER_EVENT_NEW_CONNECTION: {
|
||||
HQUIC conn = ev->NEW_CONNECTION.Connection;
|
||||
Runtime().api->SetCallbackHandler(conn,
|
||||
reinterpret_cast<void*>(&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<std::uint8_t>& 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<unsigned>(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<unsigned>(s),
|
||||
creds.selfSigned, creds.certPath));
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
ListenerQUIC::ListenerQUIC(std::uint16_t port,
|
||||
std::string alpnIn,
|
||||
QUICServerCredentials creds,
|
||||
std::function<void(ClientQUIC*)> 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>())
|
||||
{
|
||||
impl->outer = this;
|
||||
impl->configuration = OpenServerConfiguration(alpn, creds, impl->alpnBuf);
|
||||
|
||||
QUIC_STATUS s = Runtime().api->ListenerOpen(Runtime().registration,
|
||||
reinterpret_cast<QUIC_LISTENER_CALLBACK_HANDLER>(&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<unsigned>(s)));
|
||||
}
|
||||
|
||||
QUIC_BUFFER alpnBuffer{};
|
||||
alpnBuffer.Length = static_cast<std::uint32_t>(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<unsigned>(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(); });
|
||||
}
|
||||
172
interfaces/Crafter.Network-ClientQUIC.cppm
Normal file
172
interfaces/Crafter.Network-ClientQUIC.cppm
Normal file
|
|
@ -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 <msquic.h>
|
||||
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<char> RecieveSync();
|
||||
|
||||
// Read until the peer closes the send-side, accumulating all chunks.
|
||||
std::vector<char> RecieveUntilCloseSync();
|
||||
|
||||
// Read exactly bufferSize bytes; throws if the peer closes early.
|
||||
std::vector<char> RecieveUntilFullSync(std::uint32_t bufferSize);
|
||||
|
||||
// Async variants: dispatched on Crafter.Thread's ThreadPool.
|
||||
void RecieveAsync(std::function<void(std::vector<char>)> recieveCallback);
|
||||
void RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback);
|
||||
void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback);
|
||||
|
||||
// Cleanly shut down the stream (both directions).
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> 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<void(QUICStream)> callback);
|
||||
|
||||
// Register a handler for datagrams from the peer. Called on the
|
||||
// msquic worker; copy/queue and return promptly.
|
||||
void OnDatagram(std::function<void(std::vector<char>)> callback);
|
||||
|
||||
// Block the caller until the next datagram arrives; returns it.
|
||||
// Throws QUICClosedException if the connection closes first.
|
||||
std::vector<char> 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> impl;
|
||||
friend class ListenerQUIC;
|
||||
friend class QUICStream;
|
||||
};
|
||||
}
|
||||
75
interfaces/Crafter.Network-ListenerQUIC.cppm
Normal file
75
interfaces/Crafter.Network-ListenerQUIC.cppm
Normal file
|
|
@ -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 <msquic.h>
|
||||
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<void(ClientQUIC*)> connectCallback;
|
||||
std::uint32_t concurrentClientLimit;
|
||||
std::uint32_t totalClientLimit;
|
||||
std::string alpn;
|
||||
|
||||
ListenerQUIC(std::uint16_t port,
|
||||
std::string alpn,
|
||||
QUICServerCredentials creds,
|
||||
std::function<void(ClientQUIC*)> connectCallback,
|
||||
std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(),
|
||||
std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::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> impl;
|
||||
std::uint32_t totalClientCounter = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -24,4 +24,6 @@ export import :ClientTCP;
|
|||
export import :ListenerTCP;
|
||||
export import :ClientHTTP;
|
||||
export import :ListenerHTTP;
|
||||
export import :HTTP;
|
||||
export import :HTTP;
|
||||
export import :ClientQUIC;
|
||||
export import :ListenerQUIC;
|
||||
72
project.cpp
72
project.cpp
|
|
@ -4,19 +4,23 @@ namespace fs = std::filesystem;
|
|||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
constexpr std::array<std::string_view, 6> networkInterfaces = {
|
||||
constexpr std::array<std::string_view, 8> 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<std::string_view, 4> networkImplementations = {
|
||||
constexpr std::array<std::string_view, 6> 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<std::string> depArgs(args.begin(), args.end());
|
||||
|
|
@ -27,45 +31,41 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> 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<fs::path, 6> ifaces;
|
||||
std::ranges::copy(networkInterfaces, ifaces.begin());
|
||||
std::array<fs::path, 4> 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<fs::path, 6> ifaces;
|
||||
std::ranges::copy(networkInterfaces, ifaces.begin());
|
||||
std::array<fs::path, 5> 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/$<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" };
|
||||
std::array<fs::path, 8> ifaces;
|
||||
std::ranges::copy(networkInterfaces, ifaces.begin());
|
||||
std::array<fs::path, 6> impls;
|
||||
std::ranges::copy(networkImplementations, impls.begin());
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
20
tests/ShouldRecieveHTTP/project.cpp
Normal file
20
tests/ShouldRecieveHTTP/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldRecieveHTTP" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
20
tests/ShouldSendHTTP/project.cpp
Normal file
20
tests/ShouldSendHTTP/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendHTTP" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
20
tests/ShouldSendRecieveHTTP/project.cpp
Normal file
20
tests/ShouldSendRecieveHTTP/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendRecieveHTTP" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
20
tests/ShouldSendRecieveKeepaliveHTTP/project.cpp
Normal file
20
tests/ShouldSendRecieveKeepaliveHTTP/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendRecieveKeepaliveHTTP" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
20
tests/ShouldSendRecieveLargeHTTP/project.cpp
Normal file
20
tests/ShouldSendRecieveLargeHTTP/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendRecieveLargeHTTP" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -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<std::string> serverHeard;
|
||||
auto serverHeardFuture = serverHeard.get_future();
|
||||
|
||||
QUICServerCredentials serverCreds;
|
||||
serverCreds.selfSigned = true;
|
||||
|
||||
ListenerQUIC listener(port, std::string(alpn), serverCreds, [&](ClientQUIC* peer) {
|
||||
peer->OnDatagram([&](std::vector<char> 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;
|
||||
}
|
||||
}
|
||||
20
tests/ShouldSendRecieveQUICDatagram/project.cpp
Normal file
20
tests/ShouldSendRecieveQUICDatagram/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendRecieveQUICDatagram" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -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<std::string> 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<char> got = stream.RecieveSync();
|
||||
stream.SendSync(got.data(), static_cast<std::uint32_t>(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<char> 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;
|
||||
}
|
||||
}
|
||||
20
tests/ShouldSendRecieveQUICStream/project.cpp
Normal file
20
tests/ShouldSendRecieveQUICStream/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "ShouldSendRecieveQUICStream" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue