/* 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 module Crafter.Network:ClientHTTP_impl; import :ClientHTTP; import :ClientQUIC; import :HTTP; import :HTTP3; import Crafter.Thread; import std; using namespace Crafter; 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; 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. } }); controlStream = quic.OpenStream(/*unidirectional=*/true); auto prelude = HTTP3::BuildControlStreamPrelude(); controlStream.SendSync(prelude.data(), static_cast(prelude.size()), /*finish=*/false); } }; ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds) : host(host), port(port), impl(std::make_unique(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& bytes) { HTTPResponse response; bool sawHeaders = false; std::size_t pos = 0; const auto* p = reinterpret_cast(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"); } 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(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 { response.headers.emplace(std::move(name), std::move(value)); } } 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(p + pos), static_cast(frameLen)); } else { // Unknown frame types are reserved/extensions — RFC 9114 §9 // says skip them. } pos += static_cast(frameLen); } if (!sawHeaders) { throw HTTP3::HTTP3ProtocolError("response stream had no HEADERS frame"); } return response; } } void ClientHTTP::SendAsync(const HTTPRequest& request, std::function onSuccess, std::function 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"); } }); } HTTPResponse ClientHTTP::Send(const HTTPRequest& request) { QUICStream stream = impl->quic.OpenStream(); // Pseudo-headers MUST appear before regular fields (RFC 9114 §4.3). std::vector> 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(std::tolower(c)); }); fields.emplace_back(std::move(lower), value); } auto encoded = HTTP3::EncodeFieldSection(fields); std::vector wire; HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size()); if (!request.body.empty()) { HTTP3::WriteFrame(wire, HTTP3::kFrameData, reinterpret_cast(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(wire.size()), /*finish=*/true); auto raw = stream.RecieveUntilCloseSync(); return ParseResponseFrames(raw); }