prax 0.1.1

a web penetration proxy hosted in neovim
Documentation
use std::{fs::File, io::BufReader, path::Path, sync::Arc};

use rustls::{
    pki_types::{CertificateDer, PrivateKeyDer},
    ClientConfig, RootCertStore, ServerConfig,
};
use rustls_pemfile::Item;

use crate::cli::CertOpts;

#[derive(Clone)]
pub struct Tls {
    pub client: Arc<ClientConfig>,
    pub server: Arc<ServerConfig>,
}

#[derive(Debug, thiserror::Error)]
pub enum TlsLoadError {
    #[error("failed to load key: {0}")]
    Key(LoadError),
    #[error("failed to load cert: {0}")]
    Cert(LoadError),

    #[error("failed to construct context: {0}")]
    Tls(#[from] rustls::Error),
}

#[derive(Debug, thiserror::Error)]
pub enum LoadError {
    #[error("no cryptographical content")]
    NoContent,

    #[error("content mismatch")]
    ContentMismatch,

    #[error("io error {0}")]
    IO(#[from] std::io::Error),
}

impl Tls {
    pub fn load(opts: CertOpts) -> Result<Option<Self>, TlsLoadError> {
        let Some(key) = opts.key else { return Ok(None) };
        let Some(cert) = opts.cert else {
            return Ok(None);
        };

        let root_store = RootCertStore {
            roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
        };

        let key = load_key(&key).map_err(TlsLoadError::Key)?;
        let certs = load_certs(&cert).map_err(TlsLoadError::Cert)?;

        let client = ClientConfig::builder()
            .with_root_certificates(root_store)
            .with_no_client_auth();

        let server = ServerConfig::builder()
            .with_no_client_auth()
            .with_single_cert(certs, key)?;

        let client = Arc::new(client);
        let server = Arc::new(server);

        Ok(Some(Tls { client, server }))
    }
}

fn load_key(path: &Path) -> Result<PrivateKeyDer<'static>, LoadError> {
    let file = File::open(path)?;

    let mut buf = BufReader::new(file);

    let Some(item) = rustls_pemfile::read_one(&mut buf)? else {
        return Err(LoadError::NoContent);
    };

    match item {
        Item::Pkcs1Key(key) => Ok(PrivateKeyDer::Pkcs1(key)),
        Item::Pkcs8Key(key) => Ok(PrivateKeyDer::Pkcs8(key)),
        Item::Sec1Key(key) => Ok(PrivateKeyDer::Sec1(key)),
        _ => Err(LoadError::ContentMismatch),
    }
}

fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, LoadError> {
    let file = File::open(path)?;
    let mut certs = Vec::new();

    let mut buf = BufReader::new(file);

    for item in rustls_pemfile::read_all(&mut buf) {
        let item = item?;

        match item {
            Item::X509Certificate(cert) => certs.push(cert),
            _ => continue,
        }
    }

    Ok(certs)
}