//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Network:ClientHTTP1; import std; import :HTTP; import :HTTP1; import :TLS; #ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { // HTTP/1.1 client over TCP, with or without TLS, for peers that cannot // speak HTTP/3. The request/response types are the ones the HTTP/3 client // uses, so swapping ClientHTTP for ClientHTTP1 is a one-line change at the // call site. // // The connection is persistent: the first Send() dials, and later calls // reuse the socket unless the peer asked for it to be closed // (`Connection: close`, or an HTTP/1.0 response without // `Connection: keep-alive`). A reused connection that turns out to have // been closed by the peer in the meantime — the unavoidable race in // HTTP/1.1 keep-alive — is redialled once and the request replayed; // a freshly dialled connection is never replayed on, so a genuinely // broken server surfaces as an exception rather than a retry loop. // // Thread-affinity matches ClientHTTP: one ClientHTTP1 serves one caller // at a time; distinct instances are independent. // // `http://` or `https://` is chosen by the constructor: pass // TLSClientCredentials and every byte goes through libssl (see :TLS), // leave them out and the transport is plaintext. TLS changes nothing // above the transport — the same keep-alive, replay and framing rules // apply, and `authority` still defaults to host:port with the scheme's // default port elided (443 under TLS, 80 without). export class ClientHTTP1 { public: std::string host; std::uint16_t port; ClientHTTP1(const char* host, std::uint16_t port); ClientHTTP1(std::string host, std::uint16_t port); // https://. The credentials verify the server's certificate chain and // its name against `host` by default; see TLSClientCredentials for // self-signed peers, private trust anchors and client certificates. // Throws TLSException if the certificate is rejected. ClientHTTP1(const char* host, std::uint16_t port, TLSClientCredentials credentials); ClientHTTP1(std::string host, std::uint16_t port, TLSClientCredentials credentials); ~ClientHTTP1(); ClientHTTP1(const ClientHTTP1&) = delete; ClientHTTP1(ClientHTTP1&&) noexcept; // Send a request and read the full response. `authority` defaults to // the host:port this client was constructed with; `scheme` is ignored // — HTTP/1.1 request targets are origin-form and the transport was // already decided by the constructor. HTTPResponse Send(const HTTPRequest& request); // Send a request and deliver the response (or the error text) via // callback, on Crafter.Thread's ThreadPool. void SendAsync(const HTTPRequest& request, std::function onSuccess, std::function onError); // Whether a pooled connection is currently open. Mostly useful for // tests asserting that keep-alive actually kept the socket. bool Connected() const noexcept; // Whether this client speaks https://. bool Secure() const noexcept; // ALPN protocol the last connection negotiated, empty for plaintext // or when the server offered no ALPN. For an https:// client with the // default credentials this is "http/1.1". std::string_view Protocol() const noexcept; // Drop the pooled connection; the next Send() dials again. void Disconnect(); // Limits applied to responses. Set before the first Send(). HTTP1::MessageLimits limits; // How long to wait for the next piece of a response before giving // up on a server that accepted the connection and then went quiet. std::chrono::milliseconds timeout{30000}; // How long the TLS handshake may take, on an https:// client. Separate // from `timeout` because it covers a multi-round-trip exchange before // any request has been written. std::chrono::milliseconds handshakeTimeout{15000}; // How long the TCP connect may take, for each address the host // resolves to (see ClientTCP). Without it a host that drops the SYNs // held Send() for the kernel's retries, about two minutes, whatever // `timeout` said. std::chrono::milliseconds connectTimeout{15000}; private: struct Impl; std::unique_ptr impl; }; // What FetchHTTP1 needs beyond the URL. export struct FetchOptionsHTTP1 { std::string method = "GET"; // Lowercase names, as on HTTPRequest. Host comes from the URL. std::unordered_map headers; std::string body; // As on ClientHTTP1, for every request of the chain. std::chrono::milliseconds connectTimeout{15000}; std::chrono::milliseconds handshakeTimeout{15000}; std::chrono::milliseconds timeout{30000}; HTTP1::MessageLimits limits; // For every https:// request of the chain. TLSClientCredentials credentials; // Python urllib's limits: a chain stops at a redirect to a URL it has // already been sent to `maxRepeats` times, or once it has been sent // to `maxRedirects` different URLs. std::uint32_t maxRedirects = 10; std::uint32_t maxRepeats = 4; }; export struct FetchResultHTTP1 { // The last response of the chain. A redirect only when it had nothing // to follow — no Location, a method urllib does not redirect, a // scheme other than http(s) — or when the limits stopped the chain. HTTPResponse response; // The URL that answered with `response`. std::string url; // Redirects followed to get there. std::uint32_t redirects = 0; // The limits stopped the chain; `response` is the redirect that would // have gone past them. bool limited = false; }; // One request to an http:// or https:// URL, following redirects the way // Python's urllib does, so a port of urllib code keeps its behaviour: // 301, 302, 303, 307 and 308 are followed for GET and HEAD, the first // three also for POST; the redirected request is a GET (a HEAD stays a // HEAD) without the body and its content headers. A relative Location is // resolved against the URL that sent it, as urljoin resolves it; a // redirect may switch between http:// and https://. Each redirect's body // is read before the next request goes out. Proxies are not consulted. // // The URL is `scheme://[user@]host[:port][/path][?query][#fragment]`, // with an IPv6 host in brackets; the fragment is not sent and the user // part is dropped. Throws std::invalid_argument for a URL that is not // http(s) or has no host or a bad port, and whatever a request throws // (SocketErrorException, SocketTimeoutException, TLSException, // std::runtime_error) otherwise. export FetchResultHTTP1 FetchHTTP1(std::string_view url, const FetchOptionsHTTP1& options = {}); } #endif