added QUIC

This commit is contained in:
Jorijn van der Graaf 2026-05-06 04:06:17 +02:00
commit 45479a46ff
22 changed files with 1448 additions and 66 deletions

View 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(&regConfig, &registration);
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; }

View 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(&regConfig, &registration);
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(); });
}