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>
169 lines
5.8 KiB
Rust
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)
|
|
}
|