//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include module Crafter.Network:ClientHTTP1_impl; import :ClientHTTP1; import :ClientTCP; import :HTTP; import :HTTP1; import :Stream; import :TLS; import Crafter.Thread; import std; using namespace Crafter; namespace { // Host header value. The port is elided when it is the scheme default, // which is what every other HTTP/1.1 client on the wire does and what // virtual-host matching on the far side tends to expect. std::string DefaultAuthority(const std::string& host, std::uint16_t port, bool secure) { if (port == (secure ? 443 : 80)) return host; return host + ":" + std::to_string(port); } } struct ClientHTTP1::Impl { std::string host; std::uint16_t port; // Null for plaintext; shared by every connection this client dials, so the // trust store is parsed once rather than per redial. std::shared_ptr tls; std::unique_ptr tcp; // Sits on top of `tcp` and must therefore be destroyed before it. std::unique_ptr stream; std::string protocol; void Connect(std::chrono::milliseconds handshakeTimeout) { tcp = std::make_unique(host, port); try { if (tls) { stream = TLSStream::Connect(tcp->socketid, tls, host, handshakeTimeout); } else { stream = std::make_unique(tcp->socketid); } } catch (...) { // A half-built connection must not be left pooled: the next Send() // would treat it as reusable and read from a socket with no // session on it. tcp.reset(); throw; } protocol = std::string(stream->Protocol()); } void Close() { stream.reset(); tcp.reset(); } // One request/response exchange on the current connection. `received` // is set as soon as any response byte arrives, which is what decides // whether a failure is safe to replay on a new connection. HTTPResponse Exchange(const std::string& wire, std::string_view method, const HTTP1::MessageLimits& limits, std::chrono::milliseconds timeout, bool& received) { stream->Write(wire.data(), wire.size(), timeout); HTTP1::MessageParser parser(HTTP1::MessageKind::Response, limits); parser.SetRequestMethod(method); std::vector chunk(16 * 1024); while (!parser.Complete()) { std::size_t read = 0; const StreamStatus status = stream->ReadSome(chunk.data(), chunk.size(), timeout, read); if (status == StreamStatus::TimedOut) { throw std::runtime_error("timed out waiting for the response"); } if (status == StreamStatus::Closed) { // Peer closed. Completes a response framed by close; // anything else throws out of Finish(). parser.Finish(); break; } received = true; parser.Feed(chunk.data(), read); } if (!parser.Complete()) { throw std::runtime_error("connection closed before a complete response arrived"); } HTTPResponse response = parser.TakeResponse(); if (!parser.KeepAlive()) Close(); return response; } }; ClientHTTP1::ClientHTTP1(const char* host, std::uint16_t port) : host(host), port(port), impl(std::make_unique(std::string(host), port)) {} ClientHTTP1::ClientHTTP1(std::string host, std::uint16_t port) : ClientHTTP1(host.c_str(), port) {} ClientHTTP1::ClientHTTP1(const char* host, std::uint16_t port, TLSClientCredentials credentials) : host(host), port(port), impl(std::make_unique(std::string(host), port)) { // Built here rather than on first Send() so bad credentials — an // unreadable certificate, a trust anchor that is not a certificate — // surface at construction, where the caller is still looking. impl->tls = TLSContext::Client(credentials); } ClientHTTP1::ClientHTTP1(std::string host, std::uint16_t port, TLSClientCredentials credentials) : ClientHTTP1(host.c_str(), port, std::move(credentials)) {} ClientHTTP1::ClientHTTP1(ClientHTTP1&&) noexcept = default; ClientHTTP1::~ClientHTTP1() = default; bool ClientHTTP1::Connected() const noexcept { return impl && impl->tcp != nullptr; } bool ClientHTTP1::Secure() const noexcept { return impl && impl->tls != nullptr; } std::string_view ClientHTTP1::Protocol() const noexcept { return impl ? std::string_view(impl->protocol) : std::string_view(); } void ClientHTTP1::Disconnect() { if (impl) impl->Close(); } HTTPResponse ClientHTTP1::Send(const HTTPRequest& request) { HTTPRequest prepared = request; if (prepared.authority.empty()) { prepared.authority = DefaultAuthority(host, port, Secure()); } const std::string wire = HTTP1::SerializeRequest(prepared); const std::string method = prepared.method.empty() ? std::string("GET") : prepared.method; // Two attempts at most, and the second one only for the keep-alive // race: the peer may have closed a pooled connection at the same moment // we wrote to it, which is indistinguishable from success until the // read fails. Nothing is replayed once a response byte has arrived. for (int attempt = 0;; ++attempt) { const bool reused = impl->tcp != nullptr; if (!reused) impl->Connect(handshakeTimeout); bool received = false; try { return impl->Exchange(wire, method, limits, timeout, received); } catch (const std::exception&) { impl->Close(); if (attempt == 0 && reused && !received) continue; throw; } } } void ClientHTTP1::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"); } }); }