use figment::{
providers::{Env, Format, Toml, Yaml},
Figment, Provider,
};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigPath {
pub tls_certificates: String,
pub tls_challenges: String,
pub tls_order: String,
pub tls_account_credentials: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigRouteHeaderAdd {
pub name: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigRouteHeaderRemove {
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigRouteHeader {
pub add: Vec<ConfigRouteHeaderAdd>,
pub remove: Vec<ConfigRouteHeaderRemove>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigRouteUpstream {
pub ip: String,
pub port: i16,
pub network: Option<String>,
pub weight: Option<i8>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigRoute {
pub host: String,
pub headers: Option<ConfigRouteHeader>,
pub path_suffix: Option<String>,
pub path_prefix: Option<String>,
pub upstreams: Vec<ConfigRouteUpstream>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigLogging {
#[serde(deserialize_with = "log_level_deser")]
pub level: LogLevel,
pub access_logs: bool,
pub error_logs: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Config {
#[serde(default)]
pub service_name: String,
pub logging: Option<ConfigLogging>,
pub paths: Option<ConfigPath>,
pub routes: Vec<ConfigRoute>,
}
impl Default for Config {
fn default() -> Self {
Config {
service_name: "proksi".to_string(),
routes: vec![],
logging: Some(ConfigLogging {
level: LogLevel::Info,
access_logs: true,
error_logs: false,
}),
paths: Some(ConfigPath {
tls_certificates: "/etc/proksi/tls/certificates".to_string(),
tls_challenges: "/etc/proksi/tls/challenges".to_string(),
tls_order: "/etc/proksi/tls/orders".to_string(),
tls_account_credentials: "/etc/proksi/tls/account".to_string(),
}),
}
}
}
impl Provider for Config {
fn metadata(&self) -> figment::Metadata {
figment::Metadata::named("proksi")
}
fn data(
&self,
) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
figment::providers::Serialized::defaults(Config::default()).data()
}
}
pub fn load_proxy_config(config_path: &str) -> Result<Config, figment::Error> {
let config: Config = Figment::new()
.merge(Config::default())
.merge(Yaml::file(format!("{}/proksi-config.yaml", config_path)))
.merge(Toml::file(format!("{}/proksi-config.toml", config_path)))
.merge(Env::prefixed("PROKSI_").split("__"))
.extract()?;
Ok(config)
}
fn log_level_deser<'de, D>(deserializer: D) -> Result<LogLevel, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"debug" => Ok(LogLevel::Debug),
"info" => Ok(LogLevel::Info),
"warn" => Ok(LogLevel::Warn),
"error" => Ok(LogLevel::Error),
_ => Err(serde::de::Error::custom(
"expected one of DEBUG, INFO, WARN, ERROR",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn helper_config_file() -> &'static str {
r#"
service_name: "proksi"
logging:
level: "INFO"
access_logs: true
error_logs: false
routes:
- host: "example.com"
path_prefix: "/api"
headers:
add:
- name: "X-Forwarded-For"
value: "<value>"
- name: "X-Api-Version"
value: "1.0"
remove:
- name: "Server"
upstreams:
- ip: "10.0.1.3/25"
port: 3000
network: "public"
"#
}
#[test]
fn test_load_config_from_yaml() {
figment::Jail::expect_with(|jail| {
let tmp_dir = jail.directory().to_string_lossy();
jail.create_file(
format!("{}/proksi-config.yaml", tmp_dir),
helper_config_file(),
)?;
let config = load_proxy_config(&tmp_dir);
let proxy_config = config.unwrap();
assert_eq!(proxy_config.service_name, "proksi");
Ok(())
});
}
#[test]
fn test_load_config_from_yaml_and_env_vars() {
figment::Jail::expect_with(|jail| {
jail.create_file(
format!("{}/proksi-config.yaml", jail.directory().to_str().unwrap()),
helper_config_file(),
)?;
jail.set_env("PROKSI_SERVICE_NAME", "new_name");
jail.set_env("PROKSI_LOGGING__LEVEL", "warn");
jail.set_env(
"PROKSI_ROUTES",
r#"[{
host="changed.example.com",
upstreams=[{ ip="10.0.1.2/24", port=3000, weight=1 }] }]
"#,
);
let config = load_proxy_config(jail.directory().to_str().unwrap());
let proxy_config = config.unwrap();
assert_eq!(proxy_config.service_name, "new_name");
assert_eq!(proxy_config.logging.unwrap().level, LogLevel::Warn);
assert_eq!(proxy_config.routes[0].host, "changed.example.com");
assert_eq!(proxy_config.routes[0].upstreams[0].ip, "10.0.1.2/24");
Ok(())
});
}
#[test]
fn test_load_config_with_defaults_only() {
let config = load_proxy_config("/tmp");
let proxy_config = config.unwrap();
let logging = proxy_config.logging.unwrap();
assert_eq!(proxy_config.service_name, "proksi");
assert_eq!(logging.level, LogLevel::Info);
assert_eq!(logging.access_logs, true);
assert_eq!(logging.error_logs, false);
assert_eq!(proxy_config.routes.len(), 0);
}
#[test]
fn test_load_config_with_defaults_and_yaml() {
figment::Jail::expect_with(|jail| {
let tmp_dir = jail.directory().to_string_lossy();
jail.create_file(
format!("{}/proksi-config.yaml", tmp_dir),
r#"
routes:
- host: "example.com"
upstreams:
- ip: "10.1.2.24/24"
port: 3000
"#,
)?;
let config = load_proxy_config(&tmp_dir);
let proxy_config = config.unwrap();
let logging = proxy_config.logging.unwrap();
let paths = proxy_config.paths.unwrap();
assert_eq!(proxy_config.service_name, "proksi");
assert_eq!(logging.level, LogLevel::Info);
assert_eq!(logging.access_logs, true);
assert_eq!(logging.error_logs, false);
assert_eq!(proxy_config.routes.len(), 1);
assert_eq!(paths.tls_account_credentials, "/etc/proksi/tls/account");
assert_eq!(paths.tls_certificates, "/etc/proksi/tls/certificates");
Ok(())
});
}
}