use std::net::IpAddr;
use clap::Parser;
#[derive(Parser)]
#[command(name = env!("CARGO_PKG_NAME"))]
#[command(about = env!("CARGO_PKG_DESCRIPTION"))]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(author = env!("CARGO_PKG_AUTHORS"))]
#[command(
long_about = "🧙 \"You shall not pass!\" - A wise guardian for your network gates\nAn efficient, secure reverse proxy with built-in rate limiting and IP filtering\n\nExample usage:\n wisegate --listen 8080 --forward 9000\n wisegate -l 8080 -f 9000 --verbose"
)]
#[command(
after_help = "Environment variables:\n CC_REVERSE_PROXY_IPS Trusted proxy IPs (enables strict mode)\n BLOCKED_IPS Comma-separated blocked client IPs\n BLOCKED_METHODS HTTP methods to block (e.g., PUT,DELETE)\n BLOCKED_PATTERNS URL patterns to block (e.g., .php,.yaml)\n RATE_LIMIT_REQUESTS Max requests per window (default: 100)\n RATE_LIMIT_WINDOW_SECS Rate limit window seconds (default: 60)\n\nFor more configuration options, see https://crates.io/crates/wisegate"
)]
pub struct Args {
#[arg(
long,
short = 'b',
help = "Bind address for listening and forwarding",
value_name = "ADDRESS",
default_value = "0.0.0.0"
)]
pub bind: String,
#[arg(
long,
short = 'l',
help = "Listen port for incoming connections",
value_name = "PORT",
default_value = "8080"
)]
pub listen: u16,
#[arg(
long,
short = 'f',
help = "Destination port for forwarded requests",
value_name = "PORT",
default_value = "9000"
)]
pub forward: u16,
#[arg(
long,
short = 'v',
help = "Show detailed configuration and startup information"
)]
pub verbose: bool,
#[arg(
long,
short = 'q',
help = "Suppress configuration output, show only essential messages",
conflicts_with = "verbose"
)]
pub quiet: bool,
#[arg(long, help = "Output logs in JSON format for structured logging")]
pub json_logs: bool,
}
impl Args {
pub fn validate(&self) -> Result<IpAddr, String> {
if self.listen == self.forward {
return Err("Listen and forward ports cannot be the same".to_string());
}
if self.listen == 0 || self.forward == 0 {
return Err("Ports must be greater than 0".to_string());
}
self.bind
.parse::<IpAddr>()
.map_err(|_| format!("Invalid bind address: '{}'", self.bind))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_validate_valid_configuration() {
let args = Args::try_parse_from(["wisegate", "-l", "8080", "-f", "9000"]).unwrap();
let result = args.validate();
assert!(result.is_ok());
assert_eq!(result.unwrap().to_string(), "0.0.0.0");
}
#[test]
fn test_validate_same_ports_error() {
let args = Args::try_parse_from(["wisegate", "-l", "8080", "-f", "8080"]).unwrap();
let result = args.validate();
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Listen and forward ports cannot be the same"
);
}
#[test]
fn test_validate_listen_port_zero_error() {
let args = Args::try_parse_from(["wisegate", "-l", "0", "-f", "9000"]).unwrap();
let result = args.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Ports must be greater than 0");
}
#[test]
fn test_validate_forward_port_zero_error() {
let args = Args::try_parse_from(["wisegate", "-l", "8080", "-f", "0"]).unwrap();
let result = args.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Ports must be greater than 0");
}
#[test]
fn test_validate_both_ports_zero_error() {
let args = Args::try_parse_from(["wisegate", "-l", "0", "-f", "0"]).unwrap();
let result = args.validate();
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Listen and forward ports cannot be the same"
);
}
#[test]
fn test_validate_invalid_bind_address() {
let args = Args::try_parse_from(["wisegate", "-l", "8080", "-f", "9000", "-b", "invalid"])
.unwrap();
let result = args.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid bind address"));
}
#[test]
fn test_validate_valid_ipv4_bind() {
let args =
Args::try_parse_from(["wisegate", "-l", "8080", "-f", "9000", "-b", "127.0.0.1"])
.unwrap();
let result = args.validate();
assert!(result.is_ok());
assert_eq!(result.unwrap().to_string(), "127.0.0.1");
}
#[test]
fn test_validate_valid_ipv6_bind() {
let args =
Args::try_parse_from(["wisegate", "-l", "8080", "-f", "9000", "-b", "::1"]).unwrap();
let result = args.validate();
assert!(result.is_ok());
assert_eq!(result.unwrap().to_string(), "::1");
}
#[test]
fn test_default_values() {
let args = Args::try_parse_from(["wisegate"]).unwrap();
assert_eq!(args.bind, "0.0.0.0");
assert_eq!(args.listen, 8080);
assert_eq!(args.forward, 9000);
assert!(!args.verbose);
assert!(!args.quiet);
assert!(!args.json_logs);
}
#[test]
fn test_verbose_flag() {
let args = Args::try_parse_from(["wisegate", "--verbose"]).unwrap();
assert!(args.verbose);
assert!(!args.quiet);
}
#[test]
fn test_quiet_flag() {
let args = Args::try_parse_from(["wisegate", "--quiet"]).unwrap();
assert!(!args.verbose);
assert!(args.quiet);
}
#[test]
fn test_json_logs_flag() {
let args = Args::try_parse_from(["wisegate", "--json-logs"]).unwrap();
assert!(args.json_logs);
}
#[test]
fn test_verbose_and_quiet_conflict() {
let result = Args::try_parse_from(["wisegate", "--verbose", "--quiet"]);
assert!(result.is_err());
}
#[test]
fn test_short_flags() {
let args = Args::try_parse_from([
"wisegate",
"-l",
"3000",
"-f",
"4000",
"-b",
"127.0.0.1",
"-v",
])
.unwrap();
assert_eq!(args.listen, 3000);
assert_eq!(args.forward, 4000);
assert_eq!(args.bind, "127.0.0.1");
assert!(args.verbose);
}
#[test]
fn test_max_port_value() {
let args = Args::try_parse_from(["wisegate", "-l", "65535", "-f", "65534"]).unwrap();
let result = args.validate();
assert!(result.is_ok());
}
#[test]
fn test_port_1() {
let args = Args::try_parse_from(["wisegate", "-l", "1", "-f", "2"]).unwrap();
let result = args.validate();
assert!(result.is_ok());
}
}