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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
//! 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)
|
||||
}
|
||||
Reference in New Issue
Block a user