use std::io;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use tokio_rustls::client::TlsStream;
use crate::account::{self, Security};
use crate::config;
pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
#[derive(Debug)]
pub enum Conn {
Plain(TcpStream),
Tls(Box<TlsStream<TcpStream>>),
}
impl AsyncRead for Conn {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() {
Conn::Plain(s) => Pin::new(s).poll_read(cx, buf),
Conn::Tls(s) => Pin::new(s.as_mut()).poll_read(cx, buf),
}
}
}
impl AsyncWrite for Conn {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match self.get_mut() {
Conn::Plain(s) => Pin::new(s).poll_write(cx, buf),
Conn::Tls(s) => Pin::new(s.as_mut()).poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
Conn::Plain(s) => Pin::new(s).poll_flush(cx),
Conn::Tls(s) => Pin::new(s.as_mut()).poll_flush(cx),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
Conn::Plain(s) => Pin::new(s).poll_shutdown(cx),
Conn::Tls(s) => Pin::new(s.as_mut()).poll_shutdown(cx),
}
}
}
impl Conn {
pub fn into_plain(self) -> Result<TcpStream> {
match self {
Conn::Plain(s) => Ok(s),
Conn::Tls(_) => bail!("the connection is already encrypted"),
}
}
}
#[derive(Clone)]
pub struct Endpoint {
pub host: String,
pub port: u16,
pub security: Security,
pub tls: Arc<ClientConfig>,
}
impl Endpoint {
pub fn shown(&self) -> String {
format!("{}:{}", crate::text::one_line(&self.host, 80), self.port)
}
}
pub fn check_login(user: &str, secret: &str) -> Result<()> {
let bad = ['\r', '\n', '\0', '\x01'];
if user.contains(bad) || secret.contains(bad) {
bail!("the username or password has a line break or control character in it");
}
Ok(())
}
pub async fn connect(endpoint: &Endpoint) -> Result<Conn> {
if endpoint.security == Security::None && !account::is_local(&endpoint.host) {
bail!("refusing to connect to {} without TLS", endpoint.shown());
}
let host = endpoint.host.trim_matches(|c| c == '[' || c == ']');
let host = match endpoint.security {
Security::None if host.eq_ignore_ascii_case("localhost") => "127.0.0.1",
_ => host,
};
let tcp = tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect((host, endpoint.port)))
.await
.map_err(|_| anyhow!("{} didn't answer", endpoint.shown()))?
.with_context(|| format!("cannot connect to {}", endpoint.shown()))?;
let _ = tcp.set_nodelay(true);
if endpoint.security == Security::None {
let peer = tcp.peer_addr()?;
if !peer.ip().is_loopback() {
bail!(
"{} isn't on this computer: not sending it a password without TLS",
peer
);
}
listener_is_ours(&tcp)
.with_context(|| format!("not sending a password without TLS to {peer}"))?;
}
match endpoint.security {
Security::Tls => start_tls(tcp, endpoint).await,
Security::Starttls | Security::None => Ok(Conn::Plain(tcp)),
}
}
#[cfg(target_os = "linux")]
fn listener_is_ours(tcp: &TcpStream) -> Result<()> {
let (me, them) = (tcp.local_addr()?, tcp.peer_addr()?);
let table = if them.is_ipv4() {
"/proc/net/tcp"
} else {
"/proc/net/tcp6"
};
let text = std::fs::read_to_string(table).with_context(|| format!("cannot read {table}"))?;
for line in text.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
let [_, local, remote, _, _, _, _, uid, ..] = fields.as_slice() else {
continue;
};
if proc_addr(local) == Some(them) && proc_addr(remote) == Some(me) {
let uid: u32 = uid.parse().context("an unreadable socket owner")?;
let euid = unsafe { libc::geteuid() };
if uid == euid || uid == 0 {
return Ok(());
}
bail!("the server on {them} is another account's");
}
}
bail!("can't tell whose server answers on {them}")
}
#[cfg(not(target_os = "linux"))]
fn listener_is_ours(_tcp: &TcpStream) -> Result<()> {
Ok(())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn proc_addr(text: &str) -> Option<std::net::SocketAddr> {
let (ip, port) = text.split_once(':')?;
let port = u16::from_str_radix(port, 16).ok()?;
let mut bytes = Vec::with_capacity(16);
for word in ip.as_bytes().chunks(8) {
let word = u32::from_str_radix(std::str::from_utf8(word).ok()?, 16).ok()?;
bytes.extend(word.to_ne_bytes());
}
let ip: std::net::IpAddr = match bytes.len() {
4 => <[u8; 4]>::try_from(bytes).ok()?.into(),
16 => <[u8; 16]>::try_from(bytes).ok()?.into(),
_ => return None,
};
Some((ip, port).into())
}
pub async fn start_tls(tcp: TcpStream, endpoint: &Endpoint) -> Result<Conn> {
let host = endpoint.host.trim_matches(|c| c == '[' || c == ']');
let name = ServerName::try_from(host.to_string())
.map_err(|_| anyhow!("{} isn't a server name", endpoint.shown()))?;
let connector = TlsConnector::from(endpoint.tls.clone());
let tls = tokio::time::timeout(CONNECT_TIMEOUT, connector.connect(name, tcp))
.await
.map_err(|_| anyhow!("{} didn't finish the TLS handshake", endpoint.shown()))?
.map_err(|e| anyhow!("TLS with {} failed: {e}", endpoint.shown()))?;
Ok(Conn::Tls(Box::new(tls)))
}
pub fn tls_config(certificate: Option<&Path>) -> Result<Arc<ClientConfig>> {
let provider = Arc::new(rustls::crypto::ring::default_provider());
let extra = match certificate {
Some(path) => read_certificates(path)?,
None => Vec::new(),
};
let platform =
rustls_platform_verifier::Verifier::new_with_extra_roots(extra.clone(), provider.clone())
.map_err(|e| anyhow!("cannot use the system's certificates: {e}"))?;
let verifier: Arc<dyn ServerCertVerifier> = if extra.is_empty() {
Arc::new(platform)
} else {
Arc::new(Pinned {
pinned: extra,
inner: Arc::new(platform),
provider: provider.clone(),
})
};
let config = ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| anyhow!("TLS: {e}"))?
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_no_client_auth();
Ok(Arc::new(config))
}
fn read_certificates(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
let shown = config::shown(path);
let pem = config::read_capped(path, 1 << 20)
.with_context(|| format!("{shown} isn't a certificate file tuimail can read"))?;
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&pem)
.collect::<Result<_, _>>()
.map_err(|e| anyhow!("{shown} isn't a PEM certificate: {e}"))?;
if certs.is_empty() {
bail!("{shown} has no certificate in it");
}
Ok(certs)
}
#[derive(Debug)]
struct Pinned {
pinned: Vec<CertificateDer<'static>>,
inner: Arc<dyn ServerCertVerifier>,
provider: Arc<rustls::crypto::CryptoProvider>,
}
impl ServerCertVerifier for Pinned {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
if self
.pinned
.iter()
.any(|pin| pin.as_ref() == end_entity.as_ref())
{
return Ok(ServerCertVerified::assertion());
}
self.inner
.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&self.provider.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&self.provider.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.provider
.signature_verification_algorithms
.supported_schemes()
}
}
pub async fn read_line<R: tokio::io::AsyncBufRead + Unpin>(
reader: &mut R,
max: usize,
) -> io::Result<Vec<u8>> {
use tokio::io::AsyncBufReadExt;
let mut line = Vec::new();
loop {
let available = reader.fill_buf().await?;
if available.is_empty() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the server closed the connection",
));
}
let (used, done) = match available.iter().position(|&b| b == b'\n') {
Some(i) => (i + 1, true),
None => (available.len(), false),
};
if line.len() + used > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"the server sent a line too long",
));
}
line.extend_from_slice(&available[..used]);
reader.consume(used);
if done {
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
return Ok(line);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_login_has_nothing_that_could_end_its_command() {
assert!(check_login("me@example.org", "pass word").is_ok());
for (case, (user, secret)) in [
("me\r\nA1 DELETE INBOX", "x"),
("me", "x\0y"),
("me", "token\x01auth=Bearer other"),
("me\x01", "x"),
]
.into_iter()
.enumerate()
{
assert!(check_login(user, secret).is_err(), "case {case}");
}
}
#[tokio::test]
async fn lines_are_read_whole_and_capped() {
let data: &[u8] = b"+OK hello\r\nsecond\nthird";
let mut reader = tokio::io::BufReader::with_capacity(4, data);
assert_eq!(read_line(&mut reader, 100).await.unwrap(), b"+OK hello");
assert_eq!(read_line(&mut reader, 100).await.unwrap(), b"second");
assert!(read_line(&mut reader, 100).await.is_err(), "no line break");
let long: &[u8] = b"0123456789\r\n";
let mut reader = tokio::io::BufReader::new(long);
assert!(read_line(&mut reader, 5).await.is_err());
}
#[tokio::test]
async fn plain_connections_go_only_to_this_computer() {
let endpoint = Endpoint {
host: "imap.example.org".into(),
port: 143,
security: Security::None,
tls: tls_config(None).unwrap(),
};
let error = connect(&endpoint).await.unwrap_err().to_string();
assert!(error.contains("without TLS"), "{error}");
}
#[test]
fn socket_tables_are_read_as_linux_writes_them() {
let addr = |ip: &str| -> String {
let ip: std::net::IpAddr = ip.parse().unwrap();
let bytes = match ip {
std::net::IpAddr::V4(v4) => v4.octets().to_vec(),
std::net::IpAddr::V6(v6) => v6.octets().to_vec(),
};
bytes
.chunks(4)
.map(|w| format!("{:08X}", u32::from_ne_bytes([w[0], w[1], w[2], w[3]])))
.collect()
};
let v4 = format!("{}:1F90", addr("127.0.0.1"));
assert_eq!(proc_addr(&v4), Some("127.0.0.1:8080".parse().unwrap()));
let v6 = format!("{}:0477", addr("::1"));
assert_eq!(proc_addr(&v6), Some("[::1]:1143".parse().unwrap()));
assert_eq!(proc_addr("zz:1"), None);
}
#[test]
fn a_certificate_file_must_hold_a_certificate() {
let dir = std::env::temp_dir().join(format!("tuimail-cert-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("cert.pem");
std::fs::write(&file, "not a certificate").unwrap();
assert!(tls_config(Some(&file)).is_err());
assert!(tls_config(Some(&dir)).is_err(), "a folder");
std::fs::remove_dir_all(&dir).unwrap();
}
}