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
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
|
||||
Loading…
Reference in a new issue