feat(tls): add a libssl TLS transport and an https:// HTTP/1.1 stack
HTTP/1.1 was plaintext-only, which left `https://` to either an HTTP/3
listener or a terminating proxy in front. Neither helps the callers this
stack exists for — curl scripts, CI tooling, old proxies — so wrap the
transport in libssl instead.
Two new partitions:
:Stream a ByteStream with per-call deadlines on both directions, plus
the plaintext socket implementation. The HTTP/1.1 client and
listener now hold a ByteStream& and never learn which
transport they have, which is what lets one code path serve
both schemes.
:TLS TLSContext/TLSStream over OpenSSL 3, with credentials for both
roles: chain and hostname verification on by default, private
trust anchors, client certificates, mutual TLS, ALPN, and an
in-process self-signed certificate for development.
Both descriptors go non-blocking and every read and write is driven by
poll() against a deadline. That is required for TLS — a blocking
descriptor cannot express a handshake timeout — and it means a plaintext
write can now time out too, instead of parking forever against a peer
that stopped reading.
ClientHTTP1 and ListenerHTTP1 gain credential-taking constructors; the
existing ones still speak http://. The listener handshakes on the
connection's own thread, so a peer that stalls mid-handshake costs one
thread rather than the accept loop, and a failed handshake is counted
rather than logged — on a public port it is ordinary traffic.
MessageParser gains SetDefaultScheme so origin-form targets report the
scheme the transport actually used; handlers shared with ListenerHTTP now
see the same "https" they would over HTTP/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1bb116b2d
commit
9c22cbe09e
11 changed files with 1299 additions and 99 deletions
179
interfaces/Crafter.Network-TLS.cppm
Normal file
179
interfaces/Crafter.Network-TLS.cppm
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Network:TLS;
|
||||
import std;
|
||||
import :Stream;
|
||||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// TLS over a TCP socket, via libssl (OpenSSL 3). This is the transport
|
||||
// that turns `ClientHTTP1`/`ListenerHTTP1` into `https://` endpoints; it
|
||||
// is deliberately protocol-agnostic, so anything else that owns a
|
||||
// connected descriptor can wrap it the same way.
|
||||
//
|
||||
// No OpenSSL type appears below: the SSL_CTX and SSL live behind the Impl
|
||||
// pointers, so importing this partition does not drag <openssl/*.h> into
|
||||
// the consumer. TLS 1.2 is the floor, the platform's cipher defaults are
|
||||
// used unchanged, and renegotiation is left to OpenSSL's own policy.
|
||||
export class TLSException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// A certificate and its private key, PEM-encoded.
|
||||
export struct TLSCertificatePem {
|
||||
std::string certificate;
|
||||
std::string privateKey;
|
||||
};
|
||||
|
||||
// The certificate the server presents. Exactly one source is used, in
|
||||
// this order: certPath/keyPath, then certPem/keyPem, then selfSigned.
|
||||
//
|
||||
// selfSigned generates an ephemeral in-memory certificate (see
|
||||
// GetSelfSignedCertificatePem) — for development, tests and LAN use. A
|
||||
// client talking to it needs either insecureNoServerValidation or the
|
||||
// certificate itself as a trust anchor.
|
||||
export struct TLSServerCredentials {
|
||||
// PEM files on disk. certPath may hold a chain (leaf first).
|
||||
std::string certPath;
|
||||
std::string keyPath;
|
||||
// The same material inline, for callers that hold it in memory
|
||||
// already (a secret store, a test) and would rather not touch disk.
|
||||
std::string certPem;
|
||||
std::string keyPem;
|
||||
bool selfSigned = false;
|
||||
|
||||
// Mutual TLS. With requireClientCertificate set, a peer that presents
|
||||
// no certificate — or one that does not chain to clientCaPath — is
|
||||
// rejected during the handshake. clientCaPath is a PEM file or a
|
||||
// directory of them; when it is empty the system trust store is used.
|
||||
std::string clientCaPath;
|
||||
bool requireClientCertificate = false;
|
||||
|
||||
// Protocols we are willing to speak, in server preference order. A
|
||||
// client that offers ALPN and none of these is rejected with
|
||||
// no_application_protocol (RFC 7301 §3.2) rather than being let
|
||||
// through to speak something we cannot parse. A client that offers no
|
||||
// ALPN at all is accepted — plenty of tooling still does not send it.
|
||||
std::vector<std::string> alpnProtocols = { "http/1.1" };
|
||||
};
|
||||
|
||||
// How the client checks the server, and what it presents itself.
|
||||
//
|
||||
// The default verifies the chain against the system trust store *and* the
|
||||
// hostname, which is the only combination that is actually safe; a chain
|
||||
// check without a name check accepts any valid certificate for any name.
|
||||
export struct TLSClientCredentials {
|
||||
// Skip both checks. Development only — it accepts any certificate,
|
||||
// including an attacker's.
|
||||
bool insecureNoServerValidation = false;
|
||||
|
||||
// Extra trust anchor: a PEM file or a directory of them. Added to the
|
||||
// system store rather than replacing it. This is how you talk to a
|
||||
// self-signed listener without giving up verification — hand the
|
||||
// client the server's certificate.
|
||||
std::string caPath;
|
||||
// The same, inline.
|
||||
std::string caPem;
|
||||
|
||||
// Overrides the name used for SNI and hostname verification. Empty
|
||||
// means the host being connected to, which is what you want unless
|
||||
// you are dialling an address that differs from the certificate name
|
||||
// (a tunnel, a pinned IP).
|
||||
std::string serverName;
|
||||
|
||||
// Client certificate for mutual TLS. Ignored when the server does not
|
||||
// ask for one.
|
||||
std::string certPath;
|
||||
std::string keyPath;
|
||||
|
||||
// Protocols to offer, in client preference order. Empty sends no ALPN
|
||||
// extension at all.
|
||||
std::vector<std::string> alpnProtocols = { "http/1.1" };
|
||||
};
|
||||
|
||||
// A configured SSL_CTX. Shared by every connection it produces — one per
|
||||
// listener, one per client — because the expensive parts (parsing the
|
||||
// certificate, loading the trust store) are per-context, and OpenSSL 3
|
||||
// lets an SSL_CTX be used concurrently from many threads.
|
||||
//
|
||||
// Held by shared_ptr: a TLSStream keeps its context alive, so a listener
|
||||
// that goes away mid-connection does not pull the configuration out from
|
||||
// under a session still using it.
|
||||
export class TLSContext {
|
||||
public:
|
||||
static std::shared_ptr<TLSContext> Server(const TLSServerCredentials& credentials);
|
||||
static std::shared_ptr<TLSContext> Client(const TLSClientCredentials& credentials);
|
||||
|
||||
~TLSContext();
|
||||
TLSContext(const TLSContext&) = delete;
|
||||
TLSContext& operator=(const TLSContext&) = delete;
|
||||
|
||||
private:
|
||||
TLSContext();
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
friend class TLSStream;
|
||||
};
|
||||
|
||||
// A TLS session over an already-connected descriptor. Non-owning, like
|
||||
// PlainStream: the descriptor stays owned by its ClientTCP, and this only
|
||||
// adds the record layer on top.
|
||||
//
|
||||
// Both factories complete the handshake before returning, so a stream you
|
||||
// hold is a stream you can write to. They throw TLSException on
|
||||
// certificate rejection, on a protocol mismatch, and on a peer that stops
|
||||
// answering mid-handshake.
|
||||
export class TLSStream final : public ByteStream {
|
||||
public:
|
||||
// Client side. `hostName` drives SNI and hostname verification unless
|
||||
// the credentials overrode it with serverName; an IP literal sets no
|
||||
// SNI (RFC 6066 forbids it) and is checked against the certificate's
|
||||
// iPAddress SANs instead.
|
||||
static std::unique_ptr<TLSStream> Connect(int descriptor,
|
||||
std::shared_ptr<TLSContext> context,
|
||||
const std::string& hostName,
|
||||
std::chrono::milliseconds timeout);
|
||||
|
||||
// Server side, on a descriptor accept(2) just handed us.
|
||||
static std::unique_ptr<TLSStream> Accept(int descriptor,
|
||||
std::shared_ptr<TLSContext> context,
|
||||
std::chrono::milliseconds timeout);
|
||||
|
||||
~TLSStream() override;
|
||||
|
||||
StreamStatus ReadSome(char* buffer, std::size_t size,
|
||||
std::chrono::milliseconds timeout,
|
||||
std::size_t& read) override;
|
||||
void Write(const void* buffer, std::size_t size,
|
||||
std::chrono::milliseconds timeout) override;
|
||||
void Shutdown() noexcept override;
|
||||
int Descriptor() const noexcept override;
|
||||
std::string_view Protocol() const noexcept override;
|
||||
bool Secure() const noexcept override { return true; }
|
||||
|
||||
// The negotiated protocol version, e.g. "TLSv1.3". For logging.
|
||||
std::string Version() const;
|
||||
// One-line subject of the peer's certificate, empty when it presented
|
||||
// none. With requireClientCertificate a non-empty value is the
|
||||
// authenticated client identity.
|
||||
std::string PeerCertificateSubject() const;
|
||||
|
||||
private:
|
||||
TLSStream();
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
// The process-wide ephemeral self-signed certificate used by
|
||||
// TLSServerCredentials{selfSigned=true}, in PEM form. Generated on first
|
||||
// call and then cached, so every listener in a process presents the same
|
||||
// certificate and a client can be handed it as a trust anchor.
|
||||
//
|
||||
// ECDSA P-256, CN=localhost, SAN {DNS:localhost, IP:127.0.0.1, IP:::1},
|
||||
// valid for 10 days. Development and tests only — it is regenerated on
|
||||
// every process start and no peer has any reason to trust it.
|
||||
export const TLSCertificatePem& GetSelfSignedCertificatePem();
|
||||
}
|
||||
#endif
|
||||
Loading…
Reference in a new issue