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>
99 lines
3.7 KiB
C++
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);
|
|
}
|