runewarp 0.1.0

Runewarp is an ingress tunneling tool for exposing local services without moving TLS termination to the edge. Clients connect out over QUIC, so you can publish services without putting your backend directly on the Internet or leaking your public IP.
Documentation
use std::fmt;
use std::path::{Path, PathBuf};

use crate::XdgPathError;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClientServerTrust {
    System,
    CaFile(PathBuf),
}

#[derive(Debug)]
pub enum ResolveClientServerTrustError {
    InvalidMode { value: String },
    UnexpectedServerCaFile,
    DefaultCaPath(XdgPathError),
}

impl fmt::Display for ResolveClientServerTrustError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidMode { value } => write!(
                formatter,
                "client.server-trust must be one of `system` or `ca-file`, got `{value}`"
            ),
            Self::UnexpectedServerCaFile => formatter.write_str(
                "client.server-ca-file may be set only when client.server-trust = \"ca-file\"",
            ),
            Self::DefaultCaPath(source) => write!(formatter, "{source}"),
        }
    }
}

impl std::error::Error for ResolveClientServerTrustError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::DefaultCaPath(source) => Some(source),
            Self::InvalidMode { .. } | Self::UnexpectedServerCaFile => None,
        }
    }
}

pub(crate) fn resolve_client_server_trust_with_default(
    trust_mode: Option<&str>,
    server_ca_file: Option<PathBuf>,
    config_dir: &Path,
    default_ca_path: impl FnOnce() -> Result<PathBuf, XdgPathError>,
) -> Result<ClientServerTrust, ResolveClientServerTrustError> {
    let trust_mode = match trust_mode.unwrap_or("system") {
        "system" => ClientServerTrustMode::System,
        "ca-file" => ClientServerTrustMode::CaFile,
        value => {
            return Err(ResolveClientServerTrustError::InvalidMode {
                value: value.to_owned(),
            });
        }
    };

    match trust_mode {
        ClientServerTrustMode::System => {
            if server_ca_file.is_some() {
                return Err(ResolveClientServerTrustError::UnexpectedServerCaFile);
            }
            Ok(ClientServerTrust::System)
        }
        ClientServerTrustMode::CaFile => Ok(ClientServerTrust::CaFile(match server_ca_file {
            Some(server_ca_file) => resolve_path(config_dir, &server_ca_file),
            None => default_ca_path().map_err(ResolveClientServerTrustError::DefaultCaPath)?,
        })),
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ClientServerTrustMode {
    System,
    CaFile,
}

fn resolve_path(config_dir: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        config_dir.join(path)
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::{
        ClientServerTrust, ResolveClientServerTrustError, resolve_client_server_trust_with_default,
    };

    #[test]
    fn defaults_to_system_trust() {
        let trust = resolve_client_server_trust_with_default(
            None,
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(trust, ClientServerTrust::System);
    }

    #[test]
    fn ca_file_trust_uses_the_default_ca_path_when_no_explicit_file_is_set() {
        let trust = resolve_client_server_trust_with_default(
            Some("ca-file"),
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/xdg-data/runewarp/client/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(
            trust,
            ClientServerTrust::CaFile(PathBuf::from("/xdg-data/runewarp/client/server-ca.crt"))
        );
    }

    #[test]
    fn ca_file_trust_resolves_relative_paths_from_the_config_directory() {
        let trust = resolve_client_server_trust_with_default(
            Some("ca-file"),
            Some(PathBuf::from("server-ca.pem")),
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(
            trust,
            ClientServerTrust::CaFile(PathBuf::from("/tmp/runewarp/server-ca.pem"))
        );
    }

    #[test]
    fn system_trust_rejects_server_ca_files() {
        let error = resolve_client_server_trust_with_default(
            Some("system"),
            Some(PathBuf::from("server-ca.pem")),
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap_err();

        assert!(matches!(
            error,
            ResolveClientServerTrustError::UnexpectedServerCaFile
        ));
    }

    #[test]
    fn rejects_unknown_trust_modes() {
        let error = resolve_client_server_trust_with_default(
            Some("hybrid"),
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap_err();

        assert!(matches!(
            error,
            ResolveClientServerTrustError::InvalidMode { value } if value == "hybrid"
        ));
    }
}