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::config_preparation::client::{
    PreparedClientConfig, prepare_client_settings_from_cli, prepare_selected_client_config,
};
use crate::settings::validate_prepared_client_settings;
use crate::{
    ClientSettings, SettingsError, XdgPathError, default_client_identity_material_dir,
    default_client_public_cert_material_dir,
};

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ClientRuntimeArgs {
    pub server_address: Option<String>,
    pub backend_address: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClientSettingsResolutionDefaults {
    pub identity_directory: PathBuf,
    pub public_cert_directory: PathBuf,
}

impl ClientSettingsResolutionDefaults {
    pub fn from_xdg() -> Result<Self, XdgPathError> {
        Ok(Self {
            identity_directory: default_client_identity_material_dir()?,
            public_cert_directory: default_client_public_cert_material_dir()?,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SelectedClientConfig {
    Explicit(PathBuf),
    Discovered(PathBuf),
    None,
}

#[derive(Debug)]
pub enum ClientSettingsResolutionError {
    XdgPath(XdgPathError),
    Settings(SettingsError),
    Validation {
        path: Option<PathBuf>,
        messages: Vec<String>,
    },
}

impl ClientSettingsResolutionError {
    pub fn validation_messages(&self) -> Option<&[String]> {
        match self {
            Self::Settings(SettingsError::Validation { messages, .. }) => Some(messages),
            Self::Validation { messages, .. } => Some(messages),
            Self::XdgPath(_)
            | Self::Settings(SettingsError::Read { .. } | SettingsError::Parse { .. }) => None,
        }
    }

    pub fn selected_config_path(&self) -> Option<&Path> {
        match self {
            Self::Settings(SettingsError::Read { path, .. })
            | Self::Settings(SettingsError::Parse { path, .. })
            | Self::Settings(SettingsError::Validation { path, .. }) => Some(path.as_path()),
            Self::Validation {
                path: Some(path), ..
            } => Some(path.as_path()),
            Self::XdgPath(_) | Self::Validation { path: None, .. } => None,
        }
    }
}

impl fmt::Display for ClientSettingsResolutionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::XdgPath(error) => write!(formatter, "{error}"),
            Self::Settings(error) => write!(formatter, "{error}"),
            Self::Validation {
                path: Some(path),
                messages,
            } => {
                write!(formatter, "invalid client config in {}:", path.display())?;
                for message in messages {
                    write!(formatter, "\n- {message}")?;
                }
                Ok(())
            }
            Self::Validation {
                path: None,
                messages,
            } => {
                formatter.write_str("invalid client settings:")?;
                for message in messages {
                    write!(formatter, "\n- {message}")?;
                }
                Ok(())
            }
        }
    }
}

impl std::error::Error for ClientSettingsResolutionError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::XdgPath(error) => Some(error),
            Self::Settings(error) => Some(error),
            Self::Validation { .. } => None,
        }
    }
}

pub fn select_client_config(config: Option<PathBuf>) -> Result<SelectedClientConfig, XdgPathError> {
    crate::config_preparation::client::select_client_config(config)
}

pub fn resolve_client_settings_from_cli(
    config: Option<PathBuf>,
    runtime: ClientRuntimeArgs,
) -> Result<ClientSettings, ClientSettingsResolutionError> {
    let prepared = prepare_client_settings_from_cli(config, runtime)?;
    validate_resolved_client_settings(prepared)
}

pub fn resolve_selected_client_settings(
    selected_config: SelectedClientConfig,
    runtime: &ClientRuntimeArgs,
    defaults: &ClientSettingsResolutionDefaults,
) -> Result<ClientSettings, ClientSettingsResolutionError> {
    let default_identity_directory = || Ok(defaults.identity_directory.clone());
    let default_public_cert_directory = || Ok(defaults.public_cert_directory.clone());
    let prepared = prepare_selected_client_config(
        selected_config,
        runtime,
        &default_identity_directory,
        &default_public_cert_directory,
    )?;
    validate_resolved_client_settings(prepared)
}

fn validate_resolved_client_settings(
    prepared: PreparedClientConfig,
) -> Result<ClientSettings, ClientSettingsResolutionError> {
    let selected_path = prepared.selected_path.clone();
    let validation_path = selected_path.as_deref().unwrap_or_else(|| Path::new("."));
    validate_prepared_client_settings(validation_path, prepared).map_err(|error| {
        match (selected_path.as_deref(), error) {
            (
                None,
                SettingsError::Validation {
                    messages,
                    section: _,
                    path: _,
                },
            ) => ClientSettingsResolutionError::Validation {
                path: None,
                messages,
            },
            (_, error) => ClientSettingsResolutionError::Settings(error),
        }
    })
}