Skip to main content

wisegate/
server.rs

1//! Server startup and information display.
2//!
3//! This module handles the startup banner and configuration display
4//! for the WiseGate reverse proxy.
5
6use crate::env_vars;
7use std::env;
8use tracing::{debug, info, warn};
9use wisegate_core::ConfigProvider;
10
11/// Configuration for startup display.
12///
13/// Decouples the startup info display from CLI argument parsing,
14/// allowing the server module to be used independently.
15#[derive(Clone, Debug)]
16pub struct StartupConfig {
17    /// Port to listen on
18    pub listen_port: u16,
19    /// Port to forward to
20    pub forward_port: u16,
21    /// Bind address
22    pub bind_address: String,
23    /// Whether to show verbose output
24    pub verbose: bool,
25    /// Whether to suppress output
26    pub quiet: bool,
27}
28
29/// Prints the startup banner with current configuration.
30///
31/// Displays information about the server's configuration including:
32/// - Version and binding information
33/// - Rate limiting settings
34/// - Proxy configuration (timeout, max body size)
35/// - Security settings (IP filtering, blocked methods/patterns)
36///
37/// In verbose mode, also displays all environment variable configurations.
38///
39/// # Arguments
40///
41/// * `startup_config` - The startup display configuration
42/// * `config` - Configuration provider for all settings
43///
44/// # Example
45///
46/// ```no_run
47/// use wisegate::server::{print_startup_info, StartupConfig};
48/// use wisegate::config::EnvVarConfig;
49///
50/// let startup = StartupConfig {
51///     listen_port: 8080,
52///     forward_port: 9000,
53///     bind_address: "0.0.0.0".to_string(),
54///     verbose: false,
55///     quiet: false,
56/// };
57/// let config = EnvVarConfig::new();
58/// print_startup_info(&startup, &config);
59/// ```
60pub fn print_startup_info(startup_config: &StartupConfig, config: &impl ConfigProvider) {
61    if startup_config.quiet {
62        return;
63    }
64
65    let rate_config = config.rate_limit_config();
66    let proxy_config = config.proxy_config();
67    let allowed_proxy_ips = config.allowed_proxy_ips();
68
69    info!(
70        version = env!("CARGO_PKG_VERSION"),
71        listen_port = startup_config.listen_port,
72        forward_port = startup_config.forward_port,
73        bind_address = %startup_config.bind_address,
74        "WiseGate starting"
75    );
76
77    info!(
78        max_requests = rate_config.max_requests,
79        window_secs = rate_config.window_duration.as_secs(),
80        "Rate limiting configured"
81    );
82
83    info!(
84        timeout_secs = proxy_config.timeout.as_secs(),
85        max_body_mb = proxy_config.max_body_size_mb(),
86        "Proxy configured"
87    );
88
89    let mode = if allowed_proxy_ips.is_some() {
90        "strict"
91    } else {
92        "permissive"
93    };
94    let trusted_proxies = allowed_proxy_ips.map(|ips| ips.len()).unwrap_or(0);
95
96    info!(
97        mode = mode,
98        trusted_proxies = trusted_proxies,
99        blocked_ips = config.blocked_ips().len(),
100        blocked_methods = config.blocked_methods().len(),
101        blocked_patterns = config.blocked_patterns().len(),
102        "Security configured"
103    );
104
105    // Display authentication status
106    let basic_auth_enabled = config.is_basic_auth_enabled();
107    let bearer_auth_enabled = config.is_bearer_auth_enabled();
108
109    if basic_auth_enabled || bearer_auth_enabled {
110        info!(
111            basic_auth = basic_auth_enabled,
112            basic_auth_users = config.auth_credentials().len(),
113            bearer_token = bearer_auth_enabled,
114            realm = config.auth_realm(),
115            "Authentication configured"
116        );
117    } else {
118        debug!("Authentication disabled (no credentials or bearer token configured)");
119    }
120
121    warn_on_suspicious_config(startup_config, config);
122
123    // Show environment configuration in verbose mode
124    if startup_config.verbose {
125        print_env_config();
126    }
127}
128
129/// Warns at startup when the configuration looks like a common misconfiguration.
130///
131/// These are surfaced even in non-verbose mode (but not in quiet mode) because
132/// they often produce silent "all requests are 400" symptoms that beginners
133/// struggle to diagnose.
134fn warn_on_suspicious_config(startup_config: &StartupConfig, config: &impl ConfigProvider) {
135    if let Some(proxy_ips) = config.allowed_proxy_ips() {
136        // 0.0.0.0 is a bind-only sentinel — it never appears in a Forwarded `by=` field.
137        if proxy_ips.iter().any(|ip| ip == "0.0.0.0") {
138            warn!(
139                "CC_REVERSE_PROXY_IPS contains 0.0.0.0 — this is the bind sentinel, \
140                 not a real proxy IP. Strict mode will reject every request."
141            );
142        }
143        // Listening on a public interface with strict mode but no auth is OK,
144        // but listening on a public interface with NO proxy IPs and NO auth
145        // exposes the upstream to anyone who can reach the port.
146    } else if startup_config.bind_address == "0.0.0.0"
147        && !config.is_auth_enabled()
148        && config.blocked_ips().is_empty()
149    {
150        warn!(
151            "Listening on 0.0.0.0 in permissive mode with no authentication and no IP \
152             blocklist — this proxy is openly reachable. Set CC_REVERSE_PROXY_IPS, \
153             CC_HTTP_BASIC_AUTH, or BLOCKED_IPS, or bind to 127.0.0.1 for local testing."
154        );
155    }
156}
157
158/// Print environment variable configuration status (used in verbose mode)
159fn print_env_config() {
160    for &var_name in env_vars::all_env_vars() {
161        match env::var(var_name) {
162            Ok(value) => {
163                let display_value = mask_sensitive_value(var_name, &value);
164                debug!(name = var_name, value = %display_value, "Environment variable");
165            }
166            Err(_) => {
167                debug!(name = var_name, value = "[NOT SET]", "Environment variable");
168            }
169        }
170    }
171}
172
173/// Masks sensitive values in environment variable display.
174///
175/// Returns "[CONFIGURED]" for variables containing sensitive keywords
176/// like "IP", "PROXY", "AUTH", "TOKEN", "BEARER", or "PASSWORD".
177fn mask_sensitive_value(var_name: &str, value: &str) -> String {
178    let upper = var_name.to_uppercase();
179    if upper.contains("IP")
180        || upper.contains("PROXY")
181        || upper.contains("AUTH")
182        || upper.contains("TOKEN")
183        || upper.contains("BEARER")
184        || upper.contains("PASSWORD")
185        || upper.contains("SECRET")
186    {
187        "[CONFIGURED]".to_string()
188    } else {
189        value.to_string()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    // ===========================================
198    // mask_sensitive_value tests
199    // ===========================================
200
201    #[test]
202    fn test_mask_sensitive_value_with_ip() {
203        assert_eq!(
204            mask_sensitive_value("BLOCKED_IPS", "192.168.1.1"),
205            "[CONFIGURED]"
206        );
207        assert_eq!(
208            mask_sensitive_value("TRUSTED_PROXY_IPS", "10.0.0.1"),
209            "[CONFIGURED]"
210        );
211        assert_eq!(
212            mask_sensitive_value("CC_REVERSE_PROXY_IPS", "172.16.0.1"),
213            "[CONFIGURED]"
214        );
215    }
216
217    #[test]
218    fn test_mask_sensitive_value_with_proxy() {
219        assert_eq!(
220            mask_sensitive_value("PROXY_ALLOWLIST", "10.0.0.1"),
221            "[CONFIGURED]"
222        );
223        assert_eq!(
224            mask_sensitive_value("TRUSTED_PROXY_IPS_VAR", "CUSTOM_VAR"),
225            "[CONFIGURED]"
226        );
227    }
228
229    #[test]
230    fn test_mask_sensitive_value_with_auth() {
231        assert_eq!(
232            mask_sensitive_value("CC_HTTP_BASIC_AUTH", "admin:secret"),
233            "[CONFIGURED]"
234        );
235        assert_eq!(
236            mask_sensitive_value("CC_HTTP_BASIC_AUTH_REALM", "MyRealm"),
237            "[CONFIGURED]"
238        );
239    }
240
241    #[test]
242    fn test_mask_sensitive_value_with_token() {
243        assert_eq!(
244            mask_sensitive_value("CC_BEARER_TOKEN", "my-secret-token"),
245            "[CONFIGURED]"
246        );
247    }
248
249    #[test]
250    fn test_mask_sensitive_value_non_sensitive() {
251        assert_eq!(mask_sensitive_value("RATE_LIMIT_REQUESTS", "100"), "100");
252        assert_eq!(mask_sensitive_value("MAX_BODY_SIZE_MB", "50"), "50");
253        assert_eq!(
254            mask_sensitive_value("BLOCKED_METHODS", "TRACE,CONNECT"),
255            "TRACE,CONNECT"
256        );
257        assert_eq!(
258            mask_sensitive_value("BLOCKED_PATTERNS", ".php,.env"),
259            ".php,.env"
260        );
261    }
262}