use std::fmt;
use std::io::{self, Read, Write};
use std::net::{Shutdown, SocketAddr, TcpStream, ToSocketAddrs};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::sync::{Arc, mpsc};
use std::time::Duration;
use rustls::pki_types::{CertificateDer, ServerName, pem::PemObject};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
pub const CERT_FILE: &str = "SSL_CERT_FILE";
#[derive(Clone, Debug)]
pub enum Address {
Socket(PathBuf),
Network(Remote),
}
impl Address {
#[must_use]
pub fn named(&self) -> String {
match self {
Self::Socket(path) => path.display().to_string(),
Self::Network(remote) => remote.endpoint.clone(),
}
}
}
#[derive(Clone)]
pub struct Remote {
endpoint: String,
server_name: ServerName<'static>,
trust: Trust,
}
impl fmt::Debug for Remote {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Remote")
.field("endpoint", &self.endpoint)
.field("server_name", &self.server_name)
.finish_non_exhaustive()
}
}
impl Remote {
pub fn new(endpoint: &str, server_name: &str, trust: &Trust) -> Result<Self, String> {
if !endpoint_parses(endpoint) {
return Err(format!(
"endpoint {endpoint:?} is not <host>:<port> with a port from 1 to 65535"
));
}
Ok(Self {
endpoint: endpoint.to_owned(),
server_name: name(server_name)?,
trust: trust.clone(),
})
}
#[must_use]
pub fn endpoint(&self) -> &str {
&self.endpoint
}
}
pub fn name(text: &str) -> Result<ServerName<'static>, String> {
ServerName::try_from(text.to_owned())
.map_err(|_| format!("server_name {text:?} is not a DNS name or an IP address"))
}
#[must_use]
pub fn endpoint_parses(text: &str) -> bool {
let Some((host, port)) = text.rsplit_once(':') else {
return false;
};
let port_ok = port.parse::<u16>().is_ok_and(|port| port != 0);
let host_ok = host.strip_prefix('[').map_or_else(
|| {
!host.is_empty()
&& host.len() <= 253
&& host
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')
},
|inner| {
inner
.strip_suffix(']')
.is_some_and(|inner| inner.parse::<std::net::Ipv6Addr>().is_ok())
},
);
port_ok && host_ok
}
#[derive(Clone)]
pub struct Trust(Arc<ClientConfig>);
impl Trust {
pub fn system() -> Result<Self, String> {
let mut roots = mozilla();
if let Some(file) = std::env::var_os(CERT_FILE) {
add_file(&mut roots, Path::new(&file))?;
}
Self::from_roots(roots)
}
pub fn mozilla() -> Result<Self, String> {
Self::from_roots(mozilla())
}
#[cfg(test)]
pub(crate) fn only(file: &Path) -> Result<Self, String> {
let mut roots = RootCertStore::empty();
add_file(&mut roots, file)?;
Self::from_roots(roots)
}
#[must_use]
pub(crate) fn client_config(&self) -> Arc<ClientConfig> {
Arc::clone(&self.0)
}
fn from_roots(roots: RootCertStore) -> Result<Self, String> {
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.map(|builder| {
Self(Arc::new(
builder.with_root_certificates(roots).with_no_client_auth(),
))
})
.map_err(|error| format!("door_trust_roots_invalid: protocol versions: {error}"))
}
}
fn mozilla() -> RootCertStore {
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
roots
}
fn add_file(roots: &mut RootCertStore, file: &Path) -> Result<(), String> {
let invalid =
|why: &dyn fmt::Display| format!("door_trust_roots_invalid: {}: {why}", file.display());
let mut added = 0_usize;
for certificate in CertificateDer::pem_file_iter(file).map_err(|error| invalid(&error))? {
roots
.add(certificate.map_err(|error| invalid(&error))?)
.map_err(|error| invalid(&error))?;
added += 1;
}
if added == 0 {
return Err(invalid(&"holds no certificate"));
}
Ok(())
}
#[derive(Debug)]
pub enum Refused {
Unreached(String),
NotTls(String),
Untrusted(String),
TlsRefused(String),
}
impl Refused {
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::Unreached(_) => "door_store_unreached",
Self::NotTls(_) => "door_store_not_tls",
Self::Untrusted(_) => "door_store_tls_untrusted",
Self::TlsRefused(_) => "door_store_tls_refused",
}
}
#[must_use]
pub fn why(&self) -> &str {
match self {
Self::Unreached(why)
| Self::NotTls(why)
| Self::Untrusted(why)
| Self::TlsRefused(why) => why,
}
}
}
impl fmt::Display for Refused {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.code(), self.why())
}
}
pub enum Stream {
Unix(UnixStream),
Tls(Box<StreamOwned<ClientConnection, TcpStream>>),
}
impl Stream {
pub fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
match self {
Self::Unix(stream) => stream.set_read_timeout(timeout),
Self::Tls(stream) => stream.sock.set_read_timeout(timeout),
}
}
pub fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
match self {
Self::Unix(stream) => stream.set_write_timeout(timeout),
Self::Tls(stream) => stream.sock.set_write_timeout(timeout),
}
}
pub fn hangup(&self) -> io::Result<Hangup> {
Ok(match self {
Self::Unix(stream) => Hangup::Unix(stream.try_clone()?),
Self::Tls(stream) => Hangup::Tcp(stream.sock.try_clone()?),
})
}
}
impl Read for Stream {
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
match self {
Self::Unix(stream) => stream.read(bytes),
Self::Tls(stream) => match stream.read(bytes) {
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(0),
read => read,
},
}
}
}
impl Write for Stream {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
match self {
Self::Unix(stream) => stream.write(bytes),
Self::Tls(stream) => stream.write(bytes),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Unix(stream) => stream.flush(),
Self::Tls(stream) => stream.flush(),
}
}
}
pub enum Hangup {
Unix(UnixStream),
Tcp(TcpStream),
}
impl Hangup {
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match self {
Self::Unix(stream) => stream.shutdown(how),
Self::Tcp(stream) => stream.shutdown(how),
}
}
}
pub fn open(remote: &Remote, timeout: Duration) -> Result<Stream, Refused> {
let endpoint = remote.endpoint.clone();
let (sender, receiver) = mpsc::channel();
let thread_endpoint = endpoint.clone();
std::thread::Builder::new()
.name("haem-network-connect".to_owned())
.spawn(move || {
let _unwaited = sender.send(connect(&thread_endpoint, timeout));
})
.map_err(|error| Refused::Unreached(format!("the connect could not start: {error}")))?;
let mut tcp = match receiver.recv_timeout(timeout) {
Ok(Ok(tcp)) => tcp,
Ok(Err(error)) => {
return Err(Refused::Unreached(format!(
"could not connect to {endpoint}: {error}"
)));
}
Err(_) => {
return Err(Refused::Unreached(format!(
"could not connect to {endpoint} within {} ms",
timeout.as_millis()
)));
}
};
let unbounded = |error: io::Error| {
Refused::Unreached(format!(
"could not bound the connection to {endpoint}: {error}"
))
};
tcp.set_read_timeout(Some(timeout)).map_err(unbounded)?;
tcp.set_write_timeout(Some(timeout)).map_err(unbounded)?;
let mut connection =
ClientConnection::new(Arc::clone(&remote.trust.0), remote.server_name.clone()).map_err(
|error| Refused::TlsRefused(format!("{endpoint}: TLS could not start: {error}")),
)?;
while connection.is_handshaking() {
if let Err(error) = connection.complete_io(&mut tcp) {
return Err(handshake_failed(&endpoint, &error));
}
}
Ok(Stream::Tls(Box::new(StreamOwned::new(connection, tcp))))
}
fn connect(endpoint: &str, timeout: Duration) -> io::Result<TcpStream> {
let addresses: Vec<SocketAddr> = endpoint.to_socket_addrs()?.collect();
let mut last = io::Error::new(
io::ErrorKind::NotFound,
"the endpoint's name resolved to no address",
);
for address in addresses {
match TcpStream::connect_timeout(&address, timeout) {
Ok(tcp) => return Ok(tcp),
Err(error) => last = error,
}
}
Err(last)
}
fn handshake_failed(endpoint: &str, error: &io::Error) -> Refused {
let tls = error
.get_ref()
.and_then(|inner| inner.downcast_ref::<rustls::Error>());
match tls {
Some(rustls::Error::InvalidCertificate(why)) => Refused::Untrusted(format!(
"{endpoint}: the store's certificate is not trusted for its server_name: {why:?}"
)),
Some(rustls::Error::AlertReceived(alert)) => Refused::TlsRefused(format!(
"{endpoint}: the store refused the TLS handshake: {alert:?}"
)),
_ if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) =>
{
Refused::Unreached(format!(
"{endpoint}: no TLS handshake answer within the bound: {error}"
))
}
_ => Refused::NotTls(format!(
"{endpoint}: the endpoint did not complete a TLS handshake; a network store is reached only over TLS, never plain TCP: {error}"
)),
}
}