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

@ -19,190 +19,150 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/uio.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <msquic.h>
module Crafter.Network:ClientHTTP_impl;
import :ClientHTTP;
import :ClientQUIC;
import :HTTP;
import :HTTP3;
import Crafter.Thread;
import std;
using namespace Crafter;
ClientHTTP::ClientHTTP(const char* host, std::uint16_t port): host(host), port(port), client(host, port) {
struct ClientHTTP::Impl {
ClientQUIC quic;
// Outgoing control stream — RFC 9114 §6.2.1: each peer MUST open a
// unidirectional control stream and send a SETTINGS frame as its first
// frame. Most real h3 servers (cloudflare, nghttp3, lsquic, …) close
// the connection with H3_MISSING_SETTINGS if we don't. The stream
// stays open for the lifetime of the connection; we never FIN it.
QUICStream controlStream;
}
ClientHTTP::ClientHTTP(std::string host, std::uint16_t port): ClientHTTP(host.c_str(), port) {
}
HTTPResponse ClientHTTP::Send(const char* request, std::uint32_t length) {
std::cout << "Send started" << std::endl;
client.Send(request, length);
std::cout << "Send Complete" << std::endl;
std::vector<char> buffer;
HTTPResponse response;
std::uint32_t i = 0;
std::uint32_t statusStart = 0;
while(true) {
try {
buffer = client.RecieveSync();
std::cout << "Recieved: " << buffer.size() << std::endl;
} catch(const SocketClosedException& e) {
std::cout << "Retry" << std::endl;
client.Stop();
client.Connect();
client.Send(request, length);
buffer = client.RecieveSync();
std::cout << "Recieved: " << buffer.size() << std::endl;
}
for(; i < buffer.size(); i++) {
if(buffer[i] == ' ') {
statusStart = i;
break;
Impl(const char* host, std::uint16_t port, QUICClientCredentials creds)
: quic(host, port, std::string(HTTP3::kAlpn), creds) {
// Drain any unidi streams the server opens to us (its control
// stream + optional QPACK encoder/decoder streams). We don't act
// on the contents — SETTINGS we accept by defaults, dynamic-table
// mutations we discard since we operate with no dynamic table.
// Any bidi stream from the server would be a server push, which
// we don't support — best-effort drain it as well.
quic.OnStream([](QUICStream stream) {
try {
while (true) (void)stream.RecieveSync();
} catch (...) {
// Stream / connection closed. Done.
}
}
for(; i < buffer.size(); i++) {
if(buffer[i] == '\r') {
response.status.assign(buffer.data()+statusStart+1, i-statusStart-1);
break;
}
}
i+=2;
while(i < buffer.size()) {
std::uint32_t headerStart = i;
std::string headerName;
for(; i < buffer.size(); i++) {
if(buffer[i] == ':') {
headerName.assign(buffer.data()+headerStart, i-headerStart);
std::transform(headerName.begin(), headerName.end(), headerName.begin(), [](unsigned char c){ return std::tolower(c); });
i+=2;
break;
}
}
headerStart = i;
std::string headerValue;
for(; i < buffer.size(); i++) {
if(buffer[i] == '\r' && buffer[i+1] == '\n') {
headerValue.assign(buffer.data()+headerStart, i-headerStart);
response.headers.insert({headerName, headerValue});
if(buffer[i+2] == '\r'){
goto headersComplete;
} else{
i+=2;
break;
}
}
}
}
});
i = 0;
controlStream = quic.OpenStream(/*unidirectional=*/true);
auto prelude = HTTP3::BuildControlStreamPrelude();
controlStream.SendSync(prelude.data(),
static_cast<std::uint32_t>(prelude.size()),
/*finish=*/false);
}
headersComplete:;
std::cout << "Header complete" << std::endl;
i+=4;
std::unordered_map<std::string, std::string>::iterator it = response.headers.find("content-length");
if(it != response.headers.end())
{
const int lenght = std::stoi(it->second);
std::cout << "Content lenght: " << lenght << std::endl;
response.body.resize(lenght, 0);
if(i < buffer.size()){
std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i);
}
const int remaining = lenght-(buffer.size()-i);
std::cout << "Remain: " << remaining << std::endl;
if(remaining > 0){
std::vector<char> bodyBuffer = client.RecieveUntilFullSync(remaining);
std::memcpy(&response.body[ buffer.size()-i], bodyBuffer.data(), bodyBuffer.size());
std::cout << "Recieved: " << bodyBuffer.size() << std::endl;
}
} else {
std::cout << "No Content Lenght" << std::endl;
std::unordered_map<std::string, std::string>::iterator it = response.headers.find("transfer-encoding");
if(it != response.headers.end() && it->second == "chunked") {
std::cout << "Chunked" << std::endl;
while(i < buffer.size()){
std::string lenght;
int lenghtStart = i;
for(; i < buffer.size(); i++) {
if(buffer[i] == '\r') {
lenght.assign(buffer.data()+lenghtStart, i-lenghtStart);
break;
}
}
i+=2;
int lenghtInt = stoi(lenght, 0, 8);
if(lenghtInt != 0){
int oldSize = response.body.size();
response.body.resize(oldSize+lenghtInt, 0);
if(buffer.size() < lenghtInt) {
std::memcpy(&response.body[oldSize], buffer.data()+i, buffer.size()-i);
std::vector<char> bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-buffer.size());
std::memcpy(&response.body[oldSize+(buffer.size()-i)], buffer.data(), buffer.size());
} else {
std::memcpy(&response.body[oldSize], buffer.data()+i, lenghtInt);
i+=lenghtInt;
}
} else{
goto bodyFinished;
}
};
ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds)
: host(host), port(port), impl(std::make_unique<Impl>(host, port, std::move(creds))) {}
ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds)
: ClientHTTP(host.c_str(), port, std::move(creds)) {}
ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default;
ClientHTTP::~ClientHTTP() = default;
namespace {
// Parse a sequence of HTTP/3 frames from `bytes`. Populates response from
// the first HEADERS frame and concatenates all DATA payloads. Trailing
// HEADERS frames (trailers) are decoded but discarded. Throws on
// malformed input.
HTTPResponse ParseResponseFrames(const std::vector<char>& bytes) {
HTTPResponse response;
bool sawHeaders = false;
std::size_t pos = 0;
const auto* p = reinterpret_cast<const std::uint8_t*>(bytes.data());
std::size_t avail = bytes.size();
while (pos < avail) {
std::uint64_t frameType = 0, frameLen = 0;
std::size_t cn = 0;
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) {
throw HTTP3::HTTP3ProtocolError("truncated frame type");
}
while(true) {
std::vector<char> bodyBuffer = client.RecieveSync();
int i2 = 0;
while(i2 < bodyBuffer.size()){
std::string lenght;
int lenghtStart = i2;
for(; i2 < bodyBuffer.size(); i2++) {
if(buffer[i2] == '\r') {
lenght.assign(bodyBuffer.data()+lenghtStart, i2-lenghtStart);
break;
}
}
i2+=2;
int lenghtInt = stoi(lenght, 0, 8);
if(lenghtInt != 0){
int oldSize = response.body.size();
response.body.resize(oldSize+lenghtInt, 0);
if(bodyBuffer.size() < lenghtInt) {
std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, bodyBuffer.size()-i2);
std::vector<char> bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-bodyBuffer.size());
std::memcpy(&response.body[oldSize+(bodyBuffer.size()-i2)], bodyBuffer2.data(), bodyBuffer2.size());
pos += cn;
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) {
throw HTTP3::HTTP3ProtocolError("truncated frame length");
}
pos += cn;
if (pos + frameLen > avail) {
throw HTTP3::HTTP3ProtocolError("frame length runs past buffer");
}
if (frameType == HTTP3::kFrameHeaders) {
auto fields = HTTP3::DecodeFieldSection(p + pos, static_cast<std::size_t>(frameLen));
if (!sawHeaders) {
for (auto& [name, value] : fields) {
if (name == ":status") {
response.status = std::move(value);
} else if (!name.empty() && name[0] == ':') {
// Unknown response pseudo-header — ignore.
} else {
std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, lenghtInt);
i2+=lenghtInt;
response.headers.emplace(std::move(name), std::move(value));
}
} else{
goto bodyFinished;
}
sawHeaders = true;
}
// Trailer HEADERS frames are skipped; the field section was
// already decoded above and the contents discarded.
} else if (frameType == HTTP3::kFrameData) {
response.body.append(reinterpret_cast<const char*>(p + pos),
static_cast<std::size_t>(frameLen));
} else {
// Unknown frame types are reserved/extensions — RFC 9114 §9
// says skip them.
}
bodyFinished:;
} else {
std::cout << "Recv until close" << std::endl;
std::vector<char> bodyBuffer = client.RecieveUntilCloseSync();
response.body.resize((buffer.size()-i)+(bodyBuffer.size()), 0);
if(i < buffer.size()){
std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i);
}
std::memcpy(&response.body[buffer.size()-i], bodyBuffer.data(), bodyBuffer.size());
std::cout << "Closed" << std::endl;
pos += static_cast<std::size_t>(frameLen);
}
if (!sawHeaders) {
throw HTTP3::HTTP3ProtocolError("response stream had no HEADERS frame");
}
return response;
}
std::cout << "Response recieved" << std::endl;
return response;
}
HTTPResponse ClientHTTP::Send(std::string request) {
return Send(request.c_str(), request.size());
HTTPResponse ClientHTTP::Send(const HTTPRequest& request) {
QUICStream stream = impl->quic.OpenStream();
// Pseudo-headers MUST appear before regular fields (RFC 9114 §4.3).
std::vector<std::pair<std::string, std::string>> fields;
fields.reserve(4 + request.headers.size());
fields.emplace_back(":method", request.method.empty() ? std::string("GET") : request.method);
fields.emplace_back(":scheme", request.scheme.empty() ? std::string("https") : request.scheme);
fields.emplace_back(":authority",
request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority);
fields.emplace_back(":path", request.path.empty() ? std::string("/") : request.path);
for (const auto& [name, value] : request.headers) {
// HTTP/3 forbids uppercase in field names — lowercase defensively.
std::string lower = name;
std::ranges::transform(lower, lower.begin(),
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
fields.emplace_back(std::move(lower), value);
}
auto encoded = HTTP3::EncodeFieldSection(fields);
std::vector<std::uint8_t> wire;
HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size());
if (!request.body.empty()) {
HTTP3::WriteFrame(wire, HTTP3::kFrameData,
reinterpret_cast<const std::uint8_t*>(request.body.data()),
request.body.size());
}
// Send the entire request and FIN our send-side. HTTP/3 servers need FIN
// to know the request is complete — there's no Content-Length signal.
stream.SendSync(wire.data(), static_cast<std::uint32_t>(wire.size()), /*finish=*/true);
auto raw = stream.RecieveUntilCloseSync();
return ParseResponseFrames(raw);
}