One request to an http(s) URL with redirects followed by RFC 9110 §15.4: 301, 302, 303, 307 and 308 for every method; a 303 makes the request a GET without its body and content headers, a 301 or 302 does so for a POST alone, a 307 or 308 keeps the method and the body. The Location is resolved against the sending URL by RFC 3986, a hop between http and https is allowed, Authorization, Proxy-Authorization and Cookie are dropped on a change of origin, and each hop is a client of its own, since one ClientHTTP1 is one origin. A redirect without a usable Location, to another scheme, back to a URL already visited or past maxRedirects comes back as the response, with `stop` saying why. The new :URL partition is the URL type behind it, exported: Parse, Resolve (RFC 3986 §5.2; its §5.4 examples are the test), ToString, and the pieces a request is built from. This replaces the FetchHTTP1 free function, which followed redirects the way Python's urllib did (its repeat limit, its quoting of the Location, a POST turned back on a 307) for a port that no longer needs the fidelity. Breaking: FetchHTTP1, FetchOptionsHTTP1 and FetchResultHTTP1 are gone; use ClientHTTP1::Fetch, FetchOptions and FetchResult, whose `url` is a URL and whose `limited` became `stop`.
166 lines
7.7 KiB
C++
166 lines
7.7 KiB
C++
//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;
|
|
import :URL;
|
|
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
namespace Crafter {
|
|
// What ClientHTTP1::Fetch needs beyond the URL.
|
|
export struct FetchOptions {
|
|
std::string method = "GET";
|
|
// Lowercase names, as on HTTPRequest. Host comes from the URL.
|
|
std::unordered_map<std::string, std::string> 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;
|
|
// How many redirects are followed at most; the one after comes back
|
|
// unfollowed.
|
|
std::uint32_t maxRedirects = 10;
|
|
};
|
|
|
|
// Why a Fetch ended on a redirect it did not follow.
|
|
export enum class FetchStop {
|
|
// It did not: the chain ended on a final response.
|
|
None,
|
|
// The redirect carried no Location, or one that is no URL.
|
|
NoLocation,
|
|
// The Location names a scheme other than http or https.
|
|
OtherScheme,
|
|
// The Location is a URL the chain has already been sent to.
|
|
Loop,
|
|
// maxRedirects redirects had been followed already.
|
|
Limit,
|
|
};
|
|
|
|
export struct FetchResult {
|
|
// The last response of the chain: the final response, or the
|
|
// redirect that was not followed, with `stop` saying why.
|
|
HTTPResponse response;
|
|
// The URL that answered with `response`.
|
|
URL url;
|
|
// Redirects followed to get there.
|
|
std::uint32_t redirects = 0;
|
|
FetchStop stop = FetchStop::None;
|
|
};
|
|
|
|
// 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<void(HTTPResponse)> onSuccess,
|
|
std::function<void(std::string)> 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();
|
|
|
|
// One request to an http:// or https:// URL, following redirects by
|
|
// RFC 9110 §15.4: 301, 302, 303, 307 and 308, for every method. A
|
|
// 303 turns the request into a GET (a HEAD stays a HEAD) without its
|
|
// body and content headers; 301 and 302 do the same for a POST
|
|
// alone; 307 and 308 keep the method and the body. The Location is
|
|
// resolved against the URL that sent it (URL::Resolve), and a
|
|
// redirect may move between http:// and https://, `credentials`
|
|
// serving every https:// hop. Authorization, Proxy-Authorization and
|
|
// Cookie are not carried to another origin. Each hop is a client of
|
|
// its own, since one ClientHTTP1 is one origin: the redirect's body
|
|
// is read and its connection closed before the next request goes
|
|
// out. A redirect without a usable Location, to another scheme, to
|
|
// a URL already visited, or past maxRedirects comes back as the
|
|
// response with `stop` set. Proxies are not consulted.
|
|
//
|
|
// Throws std::invalid_argument for a URL that is not http(s) or that
|
|
// URL::Parse refuses, and whatever a request throws otherwise
|
|
// (SocketErrorException, SocketTimeoutException, TLSException,
|
|
// HTTP1::HTTP1ProtocolError, std::runtime_error).
|
|
static FetchResult Fetch(std::string_view url, const FetchOptions& options = {});
|
|
|
|
// 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> impl;
|
|
};
|
|
}
|
|
#endif
|