#![allow(clippy::module_name_repetitions)]
use std::io::{self, BufRead as _};
use std::sync::Arc;
use thiserror::Error;
const REQUIRED_ALPN: &[u8] = b"http/1.1";
#[derive(Debug, Error)]
pub enum TlsError {
#[error("invalid server name: {0}")]
InvalidServerName(String),
#[error("rustls error: {0}")]
Rustls(#[from] rustls::Error),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("server negotiated unexpected ALPN: {0:?}")]
BadAlpn(Vec<u8>),
}
pub struct TlsAdapter {
conn: rustls::ClientConnection,
peer_closed_notify: bool,
}
impl std::fmt::Debug for TlsAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TlsAdapter")
.field("is_handshaking", &self.conn.is_handshaking())
.finish()
}
}
impl TlsAdapter {
pub fn new_client(server_name: &str) -> Result<Self, TlsError> {
let root_store = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
let mut config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
config.alpn_protocols = vec![REQUIRED_ALPN.to_vec()];
Self::new_client_with_config(server_name, Arc::new(config))
}
pub fn new_client_with_config(
server_name: &str,
config: Arc<rustls::ClientConfig>,
) -> Result<Self, TlsError> {
let name = rustls::pki_types::ServerName::try_from(server_name.to_owned())
.map_err(|_| TlsError::InvalidServerName(server_name.to_owned()))?;
let conn = rustls::ClientConnection::new(config, name)?;
Ok(Self {
conn,
peer_closed_notify: false,
})
}
#[must_use]
pub fn is_handshaking(&self) -> bool {
self.conn.is_handshaking()
}
pub fn verify_alpn(&self) -> Result<(), TlsError> {
match self.conn.alpn_protocol() {
None => Ok(()),
Some(p) if p == REQUIRED_ALPN => Ok(()),
Some(other) => Err(TlsError::BadAlpn(other.to_vec())),
}
}
#[must_use]
pub fn received_close_notify(&self) -> bool {
self.peer_closed_notify
}
pub fn send_close_notify(&mut self) {
self.conn.send_close_notify();
}
pub fn ingest_ciphertext<F>(
&mut self,
mut src: &[u8],
dst_ciphertext: &mut Vec<u8>,
mut on_plaintext: F,
) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
let mut processed = false;
while !src.is_empty() {
if !self.conn.wants_read() {
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
if !self.conn.wants_read() {
return Ok(());
}
}
let before = src.len();
let n = self.conn.read_tls(&mut src)?;
if n == 0 || src.len() == before {
break;
}
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
processed = true;
}
if !processed {
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
}
Ok(())
}
fn process_and_drain<F>(
&mut self,
dst_ciphertext: &mut Vec<u8>,
on_plaintext: &mut F,
) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
let io_state = self.conn.process_new_packets()?;
self.peer_closed_notify |= io_state.peer_has_closed();
self.drain_plaintext(on_plaintext)?;
self.drain_ciphertext(dst_ciphertext)?;
Ok(())
}
fn drain_plaintext<F>(&mut self, on_plaintext: &mut F) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
loop {
let mut reader = self.conn.reader();
match reader.fill_buf() {
Ok([]) => break,
Ok(chunk) => {
let n = chunk.len();
on_plaintext(chunk);
reader.consume(n);
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) => return Err(TlsError::Io(e)),
}
}
Ok(())
}
fn drain_ciphertext(&mut self, dst_ciphertext: &mut Vec<u8>) -> Result<(), TlsError> {
while self.conn.wants_write() {
let n = self.conn.write_tls(dst_ciphertext)?;
if n == 0 {
break;
}
}
Ok(())
}
pub fn egress_plaintext(
&mut self,
src: &[u8],
dst_ciphertext: &mut Vec<u8>,
) -> Result<(), TlsError> {
if !src.is_empty() {
std::io::Write::write_all(&mut self.conn.writer(), src)?;
}
self.drain_ciphertext(dst_ciphertext)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn construct_with_valid_name() {
let r = TlsAdapter::new_client("www.example.com");
assert!(r.is_ok());
}
#[test]
fn handshake_initially_pending() {
let t = TlsAdapter::new_client("www.example.com").unwrap();
assert!(t.is_handshaking());
}
#[test]
fn invalid_server_name_rejected() {
let r = TlsAdapter::new_client("");
assert!(matches!(r, Err(TlsError::InvalidServerName(_))));
}
}