Crafter.Network/implementations/Crafter.Network-Stream.cpp
catbot 9c22cbe09e 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>
2026-07-28 20:13:30 +00:00

99 lines
3.7 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <poll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <cerrno>
module Crafter.Network:Stream_impl;
import :Stream;
import std;
using namespace Crafter;
void Crafter::SetNonBlocking(int descriptor) {
const int flags = fcntl(descriptor, F_GETFL, 0);
if (flags == -1 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) == -1) {
throw std::runtime_error(std::string("could not make the socket non-blocking: ")
+ std::strerror(errno));
}
}
bool Crafter::PollDescriptor(int descriptor, short events,
std::chrono::steady_clock::time_point deadline) {
for (;;) {
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now());
// A deadline already in the past still gets one non-blocking look, so
// a zero timeout means "is it ready right now" rather than "give up".
const int wait = left.count() > 0 ? static_cast<int>(left.count()) : 0;
pollfd descriptors{ .fd = descriptor, .events = events, .revents = 0 };
const int ready = poll(&descriptors, 1, wait);
if (ready < 0) {
if (errno == EINTR) continue;
throw std::runtime_error(std::string("poll failed: ") + std::strerror(errno));
}
if (ready == 0) return false;
return true;
}
}
PlainStream::PlainStream(int descriptor) : descriptor(descriptor) {
SetNonBlocking(descriptor);
}
StreamStatus PlainStream::ReadSome(char* buffer, std::size_t size,
std::chrono::milliseconds timeout,
std::size_t& read) {
read = 0;
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
const auto got = recv(descriptor, buffer, size, 0);
if (got > 0) {
read = static_cast<std::size_t>(got);
return StreamStatus::Data;
}
if (got == 0) return StreamStatus::Closed;
if (errno == EINTR) continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
if (!PollDescriptor(descriptor, POLLIN, deadline)) return StreamStatus::TimedOut;
continue;
}
// A reset is how a peer that stopped caring shows up; it is an end of
// connection rather than something worth a diagnostic.
if (errno == ECONNRESET) return StreamStatus::Closed;
throw std::runtime_error(std::string("recv failed: ") + std::strerror(errno));
}
}
void PlainStream::Write(const void* buffer, std::size_t size,
std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
const char* data = reinterpret_cast<const char*>(buffer);
std::size_t sent = 0;
while (sent < size) {
// MSG_NOSIGNAL: a peer that closed early must surface as EPIPE here,
// not as a SIGPIPE that takes the process down.
const auto wrote = send(descriptor, data + sent, size - sent, MSG_NOSIGNAL);
if (wrote > 0) {
sent += static_cast<std::size_t>(wrote);
continue;
}
if (wrote == 0) throw std::runtime_error("the peer closed the connection");
if (errno == EINTR) continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
if (!PollDescriptor(descriptor, POLLOUT, deadline)) {
throw std::runtime_error("timed out writing to the peer");
}
continue;
}
throw std::runtime_error(std::string("send failed: ") + std::strerror(errno));
}
}
void PlainStream::Shutdown() noexcept {
shutdown(descriptor, SHUT_WR);
}