Remove accidentally staged worktree dirs
This commit is contained in:
1
.worktrees/feat/config/cli-parsing
Submodule
1
.worktrees/feat/config/cli-parsing
Submodule
Submodule .worktrees/feat/config/cli-parsing added at d89ab71f85
1
.worktrees/feat/ops/admin-socket
Submodule
1
.worktrees/feat/ops/admin-socket
Submodule
Submodule .worktrees/feat/ops/admin-socket added at 56eda4e47c
1
.worktrees/feat/proxy/headers-and-forwarding
Submodule
1
.worktrees/feat/proxy/headers-and-forwarding
Submodule
Submodule .worktrees/feat/proxy/headers-and-forwarding added at 2791070971
1
.worktrees/feat/tls/http-redirect
Submodule
1
.worktrees/feat/tls/http-redirect
Submodule
Submodule .worktrees/feat/tls/http-redirect added at d893187c40
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
mod helpers;
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
|
||||
use reverse_proxy::config::dynamic_config::{
|
||||
BodyConfig, DynamicConfig, RateLimitConfig, SiteConfig,
|
||||
};
|
||||
@@ -254,6 +257,147 @@ async fn test_rate_limit_eviction_task() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
fn write_valid_config(dir: &std::path::Path) -> std::path::PathBuf {
|
||||
let cert_path = dir.join("cert.pem");
|
||||
let key_path = dir.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.join("valid_config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
config_path
|
||||
}
|
||||
|
||||
fn write_invalid_config(dir: &std::path::Path) -> std::path::PathBuf {
|
||||
let toml = r#"
|
||||
[rate_limit]
|
||||
requests_per_second = 0
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 0
|
||||
"#;
|
||||
let config_path = dir.join("invalid_config.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
config_path
|
||||
}
|
||||
|
||||
fn binary_path() -> std::path::PathBuf {
|
||||
let bin = env!("CARGO_BIN_EXE_reverse-proxy");
|
||||
std::path::PathBuf::from(bin)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_valid_config_exits_0() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config_path = write_valid_config(dir.path());
|
||||
let output = Command::new(binary_path())
|
||||
.arg("--config")
|
||||
.arg(config_path.to_str().unwrap())
|
||||
.arg("--validate")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"expected exit 0, got {}: stderr={}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("valid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_invalid_config_exits_1() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config_path = write_invalid_config(dir.path());
|
||||
let output = Command::new(binary_path())
|
||||
.arg("--config")
|
||||
.arg(config_path.to_str().unwrap())
|
||||
.arg("--validate")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success(), "expected exit 1, got success");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("validation failed") || stderr.contains("error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_missing_config_file_exits_1() {
|
||||
let output = Command::new(binary_path())
|
||||
.arg("--config")
|
||||
.arg("/nonexistent/path/config.toml")
|
||||
.arg("--validate")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success(), "expected exit 1, got success");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_wildcard_bind_via_cli_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("wildcard.toml");
|
||||
std::fs::write(&config_path, toml).unwrap();
|
||||
|
||||
let output = Command::new(binary_path())
|
||||
.arg("--config")
|
||||
.arg(config_path.to_str().unwrap())
|
||||
.arg("--validate")
|
||||
.arg("--allow-wildcard-bind")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"expected exit 0 with --allow-wildcard-bind, got {}: stderr={}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn test_dynamic_config_with_limit(limit_bytes: u64) -> Arc<ArcSwap<DynamicConfig>> {
|
||||
let config = DynamicConfig {
|
||||
sites: vec![SiteConfig {
|
||||
|
||||
Reference in New Issue
Block a user