use std::env;
use std::str::FromStr;
use std::time::Duration;
use std::sync::LazyLock;
use tracing::warn;
use crate::env_vars;
use wisegate_core::{
AuthenticationProvider, ConnectionProvider, Credential, Credentials, FilteringProvider,
ProxyConfig, ProxyProvider, RateLimitCleanupConfig, RateLimitConfig, RateLimitingProvider,
defaults,
};
static RATE_LIMIT_CONFIG: LazyLock<RateLimitConfig> = LazyLock::new(compute_rate_limit_config);
static RATE_LIMIT_CLEANUP_CONFIG: LazyLock<RateLimitCleanupConfig> =
LazyLock::new(compute_rate_limit_cleanup_config);
static PROXY_CONFIG: LazyLock<ProxyConfig> = LazyLock::new(compute_proxy_config);
static BLOCKED_IPS: LazyLock<Vec<String>> = LazyLock::new(compute_blocked_ips);
static BLOCKED_PATTERNS: LazyLock<Vec<String>> = LazyLock::new(compute_blocked_patterns);
static BLOCKED_METHODS: LazyLock<Vec<String>> = LazyLock::new(compute_blocked_methods);
static ALLOWED_PROXY_IPS: LazyLock<Option<Vec<String>>> =
LazyLock::new(|| compute_allowed_proxy_ips_internal(|key| std::env::var(key)));
static MAX_CONNECTIONS: LazyLock<usize> = LazyLock::new(compute_max_connections);
static AUTH_CREDENTIALS: LazyLock<Credentials> = LazyLock::new(compute_auth_credentials);
static AUTH_REALM: LazyLock<String> = LazyLock::new(compute_auth_realm);
static BEARER_TOKEN: LazyLock<Option<String>> = LazyLock::new(compute_bearer_token);
const ALLOWED_PROXY_VAR_NAMES: &[&str] = &[
"TRUSTED_PROXY_IPS",
"REVERSE_PROXY_IPS",
"PROXY_ALLOWLIST",
"ALLOWED_PROXY_IPS",
"PROXY_IPS",
];
fn parse_env_var_or_default<T>(var_name: &str, default: T) -> T
where
T: FromStr + Copy,
{
match env::var(var_name) {
Ok(value) => match value.parse() {
Ok(parsed) => parsed,
Err(_) => {
warn!(var = var_name, value = %value, "Invalid env var value, using default");
default
}
},
Err(_) => default,
}
}
fn parse_comma_separated(input: &str) -> Vec<String> {
input
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn parse_comma_separated_uppercase(input: &str) -> Vec<String> {
input
.split(',')
.map(|s| s.trim().to_uppercase())
.filter(|s| !s.is_empty())
.collect()
}
pub fn get_rate_limit_config() -> &'static RateLimitConfig {
&RATE_LIMIT_CONFIG
}
fn compute_rate_limit_config() -> RateLimitConfig {
let max_requests =
parse_env_var_or_default(env_vars::RATE_LIMIT_REQUESTS, defaults::RATE_LIMIT_REQUESTS);
let window_secs = parse_env_var_or_default(
env_vars::RATE_LIMIT_WINDOW_SECS,
defaults::RATE_LIMIT_WINDOW_SECS,
);
let config = RateLimitConfig {
max_requests,
window_duration: Duration::from_secs(window_secs),
};
if !config.is_valid() {
warn!("Invalid rate limit configuration, using defaults");
return RateLimitConfig {
max_requests: defaults::RATE_LIMIT_REQUESTS,
window_duration: Duration::from_secs(defaults::RATE_LIMIT_WINDOW_SECS),
};
}
config
}
pub fn get_rate_limit_cleanup_config() -> &'static RateLimitCleanupConfig {
&RATE_LIMIT_CLEANUP_CONFIG
}
fn compute_rate_limit_cleanup_config() -> RateLimitCleanupConfig {
let threshold = parse_env_var_or_default(
env_vars::RATE_LIMIT_CLEANUP_THRESHOLD,
defaults::RATE_LIMIT_CLEANUP_THRESHOLD,
);
let interval_secs = parse_env_var_or_default(
env_vars::RATE_LIMIT_CLEANUP_INTERVAL_SECS,
defaults::RATE_LIMIT_CLEANUP_INTERVAL_SECS,
);
RateLimitCleanupConfig {
threshold,
interval: Duration::from_secs(interval_secs),
}
}
pub fn get_proxy_config() -> &'static ProxyConfig {
&PROXY_CONFIG
}
fn compute_proxy_config() -> ProxyConfig {
let timeout_secs =
parse_env_var_or_default(env_vars::PROXY_TIMEOUT_SECS, defaults::PROXY_TIMEOUT_SECS);
let max_body_mb =
parse_env_var_or_default(env_vars::MAX_BODY_SIZE_MB, defaults::MAX_BODY_SIZE_MB);
let config = ProxyConfig {
timeout: Duration::from_secs(timeout_secs),
max_body_size: ProxyConfig::mb_to_bytes(max_body_mb),
};
if !config.is_valid() {
warn!("Invalid proxy configuration, using defaults");
return ProxyConfig {
timeout: Duration::from_secs(defaults::PROXY_TIMEOUT_SECS),
max_body_size: ProxyConfig::mb_to_bytes(defaults::MAX_BODY_SIZE_MB),
};
}
config
}
pub fn get_max_connections() -> usize {
*MAX_CONNECTIONS
}
fn compute_max_connections() -> usize {
parse_env_var_or_default(env_vars::MAX_CONNECTIONS, defaults::MAX_CONNECTIONS)
}
pub fn get_allowed_proxy_ips() -> Option<&'static Vec<String>> {
ALLOWED_PROXY_IPS.as_ref()
}
fn compute_allowed_proxy_ips_internal<F>(env_var: F) -> Option<Vec<String>>
where
F: Fn(&str) -> Result<String, std::env::VarError>,
{
if let Ok(ips) = env_var(env_vars::ALLOWED_PROXY_IPS)
&& !ips.trim().is_empty()
{
return Some(parse_comma_separated(&ips));
}
if let Ok(alt_var_name) = env_var(env_vars::TRUSTED_PROXY_IPS_VAR) {
if ALLOWED_PROXY_VAR_NAMES.contains(&alt_var_name.as_str())
&& let Ok(ips) = env_var(&alt_var_name)
&& !ips.trim().is_empty()
{
return Some(parse_comma_separated(&ips));
} else if !ALLOWED_PROXY_VAR_NAMES.contains(&alt_var_name.as_str()) {
warn!(
var = %alt_var_name,
allowed = ?ALLOWED_PROXY_VAR_NAMES,
"Invalid TRUSTED_PROXY_IPS_VAR value"
);
}
}
None
}
pub fn get_blocked_ips() -> &'static Vec<String> {
&BLOCKED_IPS
}
fn compute_blocked_ips() -> Vec<String> {
env::var(env_vars::BLOCKED_IPS)
.map(|s| parse_comma_separated(&s))
.unwrap_or_default()
}
pub fn get_blocked_patterns() -> &'static Vec<String> {
&BLOCKED_PATTERNS
}
fn compute_blocked_patterns() -> Vec<String> {
env::var(env_vars::BLOCKED_PATTERNS)
.map(|s| {
s.split(',')
.map(|p| p.trim().to_lowercase())
.filter(|p| !p.is_empty())
.collect()
})
.unwrap_or_default()
}
pub fn get_blocked_methods() -> &'static Vec<String> {
&BLOCKED_METHODS
}
fn compute_blocked_methods() -> Vec<String> {
env::var(env_vars::BLOCKED_METHODS)
.map(|s| parse_comma_separated_uppercase(&s))
.unwrap_or_default()
}
pub fn get_auth_credentials() -> &'static Credentials {
&AUTH_CREDENTIALS
}
fn compute_auth_credentials() -> Credentials {
let mut entries = Vec::new();
if let Ok(value) = env::var(env_vars::CC_HTTP_BASIC_AUTH) {
if let Some(cred) = Credential::parse(&value) {
entries.push(cred);
} else {
warn!(
var = env_vars::CC_HTTP_BASIC_AUTH,
"Invalid credential format (expected username:password)"
);
}
}
let mut index = 1;
let mut consecutive_missing = 0;
loop {
let var_name = format!("{}{}", env_vars::CC_HTTP_BASIC_AUTH_N, index);
match env::var(&var_name) {
Ok(value) => {
if consecutive_missing > 0 {
warn!(
gap_at = index - consecutive_missing,
found_at = index,
"Gap in numbered credentials, some may have been skipped"
);
}
consecutive_missing = 0;
if let Some(cred) = Credential::parse(&value) {
entries.push(cred);
} else {
warn!(var = %var_name, "Invalid credential format (expected username:password)");
}
index += 1;
}
Err(_) => {
consecutive_missing += 1;
if consecutive_missing >= 3 {
break;
}
index += 1;
}
}
}
Credentials::from_entries(entries)
}
pub fn get_auth_realm() -> &'static str {
&AUTH_REALM
}
fn compute_auth_realm() -> String {
env::var(env_vars::CC_HTTP_BASIC_AUTH_REALM)
.unwrap_or_else(|_| defaults::AUTH_REALM.to_string())
}
pub fn get_bearer_token() -> Option<&'static str> {
BEARER_TOKEN.as_deref()
}
fn compute_bearer_token() -> Option<String> {
env::var(env_vars::CC_BEARER_TOKEN)
.ok()
.filter(|s| !s.trim().is_empty())
}
#[derive(Clone, Debug)]
pub struct EnvVarConfig {
_private: (),
}
impl EnvVarConfig {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for EnvVarConfig {
fn default() -> Self {
Self::new()
}
}
impl RateLimitingProvider for EnvVarConfig {
fn rate_limit_config(&self) -> &RateLimitConfig {
get_rate_limit_config()
}
fn rate_limit_cleanup_config(&self) -> &RateLimitCleanupConfig {
get_rate_limit_cleanup_config()
}
}
impl ProxyProvider for EnvVarConfig {
fn proxy_config(&self) -> &ProxyConfig {
get_proxy_config()
}
fn allowed_proxy_ips(&self) -> Option<&[String]> {
get_allowed_proxy_ips().map(|v| v.as_slice())
}
}
impl FilteringProvider for EnvVarConfig {
fn blocked_ips(&self) -> &[String] {
get_blocked_ips()
}
fn blocked_methods(&self) -> &[String] {
get_blocked_methods()
}
fn blocked_patterns(&self) -> &[String] {
get_blocked_patterns()
}
}
impl ConnectionProvider for EnvVarConfig {
fn max_connections(&self) -> usize {
get_max_connections()
}
}
impl AuthenticationProvider for EnvVarConfig {
fn auth_credentials(&self) -> &Credentials {
get_auth_credentials()
}
fn auth_realm(&self) -> &str {
get_auth_realm()
}
fn bearer_token(&self) -> Option<&str> {
get_bearer_token()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn create_mock_env(
vars: HashMap<&str, &str>,
) -> impl Fn(&str) -> Result<String, std::env::VarError> {
move |key: &str| {
vars.get(key)
.map(|v| v.to_string())
.ok_or(std::env::VarError::NotPresent)
}
}
#[test]
fn test_get_allowed_proxy_ips_with_cc_reverse_proxy_ips() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::ALLOWED_PROXY_IPS, "192.168.1.1,10.0.0.1");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_some());
let ips = result.unwrap();
assert_eq!(ips.len(), 2);
assert_eq!(ips[0], "192.168.1.1");
assert_eq!(ips[1], "10.0.0.1");
}
#[test]
fn test_get_allowed_proxy_ips_with_alternative_var() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::TRUSTED_PROXY_IPS_VAR, "TRUSTED_PROXY_IPS");
env_vars.insert("TRUSTED_PROXY_IPS", "172.16.0.1,203.0.113.1");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_some());
let ips = result.unwrap();
assert_eq!(ips.len(), 2);
assert_eq!(ips[0], "172.16.0.1");
assert_eq!(ips[1], "203.0.113.1");
}
#[test]
fn test_get_allowed_proxy_ips_cc_takes_priority() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::ALLOWED_PROXY_IPS, "192.168.1.1");
env_vars.insert(env_vars::TRUSTED_PROXY_IPS_VAR, "MY_CUSTOM_PROXY_IPS");
env_vars.insert("MY_CUSTOM_PROXY_IPS", "172.16.0.1");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_some());
let ips = result.unwrap();
assert_eq!(ips.len(), 1);
assert_eq!(ips[0], "192.168.1.1"); }
#[test]
fn test_get_allowed_proxy_ips_fallback_to_alternative() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::TRUSTED_PROXY_IPS_VAR, "PROXY_ALLOWLIST");
env_vars.insert("PROXY_ALLOWLIST", "10.1.1.1,10.1.1.2,10.1.1.3");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_some());
let ips = result.unwrap();
assert_eq!(ips.len(), 3);
assert_eq!(ips[0], "10.1.1.1");
assert_eq!(ips[1], "10.1.1.2");
assert_eq!(ips[2], "10.1.1.3");
}
#[test]
fn test_get_allowed_proxy_ips_rejects_non_whitelisted_var() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::TRUSTED_PROXY_IPS_VAR, "DATABASE_URL");
env_vars.insert("DATABASE_URL", "10.1.1.1,10.1.1.2");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_none());
}
#[test]
fn test_get_allowed_proxy_ips_none_when_no_vars() {
let env_vars = HashMap::new();
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_none());
}
#[test]
fn test_get_allowed_proxy_ips_handles_whitespace() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::ALLOWED_PROXY_IPS, " 192.168.1.1 , 10.0.0.1 ");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_some());
let ips = result.unwrap();
assert_eq!(ips.len(), 2);
assert_eq!(ips[0], "192.168.1.1");
assert_eq!(ips[1], "10.0.0.1");
}
#[test]
fn test_get_allowed_proxy_ips_ignores_empty_alternative_var() {
let mut env_vars = HashMap::new();
env_vars.insert(env_vars::TRUSTED_PROXY_IPS_VAR, "EMPTY_PROXY_VAR");
env_vars.insert("EMPTY_PROXY_VAR", "");
let env_fn = create_mock_env(env_vars);
let result = compute_allowed_proxy_ips_internal(env_fn);
assert!(result.is_none());
}
#[test]
fn test_parse_comma_separated_basic() {
let result = parse_comma_separated("a,b,c");
assert_eq!(result, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_comma_separated_with_whitespace() {
let result = parse_comma_separated(" a , b , c ");
assert_eq!(result, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_comma_separated_empty_string() {
let result = parse_comma_separated("");
assert!(result.is_empty());
}
#[test]
fn test_parse_comma_separated_single_item() {
let result = parse_comma_separated("single");
assert_eq!(result, vec!["single"]);
}
#[test]
fn test_parse_comma_separated_filters_empty_entries() {
let result = parse_comma_separated("a,,b,,,c");
assert_eq!(result, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_comma_separated_only_commas() {
let result = parse_comma_separated(",,,");
assert!(result.is_empty());
}
#[test]
fn test_parse_comma_separated_whitespace_only() {
let result = parse_comma_separated(" , , ");
assert!(result.is_empty());
}
#[test]
fn test_parse_comma_separated_uppercase_basic() {
let result = parse_comma_separated_uppercase("get,post,put");
assert_eq!(result, vec!["GET", "POST", "PUT"]);
}
#[test]
fn test_parse_comma_separated_uppercase_mixed_case() {
let result = parse_comma_separated_uppercase("Get,POST,pUt");
assert_eq!(result, vec!["GET", "POST", "PUT"]);
}
#[test]
fn test_parse_comma_separated_uppercase_with_whitespace() {
let result = parse_comma_separated_uppercase(" get , post , put ");
assert_eq!(result, vec!["GET", "POST", "PUT"]);
}
#[test]
fn test_parse_comma_separated_uppercase_empty() {
let result = parse_comma_separated_uppercase("");
assert!(result.is_empty());
}
#[test]
fn test_parse_comma_separated_uppercase_filters_empty() {
let result = parse_comma_separated_uppercase("get,,post");
assert_eq!(result, vec!["GET", "POST"]);
}
#[test]
fn test_env_var_config_default_values() {
let config = EnvVarConfig::new();
let rate_config = config.rate_limit_config();
assert!(rate_config.is_valid());
assert_eq!(rate_config.max_requests, defaults::RATE_LIMIT_REQUESTS);
assert_eq!(
rate_config.window_duration,
Duration::from_secs(defaults::RATE_LIMIT_WINDOW_SECS)
);
assert!(config.proxy_config().is_valid());
assert!(config.rate_limit_cleanup_config().is_enabled());
assert_eq!(config.max_connections(), defaults::MAX_CONNECTIONS);
assert!(config.blocked_ips().is_empty());
assert!(config.blocked_methods().is_empty());
assert!(config.blocked_patterns().is_empty());
}
}