Files
boxmaker/docs/plans/M4a/files/crates/gatewayd/src/net.rs
T
kyleandClaude Opus 5.5 0339dc13b2 Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:05:44 -07:00

169 lines
5.8 KiB
Rust

//! A connection to the Mattermost server: TCP, or TCP with TLS through `rustls`, verified against
//! the host's trusted certificates plus an optional CA file (M4a spec, section 5). Verification
//! cannot be turned off.
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, ServerName};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
use crate::config::ServerUrl;
#[derive(Debug)]
pub enum NetError {
/// The CA file or the host's certificates could not be loaded.
Roots(String),
/// No address of the server accepted a connection.
Connect(String),
/// The TLS handshake failed: an unknown CA, a wrong name, an old protocol.
Tls(String),
}
impl std::fmt::Display for NetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NetError::Roots(why) => write!(f, "cannot load trusted certificates: {why}"),
NetError::Connect(why) => write!(f, "cannot connect: {why}"),
NetError::Tls(why) => write!(f, "TLS failed: {why}"),
}
}
}
impl std::error::Error for NetError {}
/// A connected stream, plain or TLS.
pub enum Stream {
Plain(TcpStream),
Tls(Box<StreamOwned<ClientConnection, TcpStream>>),
}
impl Stream {
/// The TCP socket underneath, for timeouts and shutdown.
pub fn tcp(&self) -> &TcpStream {
// The TcpStream: itself for Plain; `get_ref()` for Tls.
todo!()
}
pub fn set_read_timeout(&self, timeout: Option<Duration>) -> std::io::Result<()> {
// On `self.tcp()`.
todo!()
}
}
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Forward to the inner stream for each variant.
todo!()
}
}
impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
// Forward to the inner stream for each variant.
todo!()
}
fn flush(&mut self) -> std::io::Result<()> {
// Forward to the inner stream for each variant.
todo!()
}
}
/// Makes connections to one server.
#[derive(Clone)]
pub struct Connector {
server: ServerUrl,
tls: Option<Arc<ClientConfig>>,
}
impl Connector {
/// For `https`, loads the host's trusted certificates and `ca_file`; an error in either is an
/// error here, before any connection.
pub fn new(server: ServerUrl, ca_file: Option<&Path>) -> Result<Connector, NetError> {
// For tls, `client_config(ca_file)?` in an Arc; for plain, None.
todo!()
}
pub fn server(&self) -> &ServerUrl {
// The ServerUrl.
todo!()
}
/// Connect, and for TLS complete the handshake, within `timeout` for each step.
pub fn connect(&self, timeout: Duration) -> Result<Stream, NetError> {
let addrs = (self.server.host.as_str(), self.server.port)
.to_socket_addrs()
.map_err(|e| NetError::Connect(format!("{}: {e}", self.server.host)))?;
let mut last = format!("{} has no address", self.server.host);
let mut tcp = None;
for addr in addrs {
match TcpStream::connect_timeout(&addr, timeout) {
Ok(s) => {
tcp = Some(s);
break;
}
Err(e) => last = format!("{addr}: {e}"),
}
}
let tcp = tcp.ok_or(NetError::Connect(last))?;
tcp.set_read_timeout(Some(timeout))
.map_err(|e| NetError::Connect(e.to_string()))?;
tcp.set_write_timeout(Some(timeout))
.map_err(|e| NetError::Connect(e.to_string()))?;
let _ = tcp.set_nodelay(true);
let Some(config) = &self.tls else {
return Ok(Stream::Plain(tcp));
};
let name = ServerName::try_from(self.server.host.clone())
.map_err(|e| NetError::Tls(e.to_string()))?;
let conn = ClientConnection::new(Arc::clone(config), name)
.map_err(|e| NetError::Tls(e.to_string()))?;
let mut stream = StreamOwned::new(conn, tcp);
while stream.conn.is_handshaking() {
stream
.conn
.complete_io(&mut stream.sock)
.map_err(|e| NetError::Tls(e.to_string()))?;
}
Ok(Stream::Tls(Box::new(stream)))
}
}
fn client_config(ca_file: Option<&Path>) -> Result<ClientConfig, NetError> {
let mut roots = RootCertStore::empty();
let native = rustls_native_certs::load_native_certs();
let (added, _ignored) = roots.add_parsable_certificates(native.certs);
if let Some(path) = ca_file {
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
.map_err(|e| NetError::Roots(format!("{}: {e}", path.display())))?
.collect::<Result<_, _>>()
.map_err(|e| NetError::Roots(format!("{}: {e}", path.display())))?;
if certs.is_empty() {
return Err(NetError::Roots(format!(
"{} holds no certificate",
path.display()
)));
}
for cert in certs {
roots
.add(cert)
.map_err(|e| NetError::Roots(format!("{}: {e}", path.display())))?;
}
} else if added == 0 {
return Err(NetError::Roots(
"the host has no trusted certificates and no ca_file is set".to_string(),
));
}
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| NetError::Roots(e.to_string()))?
.with_root_certificates(roots)
.with_no_client_auth();
Ok(config)
}