full QUIC support

This commit is contained in:
Jorijn van der Graaf 2026-05-07 00:06:44 +02:00
commit 28fab2509b
18 changed files with 1334 additions and 645 deletions

View file

@ -150,6 +150,8 @@ struct QUICStream::Impl {
}
};
QUICStream::QUICStream() = default;
QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection)
: handle(handle), connection(connection), impl(std::make_unique<Impl>())
{
@ -159,7 +161,9 @@ QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection)
}
QUICStream::QUICStream(QUICStream&& other) noexcept
: handle(other.handle), connection(other.connection), impl(std::move(other.impl))
: handle(other.handle), connection(other.connection),
canSend(other.canSend), canReceive(other.canReceive),
impl(std::move(other.impl))
{
other.handle = nullptr;
other.connection = nullptr;
@ -170,6 +174,8 @@ QUICStream& QUICStream::operator=(QUICStream&& other) noexcept {
Stop();
handle = other.handle;
connection = other.connection;
canSend = other.canSend;
canReceive = other.canReceive;
impl = std::move(other.impl);
other.handle = nullptr;
other.connection = nullptr;
@ -183,12 +189,26 @@ QUICStream::~QUICStream() {
void QUICStream::Stop() {
if (!handle) return;
Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
// If the stream's SHUTDOWN_COMPLETE event has already fired, msquic has
// internally called StreamClose for us (see Impl::Callback) and the
// handle is no longer valid — calling StreamShutdown on it trips a
// quic_bugcheck inside msquic. Skip in that case. This is the common
// path for short-lived request/response streams where both peers FIN
// before the wrapper is destroyed.
bool alreadyClosed = false;
if (impl) {
std::lock_guard lk(impl->mtx);
alreadyClosed = impl->shutdownComplete;
}
if (!alreadyClosed) {
Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
}
handle = nullptr;
if (impl) impl->handle = nullptr;
}
void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) {
if (!handle) throw QUICClosedException();
if (!handle || !canSend) throw QUICClosedException();
auto* copy = new char[size];
std::memcpy(copy, buffer, size);
QUIC_BUFFER quicBuf{};
@ -210,7 +230,7 @@ void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) {
}
std::vector<char> QUICStream::RecieveSync() {
if (!handle) throw QUICClosedException();
if (!handle || !canReceive) throw QUICClosedException();
std::unique_lock lk(impl->mtx);
impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; });
if (!impl->pending.empty()) {
@ -222,7 +242,7 @@ std::vector<char> QUICStream::RecieveSync() {
}
std::vector<char> QUICStream::RecieveUntilCloseSync() {
if (!handle) throw QUICClosedException();
if (!handle || !canReceive) throw QUICClosedException();
std::vector<char> out;
while (true) {
std::unique_lock lk(impl->mtx);
@ -237,7 +257,7 @@ std::vector<char> QUICStream::RecieveUntilCloseSync() {
}
std::vector<char> QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) {
if (!handle) throw QUICClosedException();
if (!handle || !canReceive) throw QUICClosedException();
std::vector<char> out;
out.reserve(bufferSize);
while (out.size() < bufferSize) {
@ -285,6 +305,12 @@ struct ClientQUIC::Impl {
std::function<void(QUICStream)> onStream;
std::function<void(std::vector<char>)> onDatagram;
std::deque<std::vector<char>> datagramQueue;
// Streams the peer started before the user installed an OnStream
// handler. Without this backlog the early streams (e.g. an h3 server's
// control stream right after handshake) would be aborted in the
// PEER_STREAM_STARTED branch and the connection would die with
// H3_MISSING_SETTINGS on the peer side.
std::deque<QUICStream> pendingStreams;
ClientQUIC* outer = nullptr;
@ -325,18 +351,30 @@ struct ClientQUIC::Impl {
}
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: {
HQUIC streamHandle = ev->PEER_STREAM_STARTED.Stream;
bool unidirectional = (ev->PEER_STREAM_STARTED.Flags
& QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) != 0;
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);
if (unidirectional) {
// Peer-initiated unidi: peer sends, we read; we cannot send.
stream.canSend = false;
stream.canReceive = true;
}
std::function<void(QUICStream)> cb;
{
std::lock_guard lk(self->mtx);
cb = self->onStream;
if (!cb) {
// Buffer until OnStream is installed; OnStream's
// setter drains this queue.
self->pendingStreams.push_back(std::move(stream));
return QUIC_STATUS_SUCCESS;
}
}
auto* shared = new QUICStream(std::move(stream));
ThreadPool::Enqueue([cb, shared]{
cb(std::move(*shared));
delete shared;
});
return QUIC_STATUS_SUCCESS;
}
case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: {
@ -382,6 +420,16 @@ static HQUIC OpenClientConfiguration(const std::string& alpn, const QUICClientCr
settings.IdleTimeoutMs = 30'000;
settings.IsSet.DatagramReceiveEnabled = 1;
settings.DatagramReceiveEnabled = 1;
// Allow the server to open unidi/bidi streams to us. msquic defaults
// both peer-stream-count limits to 0; with that, the server's HTTP/3
// control stream + QPACK encoder/decoder streams can't be created and
// most h3 servers will close the connection after handshake. We don't
// currently use server push (h3 pushes ride on unidi 0x01 streams) but
// the bidi cap is harmless to grant.
settings.IsSet.PeerUnidiStreamCount = 1;
settings.PeerUnidiStreamCount = 16;
settings.IsSet.PeerBidiStreamCount = 1;
settings.PeerBidiStreamCount = 16;
HQUIC cfg = nullptr;
QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1,
@ -470,18 +518,26 @@ void ClientQUIC::Stop() {
Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
}
QUICStream ClientQUIC::OpenStream() {
QUICStream ClientQUIC::OpenStream(bool unidirectional) {
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,
QUIC_STREAM_OPEN_FLAGS openFlags = unidirectional
? QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
: QUIC_STREAM_OPEN_FLAG_NONE;
QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, openFlags,
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;
if (unidirectional) {
// We initiated the unidi stream: we send, peer reads.
stream.canSend = true;
stream.canReceive = false;
}
s = Runtime().api->StreamStart(streamHandle, QUIC_STREAM_START_FLAG_NONE);
if (QUIC_FAILED(s)) {
Runtime().api->StreamClose(streamHandle);
@ -510,7 +566,21 @@ void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) {
}
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
impl->onStream = std::move(cb);
std::deque<QUICStream> backlog;
{
std::lock_guard lk(impl->mtx);
impl->onStream = cb;
std::swap(backlog, impl->pendingStreams);
}
while (!backlog.empty()) {
auto* shared = new QUICStream(std::move(backlog.front()));
backlog.pop_front();
auto handler = cb;
ThreadPool::Enqueue([handler, shared]{
handler(std::move(*shared));
delete shared;
});
}
}
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {