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
|
|
@ -5,13 +5,14 @@ export module Crafter.Network:ClientHTTP1;
|
|||
import std;
|
||||
import :HTTP;
|
||||
import :HTTP1;
|
||||
import :TLS;
|
||||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// HTTP/1.1 client over plain TCP, 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.
|
||||
// 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
|
||||
|
|
@ -25,9 +26,12 @@ namespace Crafter {
|
|||
// Thread-affinity matches ClientHTTP: one ClientHTTP1 serves one caller
|
||||
// at a time; distinct instances are independent.
|
||||
//
|
||||
// No TLS. This talks `http://`; for an encrypted transport use
|
||||
// ClientHTTP (HTTP/3 over QUIC), or put a TLS-terminating proxy in
|
||||
// front of the HTTP/1.1 endpoint.
|
||||
// `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;
|
||||
|
|
@ -36,13 +40,21 @@ namespace Crafter {
|
|||
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 (the transport is plaintext).
|
||||
// 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
|
||||
|
|
@ -55,6 +67,14 @@ namespace Crafter {
|
|||
// 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();
|
||||
|
||||
|
|
@ -63,6 +83,10 @@ namespace Crafter {
|
|||
// 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};
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
|
|
|
|||
|
|
@ -299,6 +299,14 @@ namespace Crafter::HTTP1 {
|
|||
headRequest = EqualsIgnoreCase(requestMethod, "HEAD");
|
||||
}
|
||||
|
||||
// Scheme reported for origin-form request targets, which carry none of
|
||||
// their own. Only the transport knows — "https" once TLS is
|
||||
// terminating the connection. An absolute-form target still wins, as
|
||||
// it names its own scheme. Survives Reset().
|
||||
void SetDefaultScheme(std::string scheme) {
|
||||
defaultScheme = std::move(scheme);
|
||||
}
|
||||
|
||||
void Feed(const char* data, std::size_t size) {
|
||||
if (size != 0) buffer.append(data, size);
|
||||
Advance();
|
||||
|
|
@ -340,7 +348,7 @@ namespace Crafter::HTTP1 {
|
|||
HTTPRequest request;
|
||||
request.method = std::move(method);
|
||||
request.path = std::move(path);
|
||||
request.scheme = scheme.empty() ? std::string("http") : std::move(scheme);
|
||||
request.scheme = scheme.empty() ? defaultScheme : std::move(scheme);
|
||||
// Absolute-form targets carry their own authority and win over
|
||||
// Host (RFC 9112 §3.2.2); otherwise Host supplies it. Either way
|
||||
// it lives in the named field, matching the HTTP/3 shape, so it
|
||||
|
|
@ -759,6 +767,10 @@ namespace Crafter::HTTP1 {
|
|||
std::string method;
|
||||
std::string path;
|
||||
std::string scheme;
|
||||
// Not cleared by Reset(): the transport does not change under a
|
||||
// connection, so it is told to the parser once and applies to every
|
||||
// request on it.
|
||||
std::string defaultScheme = "http";
|
||||
std::string authority;
|
||||
std::string version;
|
||||
std::string status;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ export module Crafter.Network:ListenerHTTP1;
|
|||
import std;
|
||||
import :HTTP;
|
||||
import :HTTP1;
|
||||
import :TLS;
|
||||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// HTTP/1.1 server over plain TCP. Same route map and `fallback` shape as
|
||||
// ListenerHTTP, so a handler can be registered with both and served over
|
||||
// either protocol.
|
||||
// HTTP/1.1 server over TCP, plaintext or TLS. Same route map and
|
||||
// `fallback` shape as ListenerHTTP, so a handler can be registered with
|
||||
// both and served over either protocol.
|
||||
//
|
||||
// Each accepted connection gets its own thread and is served
|
||||
// sequentially until the peer closes it, `Connection: close` is seen, or
|
||||
|
|
@ -18,13 +19,16 @@ namespace Crafter {
|
|||
// across connections but never for two requests on the same one, which
|
||||
// is what HTTP/1.1 response ordering requires. A dedicated thread rather
|
||||
// than a ThreadPool task is deliberate: keep-alive connections are idle
|
||||
// most of their life and would otherwise pin every pool thread.
|
||||
// most of their life and would otherwise pin every pool thread. The TLS
|
||||
// handshake runs on that same per-connection thread, so a peer that
|
||||
// stalls mid-handshake cannot hold up the accept loop either.
|
||||
//
|
||||
// Implemented: keep-alive and pipelining, content-length and chunked
|
||||
// request bodies, `Expect: 100-continue`, HEAD (headers only), automatic
|
||||
// `Date`, and 400/404/500 error responses. Not implemented: TLS,
|
||||
// CONNECT tunnels, Upgrade, and chunked *responses* (handlers return a
|
||||
// complete body, so responses are always content-length framed).
|
||||
// Implemented: TLS via libssl (pass TLSServerCredentials), keep-alive and
|
||||
// pipelining, content-length and chunked request bodies, `Expect:
|
||||
// 100-continue`, HEAD (headers only), automatic `Date`, and 400/404/500
|
||||
// error responses. Not implemented: CONNECT tunnels, Upgrade, and chunked
|
||||
// *responses* (handlers return a complete body, so responses are always
|
||||
// content-length framed).
|
||||
export class ListenerHTTP1 {
|
||||
public:
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
|
||||
|
|
@ -45,6 +49,9 @@ namespace Crafter {
|
|||
// against a peer holding a thread forever.
|
||||
std::chrono::milliseconds keepAliveTimeout{15000};
|
||||
std::chrono::milliseconds requestTimeout{30000};
|
||||
// How long a TLS handshake may take, on an https:// listener. A peer
|
||||
// that connects and then says nothing is dropped after this.
|
||||
std::chrono::milliseconds handshakeTimeout{15000};
|
||||
// Limits applied to incoming requests.
|
||||
HTTP1::MessageLimits limits;
|
||||
|
||||
|
|
@ -55,6 +62,21 @@ namespace Crafter {
|
|||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||
|
||||
// https://. Every accepted connection is wrapped in TLS before a byte
|
||||
// of HTTP is read; requests reach handlers with `scheme` set to
|
||||
// "https". Throws TLSException from the constructor if the credentials
|
||||
// do not yield a usable certificate, so a misconfigured server never
|
||||
// reaches the point of listening.
|
||||
ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
TLSServerCredentials credentials);
|
||||
|
||||
// TLS plus a fallback handler.
|
||||
ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback,
|
||||
TLSServerCredentials credentials);
|
||||
|
||||
~ListenerHTTP1();
|
||||
ListenerHTTP1(const ListenerHTTP1&) = delete;
|
||||
ListenerHTTP1(ListenerHTTP1&&) noexcept;
|
||||
|
|
@ -69,6 +91,13 @@ namespace Crafter {
|
|||
std::size_t ConnectionCount() const;
|
||||
// Connections accepted since construction.
|
||||
std::uint64_t AcceptedCount() const;
|
||||
// Connections that were accepted but never got as far as HTTP because
|
||||
// the TLS handshake failed — an untrusted client certificate, a peer
|
||||
// with no protocol in common, a port scanner. Always 0 on a plaintext
|
||||
// listener.
|
||||
std::uint64_t HandshakeFailureCount() const;
|
||||
// Whether this listener speaks https://.
|
||||
bool Secure() const noexcept;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
|
|
@ -92,6 +121,15 @@ namespace Crafter {
|
|||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||
|
||||
// TLS, with and without a fallback handler.
|
||||
ListenerAsyncHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
TLSServerCredentials credentials);
|
||||
ListenerAsyncHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback,
|
||||
TLSServerCredentials credentials);
|
||||
|
||||
~ListenerAsyncHTTP1();
|
||||
void Stop();
|
||||
};
|
||||
|
|
|
|||
95
interfaces/Crafter.Network-Stream.cppm
Normal file
95
interfaces/Crafter.Network-Stream.cppm
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Network:Stream;
|
||||
import std;
|
||||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// A reliable, ordered byte stream with deadlines on both directions.
|
||||
//
|
||||
// This exists so the HTTP/1.1 client and listener can be written once and
|
||||
// run over either a bare socket or a TLS session: `PlainStream` below is
|
||||
// the `http://` transport, `TLSStream` (in :TLS) the `https://` one. The
|
||||
// HTTP/1.1 code holds a `ByteStream&` and never learns which it has.
|
||||
//
|
||||
// Every method takes its own timeout rather than the stream carrying one,
|
||||
// because HTTP/1.1 uses different budgets for different states — a long
|
||||
// idle keep-alive wait, a shorter one once a request has started.
|
||||
export enum class StreamStatus {
|
||||
Data, // `read` bytes are available in the buffer
|
||||
Closed, // the peer closed its send side, cleanly
|
||||
TimedOut, // nothing arrived before the deadline
|
||||
};
|
||||
|
||||
export class ByteStream {
|
||||
public:
|
||||
virtual ~ByteStream() = default;
|
||||
ByteStream() = default;
|
||||
ByteStream(const ByteStream&) = delete;
|
||||
ByteStream& operator=(const ByteStream&) = delete;
|
||||
|
||||
// Read whatever is already available, waiting at most `timeout` for
|
||||
// the first byte. Sets `read` and returns Data, or reports a clean
|
||||
// close / a timeout. Throws on a transport error.
|
||||
virtual StreamStatus ReadSome(char* buffer, std::size_t size,
|
||||
std::chrono::milliseconds timeout,
|
||||
std::size_t& read) = 0;
|
||||
|
||||
// Write the whole buffer. Throws if it could not all be handed over
|
||||
// within `timeout`.
|
||||
virtual void Write(const void* buffer, std::size_t size,
|
||||
std::chrono::milliseconds timeout) = 0;
|
||||
|
||||
// Best-effort orderly close of our send side. Never throws — it runs
|
||||
// on teardown paths where there is nothing useful to do with a
|
||||
// failure.
|
||||
virtual void Shutdown() noexcept = 0;
|
||||
|
||||
// The underlying descriptor, so a listener can shutdown(2) it to wake
|
||||
// a thread parked in poll().
|
||||
virtual int Descriptor() const noexcept = 0;
|
||||
|
||||
// Negotiated ALPN protocol, empty when the transport has no notion of
|
||||
// one (plaintext) or nothing was agreed.
|
||||
virtual std::string_view Protocol() const noexcept { return {}; }
|
||||
|
||||
// Whether the bytes are encrypted on the wire.
|
||||
virtual bool Secure() const noexcept { return false; }
|
||||
};
|
||||
|
||||
// Plaintext TCP. Non-owning: the descriptor stays owned by the ClientTCP
|
||||
// (or whatever else) that opened it.
|
||||
//
|
||||
// The descriptor is switched to non-blocking on construction — both
|
||||
// directions are driven by poll() against a deadline, which a blocking
|
||||
// descriptor cannot express. That is also what lets a write time out
|
||||
// instead of parking forever against a peer that has stopped reading.
|
||||
export class PlainStream final : public ByteStream {
|
||||
public:
|
||||
explicit PlainStream(int descriptor);
|
||||
|
||||
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 { return descriptor; }
|
||||
|
||||
private:
|
||||
int descriptor;
|
||||
};
|
||||
|
||||
// Put a descriptor into non-blocking mode. Exposed because :TLS needs the
|
||||
// same thing for the descriptor it wraps.
|
||||
export void SetNonBlocking(int descriptor);
|
||||
|
||||
// Wait until `descriptor` is ready for `events` (a poll(2) event mask) or
|
||||
// `deadline` passes; false means the deadline won. Retries across EINTR
|
||||
// and throws on a real poll failure. Shared with :TLS, which has to poll
|
||||
// for whichever direction OpenSSL asks for next.
|
||||
export bool PollDescriptor(int descriptor, short events,
|
||||
std::chrono::steady_clock::time_point deadline);
|
||||
}
|
||||
#endif
|
||||
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
|
||||
|
|
@ -21,6 +21,12 @@ export import :HTTP3;
|
|||
// in the browser this job is already done by fetch() behind :ClientHTTP,
|
||||
// and these partitions use exceptions and POSIX sockets.
|
||||
export import :HTTP1;
|
||||
// The byte-stream abstraction the HTTP/1.1 endpoints run over, and the libssl
|
||||
// TLS transport that turns them into https://. Exported so callers can build
|
||||
// credentials, and so anything else holding a connected socket can wrap it the
|
||||
// same way.
|
||||
export import :Stream;
|
||||
export import :TLS;
|
||||
export import :ClientHTTP1;
|
||||
export import :ListenerHTTP1;
|
||||
#endif
|
||||
Loading…
Reference in a new issue