Remove accidentally staged worktree dirs
This commit is contained in:
387
src/cli.rs
Normal file
387
src/cli.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
use crate::config::dynamic_config::{
|
||||
BodyConfig, DynamicConfig, RateLimitConfig, SerializableDynamicConfig,
|
||||
};
|
||||
use crate::config::static_config::StaticConfig;
|
||||
use crate::config::validation::validate;
|
||||
|
||||
const DEFAULT_CONFIG_PATH: &str = "/etc/reverse-proxy/config.toml";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "reverse-proxy", version, about = "Reverse proxy server")]
|
||||
pub struct Cli {
|
||||
#[arg(long, default_value = DEFAULT_CONFIG_PATH, help = "Path to config file")]
|
||||
pub config: String,
|
||||
|
||||
#[arg(long, help = "Validate config and exit")]
|
||||
pub validate: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Permit 0.0.0.0 as a bind address (for container deployments)"
|
||||
)]
|
||||
pub allow_wildcard_bind: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoadedConfig {
|
||||
pub static_config: StaticConfig,
|
||||
pub dynamic_config: DynamicConfig,
|
||||
pub allow_wildcard_bind: bool,
|
||||
}
|
||||
|
||||
pub fn parse() -> Cli {
|
||||
Cli::parse()
|
||||
}
|
||||
|
||||
pub fn parse_from<I, S>(args: I) -> Cli
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<std::ffi::OsString> + Clone,
|
||||
{
|
||||
Cli::parse_from(args)
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct RawConfig {
|
||||
#[serde(default)]
|
||||
listeners: Vec<crate::config::static_config::ListenerConfig>,
|
||||
#[serde(default)]
|
||||
allow_wildcard_bind: bool,
|
||||
#[serde(default = "crate::config::static_config::default_health_check_port")]
|
||||
health_check_port: u16,
|
||||
#[serde(default = "crate::config::static_config::default_admin_socket_path")]
|
||||
admin_socket_path: String,
|
||||
#[serde(default = "crate::config::static_config::default_shutdown_timeout_secs")]
|
||||
shutdown_timeout_secs: u64,
|
||||
#[serde(default)]
|
||||
logging: crate::config::static_config::LoggingConfig,
|
||||
rate_limit: RateLimitConfig,
|
||||
body: BodyConfig,
|
||||
}
|
||||
|
||||
pub fn load_config(cli: &Cli) -> Result<LoadedConfig> {
|
||||
let config_path = Path::new(&cli.config);
|
||||
let config_content = std::fs::read_to_string(config_path)
|
||||
.with_context(|| format!("failed to read config file: {}", cli.config))?;
|
||||
|
||||
let raw: RawConfig = toml::from_str(&config_content)
|
||||
.with_context(|| format!("failed to parse config file: {}", cli.config))?;
|
||||
|
||||
let static_config = StaticConfig {
|
||||
listeners: raw.listeners,
|
||||
allow_wildcard_bind: raw.allow_wildcard_bind,
|
||||
health_check_port: raw.health_check_port,
|
||||
admin_socket_path: raw.admin_socket_path,
|
||||
shutdown_timeout_secs: raw.shutdown_timeout_secs,
|
||||
logging: raw.logging,
|
||||
};
|
||||
|
||||
let serializable_dynamic = SerializableDynamicConfig {
|
||||
sites: collect_sites(&static_config),
|
||||
rate_limit: raw.rate_limit,
|
||||
body: raw.body,
|
||||
};
|
||||
|
||||
let dynamic_config: DynamicConfig = serializable_dynamic.into();
|
||||
|
||||
let allow_wildcard_bind = static_config.allow_wildcard_bind || cli.allow_wildcard_bind;
|
||||
|
||||
validate(&static_config, &dynamic_config, cli.allow_wildcard_bind).map_err(|errors| {
|
||||
anyhow::anyhow!(
|
||||
"config validation failed:\n{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| format!(" - {}", e))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(LoadedConfig {
|
||||
static_config,
|
||||
dynamic_config,
|
||||
allow_wildcard_bind,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_sites(static_config: &StaticConfig) -> Vec<crate::config::dynamic_config::SiteConfig> {
|
||||
let mut sites = Vec::new();
|
||||
for listener in &static_config.listeners {
|
||||
sites.extend(listener.sites.clone());
|
||||
}
|
||||
sites
|
||||
}
|
||||
|
||||
pub fn run_validate(cli: &Cli) -> Result<()> {
|
||||
match load_config(cli) {
|
||||
Ok(_) => {
|
||||
println!("Configuration is valid.");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cli_default_config_path() {
|
||||
let cli = Cli::parse_from(["reverse-proxy"]);
|
||||
assert_eq!(cli.config, DEFAULT_CONFIG_PATH);
|
||||
assert!(!cli.validate);
|
||||
assert!(!cli.allow_wildcard_bind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_custom_config_path() {
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", "/tmp/test.toml"]);
|
||||
assert_eq!(cli.config, "/tmp/test.toml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_validate_flag() {
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--validate"]);
|
||||
assert!(cli.validate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_allow_wildcard_bind_flag() {
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--allow-wildcard-bind"]);
|
||||
assert!(cli.allow_wildcard_bind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_all_flags() {
|
||||
let cli = Cli::parse_from([
|
||||
"reverse-proxy",
|
||||
"--config",
|
||||
"/custom/path.toml",
|
||||
"--validate",
|
||||
"--allow-wildcard-bind",
|
||||
]);
|
||||
assert_eq!(cli.config, "/custom/path.toml");
|
||||
assert!(cli.validate);
|
||||
assert!(cli.allow_wildcard_bind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_version_flag() {
|
||||
let result = Cli::try_parse_from(["reverse-proxy", "--version"]);
|
||||
assert!(result.is_err());
|
||||
if let Err(err) = result {
|
||||
use clap::error::ErrorKind;
|
||||
assert!(matches!(err.kind(), ErrorKind::DisplayVersion));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_missing_file() {
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", "/nonexistent/config.toml"]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("failed to read config file") || err_msg.contains("No such file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_invalid_toml() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("bad.toml");
|
||||
std::fs::write(&path, "not valid toml {{{{").unwrap();
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", path.to_str().unwrap()]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("failed to parse config file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_valid_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cert_path = dir.path().join("cert.pem");
|
||||
let key_path = dir.path().join("key.pem");
|
||||
std::fs::write(&cert_path, "cert").unwrap();
|
||||
std::fs::write(&key_path, "key").unwrap();
|
||||
|
||||
let toml = format!(
|
||||
r#"
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "127.0.0.1"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "manual"
|
||||
cert_path = "{}"
|
||||
key_path = "{}"
|
||||
|
||||
[[listeners.sites]]
|
||||
host = "test.local"
|
||||
upstream = "127.0.0.1:8080"
|
||||
"#,
|
||||
cert_path.to_str().unwrap(),
|
||||
key_path.to_str().unwrap()
|
||||
);
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_ok());
|
||||
let loaded = result.unwrap();
|
||||
assert!(!loaded.allow_wildcard_bind);
|
||||
assert_eq!(loaded.static_config.listeners.len(), 1);
|
||||
assert_eq!(loaded.dynamic_config.sites.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_wildcard_bind_or_logic() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let toml = r#"
|
||||
allow_wildcard_bind = false
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "0.0.0.0"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["test.local"]
|
||||
acme_cache_dir = "/tmp/acme"
|
||||
"#;
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let cli = Cli::parse_from([
|
||||
"reverse-proxy",
|
||||
"--config",
|
||||
config_path.to_str().unwrap(),
|
||||
"--allow-wildcard-bind",
|
||||
]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_ok());
|
||||
let loaded = result.unwrap();
|
||||
assert!(loaded.allow_wildcard_bind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_wildcard_bind_rejected_without_flag() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let toml = r#"
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "0.0.0.0"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["test.local"]
|
||||
acme_cache_dir = "/tmp/acme"
|
||||
"#;
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("config validation failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_validation_fails_reports_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let toml = r#"
|
||||
[rate_limit]
|
||||
requests_per_second = 0
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 0
|
||||
"#;
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("config validation failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_allow_wildcard_from_config_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let toml = r#"
|
||||
allow_wildcard_bind = true
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "0.0.0.0"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["test.local"]
|
||||
acme_cache_dir = "/tmp/acme"
|
||||
"#;
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||
let result = load_config(&cli);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().allow_wildcard_bind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_from_vec() {
|
||||
let args = vec!["reverse-proxy", "--validate", "--allow-wildcard-bind"];
|
||||
let cli = parse_from(args);
|
||||
assert!(cli.validate);
|
||||
assert!(cli.allow_wildcard_bind);
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,15 @@ pub struct StaticConfig {
|
||||
pub logging: LoggingConfig,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_health_check_port() -> u16 {
|
||||
pub fn default_health_check_port() -> u16 {
|
||||
9900
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_admin_socket_path() -> String {
|
||||
pub fn default_admin_socket_path() -> String {
|
||||
"/run/reverse-proxy/admin.sock".to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_shutdown_timeout_secs() -> u64 {
|
||||
pub fn default_shutdown_timeout_secs() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod health;
|
||||
pub mod logging;
|
||||
|
||||
21
src/main.rs
21
src/main.rs
@@ -1,3 +1,22 @@
|
||||
use reverse_proxy::cli;
|
||||
|
||||
fn main() {
|
||||
tracing::info!("reverse-proxy starting");
|
||||
let args = cli::parse();
|
||||
|
||||
if args.validate {
|
||||
match cli::run_validate(&args) {
|
||||
Ok(()) => std::process::exit(0),
|
||||
Err(_) => std::process::exit(1),
|
||||
}
|
||||
}
|
||||
|
||||
match cli::load_config(&args) {
|
||||
Ok(_config) => {
|
||||
tracing::info!("reverse-proxy starting");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e:#}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user