use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use rustls::ClientConfig;
use rustls::pki_types::pem::PemObject as _;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls_platform_verifier::Verifier;
use secrecy::{ExposeSecret, SecretSlice, SecretString};
use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _, ReadBuf};
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use url::Url;
use super::syslog::TLS_PORT;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RepositoryError {
#[error("the audit repository address is not tls://host[:port]")]
Address,
#[error("the audit repository address carries more than a host and a port")]
Extra,
#[error("the audit repository host is not a TLS server name")]
ServerName(#[source] rustls::pki_types::InvalidDnsNameError),
#[error("the audit repository trust roots are not PEM certificates")]
Roots(#[source] rustls::pki_types::pem::Error),
#[error("the audit repository client certificate and key are not PEM")]
Identity(#[source] rustls::pki_types::pem::Error),
#[error("the audit repository client identity holds no certificate")]
NoCertificate,
#[error("the TLS configuration for the audit repository could not be built")]
Tls(#[source] rustls::Error),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SendError {
#[error("the audit repository could not be reached")]
Connect(#[source] std::io::Error),
#[error("the TLS handshake with the audit repository failed")]
Handshake(#[source] std::io::Error),
#[error("the audit repository did not finish the {0} within its timeout")]
Timeout(Step),
#[error("the audit message could not be written to the audit repository")]
Write(#[source] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Step {
Connect,
Handshake,
Write,
Flush,
Close,
}
impl fmt::Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Connect => "connection",
Self::Handshake => "TLS handshake",
Self::Write => "write",
Self::Flush => "flush",
Self::Close => "close",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timeouts {
pub connect: Duration,
pub send: Duration,
}
#[derive(Clone)]
enum Transport {
Tls(TlsConnector, ServerName<'static>),
Plain,
}
#[derive(Clone)]
pub struct Repository {
host: String,
port: u16,
transport: Transport,
timeouts: Timeouts,
}
impl fmt::Debug for Repository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Repository")
.field("host", &self.host)
.field("port", &self.port)
.field("tls", &matches!(self.transport, Transport::Tls(..)))
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct TlsSettings {
pub roots: Option<Vec<u8>>,
pub identity: Option<SecretString>,
}
impl Repository {
pub fn tls(
address: &Url,
tls: &TlsSettings,
timeouts: Timeouts,
) -> Result<Self, RepositoryError> {
if address.scheme() != "tls" {
return Err(RepositoryError::Address);
}
let (host, port) = host_port(address, Some(TLS_PORT))?;
let name = ServerName::try_from(host.clone()).map_err(RepositoryError::ServerName)?;
let connector = TlsConnector::from(Arc::new(client_config(tls)?));
Ok(Self {
host,
port,
transport: Transport::Tls(connector, name),
timeouts,
})
}
pub fn unencrypted_for_development(
address: &Url,
timeouts: Timeouts,
) -> Result<Self, RepositoryError> {
if address.scheme() != "tcp" {
return Err(RepositoryError::Address);
}
let (host, port) = host_port(address, None)?;
Ok(Self {
host,
port,
transport: Transport::Plain,
timeouts,
})
}
pub async fn connect(&self) -> Result<Connection, SendError> {
let stream = bounded(
self.timeouts.connect,
Step::Connect,
TcpStream::connect((self.host.as_str(), self.port)),
)
.await?
.map_err(SendError::Connect)?;
stream.set_nodelay(true).map_err(SendError::Connect)?;
let stream: Box<dyn Stream> = match &self.transport {
Transport::Plain => Box::new(stream),
Transport::Tls(connector, name) => Box::new(
bounded(
self.timeouts.send,
Step::Handshake,
connector.connect(name.clone(), stream),
)
.await?
.map_err(SendError::Handshake)?,
),
};
Ok(Connection {
stream,
send_timeout: self.timeouts.send,
})
}
}
async fn bounded<T>(
limit: Duration,
step: Step,
future: impl Future<Output = T>,
) -> Result<T, SendError> {
tokio::time::timeout(limit, future)
.await
.map_err(|_elapsed| SendError::Timeout(step))
}
trait Stream: AsyncRead + AsyncWrite + Send + Sync + Unpin {}
impl<T: AsyncRead + AsyncWrite + Send + Sync + Unpin> Stream for T {}
pub struct Connection {
stream: Box<dyn Stream>,
send_timeout: Duration,
}
impl fmt::Debug for Connection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Connection").finish_non_exhaustive()
}
}
impl Connection {
pub async fn is_closed(&mut self) -> bool {
tokio::task::yield_now().await;
let mut probe = [0_u8; 1];
let mut buf = ReadBuf::new(&mut probe);
let mut cx = Context::from_waker(Waker::noop());
match Pin::new(&mut self.stream).poll_read(&mut cx, &mut buf) {
Poll::Ready(Ok(())) => buf.filled().is_empty(),
Poll::Ready(Err(_)) => true,
Poll::Pending => false,
}
}
pub async fn send(&mut self, frame: &SecretSlice<u8>) -> Result<(), SendError> {
let limit = self.send_timeout;
bounded(
limit,
Step::Write,
self.stream.write_all(frame.expose_secret()),
)
.await?
.map_err(SendError::Write)?;
bounded(limit, Step::Flush, self.stream.flush())
.await?
.map_err(SendError::Write)
}
pub async fn close(mut self) -> Result<(), SendError> {
let limit = self.send_timeout;
bounded(limit, Step::Close, self.stream.shutdown())
.await?
.map_err(SendError::Write)?;
let stream = &mut self.stream;
bounded(limit, Step::Close, async move {
let mut unread = [0_u8; 256];
loop {
match stream.read(&mut unread).await {
Ok(0) => return Ok(()),
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(());
}
Err(error) => return Err(error),
}
}
})
.await?
.map_err(SendError::Write)
}
}
fn host_port(address: &Url, default: Option<u16>) -> Result<(String, u16), RepositoryError> {
let clean = address.path().is_empty()
&& address.query().is_none()
&& address.fragment().is_none()
&& address.username().is_empty()
&& address.password().is_none();
if !clean {
return Err(RepositoryError::Extra);
}
let host = match address.host() {
Some(url::Host::Domain(name)) => name.to_owned(),
Some(url::Host::Ipv4(ip)) => ip.to_string(),
Some(url::Host::Ipv6(ip)) => ip.to_string(),
None => return Err(RepositoryError::Address),
};
let port = address.port().or(default).ok_or(RepositoryError::Address)?;
Ok((host, port))
}
fn client_config(tls: &TlsSettings) -> Result<ClientConfig, RepositoryError> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let roots = match &tls.roots {
Some(pem) => CertificateDer::pem_slice_iter(pem)
.collect::<Result<Vec<_>, _>>()
.map_err(RepositoryError::Roots)?,
None => Vec::new(),
};
let verifier = Verifier::new_with_extra_roots(roots, Arc::clone(&provider))
.map_err(RepositoryError::Tls)?;
let builder = ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(RepositoryError::Tls)?
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier));
match &tls.identity {
None => Ok(builder.with_no_client_auth()),
Some(pem) => {
let pem = pem.expose_secret().as_bytes();
let chain = CertificateDer::pem_slice_iter(pem)
.collect::<Result<Vec<_>, _>>()
.map_err(RepositoryError::Identity)?;
let key = PrivateKeyDer::from_pem_slice(pem).map_err(RepositoryError::Identity)?;
if chain.is_empty() {
return Err(RepositoryError::NoCertificate);
}
builder
.with_client_auth_cert(chain, key)
.map_err(RepositoryError::Tls)
}
}
}
#[cfg(test)]
mod tests {
use super::Connection;
const fn is_send_and_sync<T: Send + Sync>() {}
#[test]
fn a_connection_is_send_and_sync() {
const { is_send_and_sync::<Connection>() };
}
}