arbiter-proxy 0.0.22

Async HTTP reverse proxy with middleware chain architecture
Documentation
//! Configuration for the arbiter proxy, loaded from TOML.

use serde::Deserialize;
use std::path::Path;

/// Top-level proxy configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct ProxyConfig {
    /// Server listen configuration.
    pub server: ServerConfig,
    /// Upstream target configuration.
    pub upstream: UpstreamConfig,
    /// Middleware pipeline configuration.
    #[serde(default)]
    pub middleware: MiddlewareConfig,
    /// Audit logging configuration.
    #[serde(default)]
    pub audit: AuditConfig,
}

/// Audit logging configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct AuditConfig {
    /// Enable audit logging.
    #[serde(default = "default_audit_enabled")]
    pub enabled: bool,
    /// Path to an append-only audit log file (optional).
    #[serde(default)]
    pub file_path: Option<String>,
    /// Sensitive field patterns for argument redaction (overrides defaults).
    #[serde(default)]
    pub redaction_patterns: Vec<String>,
}

impl Default for AuditConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            file_path: None,
            redaction_patterns: Vec::new(),
        }
    }
}

fn default_audit_enabled() -> bool {
    true
}

/// Server bind address and port.
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
    /// Listen address, e.g. "127.0.0.1".
    #[serde(default = "default_listen_addr")]
    pub listen_addr: String,
    /// Listen port.
    #[serde(default = "default_listen_port")]
    pub listen_port: u16,
}

/// Upstream server to proxy requests to.
#[derive(Debug, Clone, Deserialize)]
pub struct UpstreamConfig {
    /// Full base URL of the upstream, e.g. "http://127.0.0.1:8081".
    pub url: String,
}

/// Configuration for the middleware pipeline.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MiddlewareConfig {
    /// Paths to block (exact match).
    #[serde(default)]
    pub blocked_paths: Vec<String>,
    /// Required headers. Requests missing any of these are rejected.
    #[serde(default)]
    pub required_headers: Vec<String>,
}

fn default_listen_addr() -> String {
    "127.0.0.1".to_string()
}

fn default_listen_port() -> u16 {
    8080
}

impl ProxyConfig {
    /// Load configuration from a TOML file at the given path.
    pub fn from_file(path: &Path) -> anyhow::Result<Self> {
        let contents = std::fs::read_to_string(path)?;
        let config: ProxyConfig = toml::from_str(&contents)?;
        Ok(config)
    }

    /// Parse configuration from a TOML string (useful for tests).
    pub fn parse(s: &str) -> anyhow::Result<Self> {
        let config: ProxyConfig = toml::from_str(s)?;
        Ok(config)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_minimal_config() {
        let toml = r#"
[server]
listen_addr = "0.0.0.0"
listen_port = 9090

[upstream]
url = "http://localhost:3000"
"#;
        let config = ProxyConfig::parse(toml).unwrap();
        assert_eq!(config.server.listen_addr, "0.0.0.0");
        assert_eq!(config.server.listen_port, 9090);
        assert_eq!(config.upstream.url, "http://localhost:3000");
        assert!(config.middleware.blocked_paths.is_empty());
    }

    #[test]
    fn parse_config_with_middleware() {
        let toml = r#"
[server]
listen_port = 8080

[upstream]
url = "http://backend:8081"

[middleware]
blocked_paths = ["/admin", "/secret"]
required_headers = ["x-api-key"]
"#;
        let config = ProxyConfig::parse(toml).unwrap();
        assert_eq!(config.middleware.blocked_paths.len(), 2);
        assert_eq!(config.middleware.required_headers, vec!["x-api-key"]);
    }

    #[test]
    fn defaults_applied() {
        let toml = r#"
[server]

[upstream]
url = "http://localhost:3000"
"#;
        let config = ProxyConfig::parse(toml).unwrap();
        assert_eq!(config.server.listen_addr, "127.0.0.1");
        assert_eq!(config.server.listen_port, 8080);
    }
}