use core::pin::Pin;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_rustls::rustls::pki_types::ServerName;
use tokio_rustls::rustls::{CertificateError, Error as RustlsError};
use super::{Target, tls_connector};
use crate::terminal_safe;
pub(crate) type ReplyStream = Pin<Box<dyn AsyncRead + Send>>;
pub(super) const UNKNOWN_ISSUER: &str = concat!(
"the certificate chain this endpoint presented does not reach a certificate authority shep trusts; ",
"shep trusts only public certificate authorities, and offers no way to add a private one or to skip verification; ",
"a leaf served without its intermediate is refused the same way, so check the chain the endpoint serves, ",
"use http:// for a private endpoint, or use a publicly trusted certificate (Let's Encrypt, or a Cloudflare-proxied hostname)"
);
pub(crate) async fn connect_and_send(
target: &Target,
request: &[u8],
) -> Result<ReplyStream, std::io::Error> {
connect_with(tls_connector(), target, request).await
}
pub(super) async fn connect_with(
connector: &tokio_rustls::TlsConnector,
target: &Target,
request: &[u8],
) -> Result<ReplyStream, std::io::Error> {
let tcp = TcpStream::connect((target.host.as_str(), target.port)).await?;
if target.https {
let domain = ServerName::try_from(target.host.clone()).map_err(std::io::Error::other)?;
let mut tls = connector
.connect(domain, tcp)
.await
.map_err(|source| peer_transport_error(&target.host, source))?;
send(&mut tls, request).await?;
Ok(Box::pin(tls))
} else {
let mut tcp = tcp;
send(&mut tcp, request).await?;
Ok(Box::pin(tcp))
}
}
async fn send<S: AsyncWrite + Unpin>(stream: &mut S, request: &[u8]) -> Result<(), std::io::Error> {
stream.write_all(request).await?;
stream.flush().await
}
fn peer_transport_error(host: &str, source: std::io::Error) -> std::io::Error {
let message = match source
.get_ref()
.and_then(|src| src.downcast_ref::<RustlsError>())
{
Some(RustlsError::InvalidCertificate(CertificateError::UnknownIssuer)) => {
UNKNOWN_ISSUER.to_owned()
}
Some(RustlsError::InvalidCertificate(
CertificateError::NotValidForName | CertificateError::NotValidForNameContext { .. },
)) => format!("the certificate for {host} is not valid for that name"),
_ => source.to_string(),
};
std::io::Error::new(source.kind(), terminal_safe::sanitise(&message).0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_handshake_error_the_peer_worded_cannot_drive_a_terminal() {
let source = std::io::Error::new(
std::io::ErrorKind::InvalidData,
"tls handshake eof \u{1b}[2J\u{7}",
);
let kept = peer_transport_error("127.0.0.1", source);
assert_eq!(kept.kind(), std::io::ErrorKind::InvalidData);
assert_eq!(kept.to_string(), "tls handshake eof [2J");
}
#[test]
fn a_rustls_unknown_issuer_becomes_the_hint() {
let source = std::io::Error::new(
std::io::ErrorKind::InvalidData,
RustlsError::InvalidCertificate(CertificateError::UnknownIssuer),
);
assert_eq!(
peer_transport_error("hooks.example.com", source).to_string(),
"the certificate chain this endpoint presented does not reach a certificate \
authority shep trusts; shep trusts only public certificate authorities, and offers \
no way to add a private one or to skip verification; a leaf served without its \
intermediate is refused the same way, so check the chain the endpoint serves, use \
http:// for a private endpoint, or use a publicly trusted certificate (Let's \
Encrypt, or a Cloudflare-proxied hostname)"
);
}
#[test]
fn a_reworded_name_refusal_cannot_carry_a_control_character() {
let source = std::io::Error::new(
std::io::ErrorKind::InvalidData,
RustlsError::InvalidCertificate(CertificateError::NotValidForNameContext {
expected: ServerName::try_from("example.com").unwrap().to_owned(),
presented: vec!["evil\u{1b}[2J.shep.test\u{7}".to_owned()],
}),
);
let kept = peer_transport_error("127.0.0.\u{1b}[2J1", source);
assert_eq!(
kept.to_string(),
"the certificate for 127.0.0.[2J1 is not valid for that name"
);
}
}