2025-11-02 15:00:53 +01:00
|
|
|
/*
|
|
|
|
|
Crafter®.Network
|
2026-05-06 01:09:40 +02:00
|
|
|
Copyright (C) 2026 Catcrafts®
|
2025-11-02 15:00:53 +01:00
|
|
|
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;
|
2026-05-07 00:06:44 +02:00
|
|
|
#include <msquic.h>
|
2025-11-02 15:00:53 +01:00
|
|
|
module Crafter.Network:ClientHTTP_impl;
|
|
|
|
|
import :ClientHTTP;
|
2026-05-07 00:06:44 +02:00
|
|
|
import :ClientQUIC;
|
|
|
|
|
import :HTTP;
|
|
|
|
|
import :HTTP3;
|
2025-11-02 15:00:53 +01:00
|
|
|
import Crafter.Thread;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
2026-05-07 00:06:44 +02:00
|
|
|
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;
|
2025-11-02 15:00:53 +01:00
|
|
|
|
2026-05-07 00:06:44 +02:00
|
|
|
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.
|
2025-11-02 16:04:32 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
});
|
2025-11-02 16:04:32 +01:00
|
|
|
|
2026-05-07 00:06:44 +02:00
|
|
|
controlStream = quic.OpenStream(/*unidirectional=*/true);
|
|
|
|
|
auto prelude = HTTP3::BuildControlStreamPrelude();
|
|
|
|
|
controlStream.SendSync(prelude.data(),
|
|
|
|
|
static_cast<std::uint32_t>(prelude.size()),
|
|
|
|
|
/*finish=*/false);
|
2025-11-02 16:04:32 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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");
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
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.
|
2025-11-02 15:00:53 +01:00
|
|
|
} else {
|
2026-05-07 00:06:44 +02:00
|
|
|
response.headers.emplace(std::move(name), std::move(value));
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
sawHeaders = true;
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
// 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.
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
pos += static_cast<std::size_t>(frameLen);
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
if (!sawHeaders) {
|
|
|
|
|
throw HTTP3::HTTP3ProtocolError("response stream had no HEADERS frame");
|
|
|
|
|
}
|
|
|
|
|
return response;
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 00:06:44 +02:00
|
|
|
|
2026-05-19 02:53:50 +02:00
|
|
|
void ClientHTTP::SendAsync(const HTTPRequest& request,
|
|
|
|
|
std::function<void(HTTPResponse)> onSuccess,
|
|
|
|
|
std::function<void(std::string)> onError) {
|
|
|
|
|
HTTPRequest copy = request;
|
|
|
|
|
ThreadPool::Enqueue([this, copy = std::move(copy),
|
|
|
|
|
onSuccess = std::move(onSuccess),
|
|
|
|
|
onError = std::move(onError)]() mutable {
|
|
|
|
|
try {
|
|
|
|
|
HTTPResponse response = this->Send(copy);
|
|
|
|
|
if (onSuccess) onSuccess(std::move(response));
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
if (onError) onError(e.what());
|
|
|
|
|
} catch (...) {
|
|
|
|
|
if (onError) onError("unknown error");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 00:06:44 +02:00
|
|
|
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);
|
2025-11-02 15:00:53 +01:00
|
|
|
}
|