http-mtls-proxy 0.1.0

A fast minimalistic mTLS proxy.
Documentation
use anyhow::{anyhow, Result};
use log::{debug, trace};
use rustls::{ClientConfig, RootCertStore};
use rustls_pemfile::{certs as read_certs, read_one, Item};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use std::{fs::File, io::BufReader, sync::Arc};
use tokio::net::TcpStream;
use tokio_rustls::{client::TlsStream, TlsConnector};
use webpki_roots::TLS_SERVER_ROOTS;

fn load_certs(filename: &str) -> Result<Vec<CertificateDer<'static>>> {
	let certfile = File::open(filename)?;
	let mut reader = BufReader::new(certfile);
	let mut certs = Vec::new();
	for cert in read_certs(&mut reader) {
		certs.push(cert?);
	}
	debug!("Found {} {filename}", certs.len());
	Ok(certs)
}

fn load_key(filename: &str) -> Result<PrivateKeyDer<'static>> {
	let keyfile = File::open(filename)?;
	let mut reader = BufReader::new(keyfile);

	loop {
		return Ok(
			match read_one(&mut reader).map_err(|_| anyhow!("cannot parse private key file"))? {
				None => return Err(anyhow!("no keys found in {filename} file")),
				Some(tmp) => {
					debug!("Scanning for key in {filename}: {tmp:?}");
					match tmp {
						Item::Pkcs1Key(key) => key.into(),
						Item::Pkcs8Key(key) => key.into(),
						Item::Sec1Key(key) => key.into(),
						Item::X509Certificate(_) | Item::Crl(_) | _ => continue,
					}
				}
			},
		);
	}
}

pub fn create_config(certificate_filename: &str, key_filename: &str) -> Result<Arc<ClientConfig>> {
	Ok(Arc::new(
		ClientConfig::builder()
			.with_root_certificates({
				let mut root_cert_store = RootCertStore::empty();
				root_cert_store.extend(TLS_SERVER_ROOTS.iter().cloned());
				root_cert_store
			})
			.with_client_auth_cert(load_certs(certificate_filename)?, load_key(key_filename)?)?,
	))
}

pub async fn connect(
	config: Arc<ClientConfig>,
	host: impl Into<String>,
	sni: impl Into<String>,
) -> Result<TlsStream<TcpStream>> {
	let host = host.into();
	let sni = sni.into();
	trace!("Connecting to {host} with SNI {sni}");
	Ok(TlsConnector::from(config)
		.connect(ServerName::try_from(sni)?, TcpStream::connect(host).await?)
		.await?)
}