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>
95 lines
4.2 KiB
C++
95 lines
4.2 KiB
C++
//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
|