porta-rs 0.1.1

Zero-trust CGNAT bypass via WireGuard tunnel
pub mod client;
pub mod schema;
pub mod server;
#[cfg(test)]
mod tests;

pub use client::ClientConfig;
pub use schema::{PortMapping, Protocol, Role};
pub use server::ServerConfig;

use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;

/// Configuration enum that can be either client or server
#[derive(Debug, Clone)]
pub enum Config {
    Client(ClientConfig),
    Server(ServerConfig),
}

impl Config {
    pub fn role(&self) -> Role {
        match self {
            Config::Client(c) => c.role,
            Config::Server(s) => s.role,
        }
    }

    pub fn interface(&self) -> &str {
        match self {
            Config::Client(c) => &c.local_interface,
            Config::Server(s) => &s.interface,
        }
    }

    pub fn ports(&self) -> &[PortMapping] {
        match self {
            Config::Client(c) => &c.ports,
            Config::Server(_) => &[],
        }
    }

    pub fn ports_mut(&mut self) -> &mut Vec<PortMapping> {
        match self {
            Config::Client(c) => &mut c.ports,
            Config::Server(_) => unreachable!("Server config has no ports"),
        }
    }

    pub fn public_key(&self) -> &str {
        match self {
            Config::Client(c) => &c.public_key,
            Config::Server(s) => &s.public_key,
        }
    }

    pub fn save(&self) -> Result<()> {
        match self {
            Config::Client(c) => c.save(),
            Config::Server(s) => s.save(),
        }
    }

    pub fn server_address(&self) -> &str {
        match self {
            Config::Client(c) => &c.server_address,
            Config::Server(_) => unreachable!("Server config has no server_address"),
        }
    }

    pub fn server_port(&self) -> u16 {
        match self {
            Config::Client(c) => c.server_port,
            Config::Server(_) => unreachable!("Server config has no server_port"),
        }
    }

    pub fn listen_port(&self) -> u16 {
        match self {
            Config::Client(_) => unreachable!("Client config has no listen_port"),
            Config::Server(s) => s.listen_port,
        }
    }

    pub fn allowed_clients_count(&self) -> usize {
        match self {
            Config::Client(_) => 0,
            Config::Server(s) => s.allowed_clients.len(),
        }
    }
}

/// Load the appropriate config based on what exists
pub fn load_config() -> Result<Config> {
    // Try client config first
    let client_path = client_config_path();
    if client_path.exists() {
        let content = fs::read_to_string(&client_path)
            .context(format!("Failed to read {}", client_path.display()))?;
        let config: ClientConfig = toml::from_str(&content)
            .context("Failed to parse client config")?;
        return Ok(Config::Client(config));
    }

    // Try server config
    let server_path = server_config_path();
    if server_path.exists() {
        let content = fs::read_to_string(&server_path)
            .context(format!("Failed to read {}", server_path.display()))?;
        let config: ServerConfig = toml::from_str(&content)
            .context("Failed to parse server config")?;
        return Ok(Config::Server(config));
    }

    Err(anyhow::anyhow!(
        "No configuration found. Run 'porta setup' first.\n\
         Searched:\n  {}\n  {}",
        client_path.display(),
        server_path.display()
    ))
}

pub fn client_config_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("porta")
        .join("client.toml")
}

pub fn server_config_path() -> PathBuf {
    PathBuf::from("/etc/porta/server.toml")
}

pub fn client_key_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("porta")
        .join("client.key")
}

pub fn server_key_path() -> PathBuf {
    PathBuf::from("/etc/porta/server.key")
}