use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::sensitive::SensitiveString;
const DEFAULT_DATA_DIR: &str = "/var/lib/geoip";
const DEFAULT_MAX_AGE_DAYS: u32 = 30;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum GeoIpProvider {
#[default]
DbIpLite,
MaxMindGeoLite2,
IpLocate,
IpInfoLite,
Sapics,
Custom,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct AutoDownloadConfig {
pub enabled: bool,
pub data_dir: PathBuf,
pub maxmind_account_id: Option<SensitiveString>,
pub maxmind_license_key: Option<SensitiveString>,
pub ipinfo_token: Option<SensitiveString>,
pub max_age_days: u32,
}
impl Default for AutoDownloadConfig {
fn default() -> Self {
Self {
enabled: true,
data_dir: PathBuf::from(DEFAULT_DATA_DIR),
maxmind_account_id: None,
maxmind_license_key: None,
ipinfo_token: None,
max_age_days: DEFAULT_MAX_AGE_DAYS,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct GeoIpConfig {
pub enabled: bool,
pub provider: GeoIpProvider,
pub city_db_path: Option<PathBuf>,
pub asn_db_path: Option<PathBuf>,
pub auto_download: AutoDownloadConfig,
}
impl Default for GeoIpConfig {
fn default() -> Self {
Self {
enabled: true,
provider: GeoIpProvider::default(),
city_db_path: None,
asn_db_path: None,
auto_download: AutoDownloadConfig::default(),
}
}
}
impl GeoIpConfig {
#[must_use]
pub fn from_cascade() -> Self {
#[cfg(feature = "config")]
{
if let Some(cfg) = crate::config::try_get()
&& let Ok(geoip) = cfg.unmarshal_key_registered::<Self>("geoip")
{
return geoip;
}
}
Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_default_is_dbip_lite() {
assert_eq!(GeoIpProvider::default(), GeoIpProvider::DbIpLite);
}
#[test]
fn provider_variants_compare_distinctly() {
assert_eq!(GeoIpProvider::DbIpLite, GeoIpProvider::DbIpLite);
assert_ne!(GeoIpProvider::DbIpLite, GeoIpProvider::MaxMindGeoLite2);
assert_ne!(GeoIpProvider::Custom, GeoIpProvider::Sapics);
assert_ne!(GeoIpProvider::IpLocate, GeoIpProvider::IpInfoLite);
}
#[test]
fn a_changed_credential_compares_unequal() {
let mut changed = GeoIpConfig::default();
changed.auto_download.ipinfo_token = Some(SensitiveString::from("a-token"));
assert_ne!(GeoIpConfig::default(), changed);
assert_eq!(changed, changed.clone());
}
#[test]
fn provider_round_trips_through_snake_case() {
for (provider, wire) in [
(GeoIpProvider::DbIpLite, "db_ip_lite"),
(GeoIpProvider::MaxMindGeoLite2, "max_mind_geo_lite2"),
(GeoIpProvider::IpLocate, "ip_locate"),
(GeoIpProvider::IpInfoLite, "ip_info_lite"),
(GeoIpProvider::Sapics, "sapics"),
(GeoIpProvider::Custom, "custom"),
] {
let quoted = format!("\"{wire}\"");
assert_eq!(serde_json::to_string(&provider).unwrap(), quoted);
let decoded: GeoIpProvider = serde_json::from_str("ed).unwrap();
assert_eq!(decoded, provider);
}
}
#[test]
fn auto_download_defaults() {
let auto = AutoDownloadConfig::default();
assert!(auto.enabled);
assert_eq!(auto.data_dir, PathBuf::from("/var/lib/geoip"));
assert_eq!(auto.max_age_days, 30);
assert!(auto.maxmind_account_id.is_none());
assert!(auto.maxmind_license_key.is_none());
assert!(auto.ipinfo_token.is_none());
}
#[test]
fn geoip_defaults() {
let config = GeoIpConfig::default();
assert!(config.enabled);
assert_eq!(config.provider, GeoIpProvider::DbIpLite);
assert!(config.city_db_path.is_none());
assert!(config.asn_db_path.is_none());
assert!(config.auto_download.enabled);
}
#[test]
fn deserialises_with_partial_keys() {
let json = r#"{
"provider": "max_mind_geo_lite2",
"auto_download": {
"data_dir": "/srv/geoip",
"max_age_days": 7,
"maxmind_account_id": "123456",
"maxmind_license_key": "secret-key"
}
}"#;
let config: GeoIpConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.provider, GeoIpProvider::MaxMindGeoLite2);
assert_eq!(config.auto_download.data_dir, PathBuf::from("/srv/geoip"));
assert_eq!(config.auto_download.max_age_days, 7);
assert_eq!(
config
.auto_download
.maxmind_license_key
.as_ref()
.map(SensitiveString::expose),
Some("secret-key")
);
assert!(config.enabled);
assert!(config.auto_download.enabled);
}
#[test]
fn credentials_are_redacted_in_debug() {
let config = GeoIpConfig {
auto_download: AutoDownloadConfig {
maxmind_account_id: Some("account-1234".into()),
maxmind_license_key: Some("licence-abcd".into()),
ipinfo_token: Some("token-wxyz".into()),
..Default::default()
},
..Default::default()
};
let rendered = format!("{config:?}");
assert!(!rendered.contains("account-1234"), "{rendered}");
assert!(!rendered.contains("licence-abcd"), "{rendered}");
assert!(!rendered.contains("token-wxyz"), "{rendered}");
assert!(rendered.contains("REDACTED"), "{rendered}");
}
#[test]
fn credentials_are_redacted_when_serialised() {
let config = GeoIpConfig {
auto_download: AutoDownloadConfig {
maxmind_license_key: Some("licence-abcd".into()),
ipinfo_token: Some("token-wxyz".into()),
..Default::default()
},
..Default::default()
};
let dumped = serde_json::to_string(&config).unwrap();
assert!(!dumped.contains("licence-abcd"), "{dumped}");
assert!(!dumped.contains("token-wxyz"), "{dumped}");
}
#[test]
fn from_cascade_falls_back_to_defaults() {
let config = GeoIpConfig::from_cascade();
assert_eq!(config.provider, GeoIpProvider::DbIpLite);
assert!(config.enabled);
}
}