kftray-portforward 0.27.30

KFtray library with port forwarding logic
Documentation
use std::sync::LazyLock;

use kftray_commons::models::hostfile::HostEntry;
use log::{
    debug,
    warn,
};

use crate::hostfile_direct::DirectHostfileManager;
use crate::hostfile_helper::HostfileHelperClient;

static HOSTFILE_MANAGER: LazyLock<HostfileManager> = LazyLock::new(HostfileManager::new);

pub struct HostfileManager {
    helper_client: Option<HostfileHelperClient>,
    direct_manager: DirectHostfileManager,
}

impl HostfileManager {
    pub fn new() -> Self {
        let helper_client = HostfileHelperClient::new().ok();
        Self {
            helper_client,
            direct_manager: DirectHostfileManager::new(),
        }
    }

    pub fn add_host_entry(&self, id: String, entry: HostEntry) -> std::io::Result<()> {
        if let Some(helper) = &self.helper_client
            && helper.is_available()
        {
            match helper.add_host_entry(id.clone(), entry.clone()) {
                Ok(_) => {
                    debug!("Successfully added host entry via helper for ID: {id}");
                    return Ok(());
                }
                Err(e) => {
                    warn!("Helper hostfile add failed: {e}, falling back to direct");
                }
            }
        }

        self.direct_manager.add_host_entry(id, entry)
    }

    pub fn remove_host_entry(&self, id: &str) -> std::io::Result<()> {
        if let Some(helper) = &self.helper_client
            && helper.is_available()
        {
            match helper.remove_host_entry(id) {
                Ok(_) => {
                    debug!("Successfully removed host entry via helper for ID: {id}");
                    return Ok(());
                }
                Err(e) => {
                    warn!("Helper hostfile remove failed: {e}, falling back to direct");
                }
            }
        }

        self.direct_manager.remove_host_entry(id)
    }

    pub fn remove_all_host_entries(&self) -> std::io::Result<()> {
        if let Some(helper) = &self.helper_client
            && helper.is_available()
        {
            match helper.remove_all_host_entries() {
                Ok(_) => {
                    debug!("Successfully removed all host entries via helper");
                    return Ok(());
                }
                Err(e) => {
                    warn!("Helper hostfile remove_all failed: {e}, falling back to direct");
                }
            }
        }

        self.direct_manager.remove_all_host_entries()
    }
}

impl Default for HostfileManager {
    fn default() -> Self {
        Self::new()
    }
}

pub fn add_host_entry(id: String, entry: HostEntry) -> std::io::Result<()> {
    HOSTFILE_MANAGER.add_host_entry(id, entry)
}

pub fn remove_host_entry(id: &str) -> std::io::Result<()> {
    HOSTFILE_MANAGER.remove_host_entry(id)
}

pub fn remove_all_host_entries() -> std::io::Result<()> {
    HOSTFILE_MANAGER.remove_all_host_entries()
}

pub fn add_ssl_host_entry(config_id: &str, alias: &str, _https_port: u16) -> std::io::Result<()> {
    let https_entry = HostEntry {
        ip: "127.0.0.1".parse().unwrap(),
        hostname: alias.to_string(),
    };
    add_host_entry(format!("{}-https", config_id), https_entry)?;

    let local_entry = HostEntry {
        ip: "127.0.0.1".parse().unwrap(),
        hostname: format!("{}.local", alias),
    };
    add_host_entry(format!("{}-https-local", config_id), local_entry)?;

    Ok(())
}

pub fn remove_ssl_host_entry(config_id: &str) -> std::io::Result<()> {
    let _ = remove_host_entry(&format!("{}-https", config_id));

    let _ = remove_host_entry(&format!("{}-https-local", config_id));

    Ok(())
}

pub fn update_hosts_with_ssl_from_config(
    config: &kftray_commons::models::config_model::Config,
) -> Result<(), String> {
    let alias = config
        .alias
        .as_ref()
        .ok_or("Alias required for SSL hosts entry")?;

    let config_id = config.id.unwrap_or(-1).to_string();
    let port = config.local_port.unwrap_or(8080);

    add_ssl_host_entry(&config_id, alias, port)
        .map_err(|e| format!("Failed to add HTTPS hosts entry: {}", e))?;

    log::info!(
        "Added HTTPS hosts entries: {} and {}.local -> 127.0.0.1:{}",
        alias,
        alias,
        port
    );
    Ok(())
}

pub fn remove_ssl_host_entry_from_config(
    config: &kftray_commons::models::config_model::Config,
) -> Result<(), String> {
    let config_id = config.id.unwrap_or(-1).to_string();

    remove_ssl_host_entry(&config_id)
        .map_err(|e| format!("Failed to remove HTTPS hosts entry: {}", e))?;

    log::info!("Removed HTTPS hosts entry for config: {}", config_id);
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::net::{
        IpAddr,
        Ipv4Addr,
    };
    use std::sync::Once;

    use super::*;

    static INIT: Once = Once::new();

    fn init() {
        INIT.call_once(|| {
            let _ = env_logger::builder().is_test(true).try_init();
        });
    }

    fn get_test_entry() -> HostEntry {
        HostEntry {
            ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)),
            hostname: "test.local".to_string(),
        }
    }

    #[test]
    fn test_add_and_remove_host_entry() {
        init();
        let _ = remove_all_host_entries();

        let id = "test-id-1".to_string();
        let entry = get_test_entry();

        let result = add_host_entry(id.clone(), entry.clone());
        assert!(result.is_ok());

        let result = remove_host_entry(&id);
        assert!(result.is_ok());
    }

    #[test]
    fn test_manager_creation() {
        init();
        let manager = HostfileManager::new();

        assert!(manager.helper_client.is_some() || manager.helper_client.is_none());
    }
}