//! 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>), } 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) -> std::io::Result<()> { // On `self.tcp()`. todo!() } } impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { // Forward to the inner stream for each variant. todo!() } } impl Write for Stream { fn write(&mut self, buf: &[u8]) -> std::io::Result { // 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>, } 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 { // 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 { 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 { 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::pem_file_iter(path) .map_err(|e| NetError::Roots(format!("{}: {e}", path.display())))? .collect::>() .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) }