eggress-config 1.0.6

TOML configuration and validation for eggress proxy
Documentation
//! Process/timeout/admin compilation.

use crate::error::ConfigError;
use crate::model::ConfigFile;
use crate::validate::validate_duration;

use super::model::*;
use super::{parse_duration_opt, resolve_password};

pub(crate) fn compile_process(config: &ConfigFile) -> ProcessConfig {
    let defaults = ProcessConfig::default();
    let process = config.process.as_ref();

    ProcessConfig {
        log_format: process
            .and_then(|p| p.log_format.clone())
            .unwrap_or(defaults.log_format),
        log_level: process
            .and_then(|p| p.log_level.clone())
            .unwrap_or(defaults.log_level),
        shutdown_grace: process
            .and_then(|p| p.shutdown_grace.as_ref())
            .and_then(|s| parse_duration_opt(s))
            .unwrap_or(defaults.shutdown_grace),
    }
}

pub(crate) fn compile_timeouts(config: &ConfigFile) -> Result<TimeoutConfig, ConfigError> {
    let defaults = TimeoutConfig::default();
    let timeouts = config.timeouts.as_ref();

    Ok(TimeoutConfig {
        handshake: timeouts
            .and_then(|t| t.handshake.as_ref())
            .map(|s| validate_duration(s))
            .transpose()?
            .unwrap_or(defaults.handshake),
        connect: timeouts
            .and_then(|t| t.connect.as_ref())
            .map(|s| validate_duration(s))
            .transpose()?
            .unwrap_or(defaults.connect),
    })
}

pub(crate) fn compile_admin(config: &ConfigFile) -> Result<Option<AdminConfig>, ConfigError> {
    let Some(admin) = config.admin.as_ref() else {
        return Ok(None);
    };

    let auth = admin.auth.as_ref().map(compile_admin_auth).transpose()?;

    let pac = admin.pac.as_ref().map(|pac_toml| {
        let path = pac_toml.path.clone().unwrap_or_else(|| "/pac".to_string());
        PacConfig {
            path,
            proxy_directive: pac_toml.proxy.clone(),
            direct_fallback: pac_toml.direct_fallback.unwrap_or(true),
            direct_hosts: pac_toml.direct_hosts.clone().unwrap_or_default(),
            direct_suffixes: pac_toml.direct_suffixes.clone().unwrap_or_default(),
        }
    });

    let static_content = admin
        .static_content
        .as_ref()
        .map(|entries| {
            entries
                .iter()
                .map(|entry| StaticRoute {
                    path: entry.path.clone(),
                    content_type: entry
                        .content_type
                        .clone()
                        .unwrap_or_else(|| "text/plain".to_string()),
                    body: entry.body.clone().unwrap_or_default(),
                })
                .collect()
        })
        .unwrap_or_default();

    Ok(Some(AdminConfig {
        bind: admin
            .bind
            .clone()
            .unwrap_or_else(|| "127.0.0.1:9090".to_string()),
        enabled: admin.enabled.unwrap_or(true),
        metrics: admin.metrics.unwrap_or(true),
        auth,
        pac,
        static_content,
    }))
}

pub(crate) fn compile_admin_auth(
    auth: &crate::model::AdminAuthConfig,
) -> Result<AdminAuthConfig, ConfigError> {
    let bearer_token = resolve_password(
        auth.bearer_token.as_deref(),
        auth.bearer_token_env.as_deref(),
        "admin.auth.bearer_token",
    )?;

    if bearer_token.as_deref().is_some_and(str::is_empty) {
        return Err(ConfigError::validation(
            "admin.auth.bearer_token",
            "bearer token must not be empty",
        ));
    }

    let basic = auth.basic_auth.as_ref();
    if bearer_token.is_some() && basic.is_some() {
        return Err(ConfigError::validation(
            "admin.auth",
            "configure either bearer_token or basic_auth, not both",
        ));
    }

    let (basic_username, basic_password) = if let Some(basic) = basic {
        let password = resolve_password(
            basic.password.as_deref(),
            basic.password_env.as_deref(),
            "admin.auth.basic_auth",
        )?;
        let Some(password) = password else {
            return Err(ConfigError::validation(
                "admin.auth.basic_auth",
                "basic_auth requires password or password_env",
            ));
        };
        if basic.user.is_empty() || password.is_empty() {
            return Err(ConfigError::validation(
                "admin.auth.basic_auth",
                "basic_auth user and password must not be empty",
            ));
        }
        (Some(basic.user.clone()), Some(password))
    } else {
        (None, None)
    };

    if bearer_token.is_none() && basic_username.is_none() {
        return Err(ConfigError::validation(
            "admin.auth",
            "authentication requires bearer_token or basic_auth",
        ));
    }

    Ok(AdminAuthConfig {
        bearer_token,
        basic_username,
        basic_password,
    })
}