use anyhow::{anyhow, Context, Result}; use std::{ env, fs, net::SocketAddr, path::{Path, PathBuf}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mode { Local, Coordinator, Worker, } #[derive(Debug, Clone)] pub struct Config { pub mode: Mode, pub cidrs: Vec, pub port: u16, pub timeout_ms: u64, pub concurrency: usize, pub chunk_size: usize, pub database_url: String, pub coordinator_bind: SocketAddr, pub coordinator_addr: SocketAddr, pub worker_name: String, } impl Config { pub fn from_env_and_args() -> Result { let mut args = env::args().skip(1).collect::>(); let mode = parse_mode(&mut args)?; let cli = CliArgs::from_args(&args)?; let cidrs = load_cidrs(cli.cidr_file.as_deref(), cli.cidrs.as_deref())?; Ok(Self { mode, cidrs, port: cli .port .or_else(|| parse_u16("SCAN_PORT", 554).ok()) .unwrap_or(554), timeout_ms: cli .timeout_ms .or_else(|| parse_u64("SCAN_TIMEOUT_MS", 1200).ok()) .unwrap_or(1200), concurrency: cli .concurrency .or_else(|| parse_usize("SCAN_CONCURRENCY", default_concurrency()).ok()) .unwrap_or_else(default_concurrency), chunk_size: cli .chunk_size .or_else(|| parse_usize("SCAN_CHUNK_SIZE", default_chunk_size()).ok()) .unwrap_or_else(default_chunk_size), database_url: cli.database_url.unwrap_or_else(database_url_from_env), coordinator_bind: cli .coordinator_bind .or_else(|| parse_socket_addr("COORDINATOR_BIND", "0.0.0.0:666").ok()) .unwrap_or_else(|| "0.0.0.0:666".parse().expect("valid default bind address")), coordinator_addr: cli .coordinator_addr .or_else(|| parse_socket_addr("COORDINATOR_ADDR", "127.0.0.1:666").ok()) .unwrap_or_else(|| { "127.0.0.1:666" .parse() .expect("valid default coordinator address") }), worker_name: cli.worker_name.unwrap_or_else(default_worker_name), }) } } #[derive(Debug, Default)] struct CliArgs { cidrs: Option, cidr_file: Option, database_url: Option, coordinator_bind: Option, coordinator_addr: Option, worker_name: Option, port: Option, timeout_ms: Option, concurrency: Option, chunk_size: Option, } impl CliArgs { fn from_args(args: &[String]) -> Result { let mut cli = Self::default(); let mut index = 0; while index < args.len() { let arg = &args[index]; match arg.as_str() { "--cidrs" => { cli.cidrs = Some(next_value(args, &mut index, "--cidrs")?); } "--cidr-file" => { cli.cidr_file = Some(PathBuf::from(next_value(args, &mut index, "--cidr-file")?)); } "--database" | "--database-url" => { cli.database_url = Some(next_value(args, &mut index, arg)?); } "--bind" => { cli.coordinator_bind = Some(parse_socket_addr_literal(&next_value( args, &mut index, "--bind", )?)?); } "--connect" => { cli.coordinator_addr = Some(parse_socket_addr_literal(&next_value( args, &mut index, "--connect", )?)?); } "--name" => { cli.worker_name = Some(next_value(args, &mut index, "--name")?); } "--port" => { cli.port = Some(parse_u16_literal(&next_value(args, &mut index, "--port")?)?); } "--timeout-ms" => { cli.timeout_ms = Some(parse_u64_literal(&next_value( args, &mut index, "--timeout-ms", )?)?); } "--concurrency" => { cli.concurrency = Some(parse_usize_literal(&next_value( args, &mut index, "--concurrency", )?)?); } "--chunk-size" => { cli.chunk_size = Some(parse_usize_literal(&next_value( args, &mut index, "--chunk-size", )?)?); } _ if arg.starts_with("--") => { return Err(anyhow!("unknown argument: {arg}")); } _ => { return Err(anyhow!("unexpected positional argument: {arg}")); } } index += 1; } Ok(cli) } } fn parse_mode(args: &mut Vec) -> Result { if let Some(first) = args.first() { if !first.starts_with('-') { let mode = parse_mode_literal(first)?; args.remove(0); return Ok(mode); } } match env::var("SCAN_MODE") { Ok(value) => parse_mode_literal(value.trim()), Err(_) => Ok(Mode::Local), } } fn parse_mode_literal(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { "local" | "scan" => Ok(Mode::Local), "coordinator" | "coord" | "server" => Ok(Mode::Coordinator), "worker" => Ok(Mode::Worker), other => Err(anyhow!( "modo inválido: {other}. Use local, coordinator ou worker" )), } } fn load_cidrs(file: Option<&Path>, inline: Option<&str>) -> Result> { if let Some(inline) = inline { let cidrs = parse_cidr_list(inline); if !cidrs.is_empty() { return Ok(cidrs); } } if let Some(file) = file { let content = fs::read_to_string(file) .with_context(|| format!("failed to read CIDR file {}", file.display()))?; let cidrs = parse_cidr_list(&content); if !cidrs.is_empty() { return Ok(cidrs); } return Err(anyhow!("arquivo de CIDRs vazio: {}", file.display())); } if let Ok(path) = env::var("SCAN_CIDR_FILE") { let path = path.trim(); if !path.is_empty() { let content = fs::read_to_string(path) .with_context(|| format!("failed to read CIDR file {path}"))?; let cidrs = parse_cidr_list(&content); if !cidrs.is_empty() { return Ok(cidrs); } return Err(anyhow!("arquivo de CIDRs vazio: {path}")); } } if let Ok(value) = env::var("SCAN_CIDRS") { let value = value.trim(); if !value.is_empty() { let cidrs = parse_cidr_list(value); if !cidrs.is_empty() { return Ok(cidrs); } } } if let Ok(value) = env::var("SCAN_CIDR") { let value = value.trim(); if !value.is_empty() { let cidrs = parse_cidr_list(value); if !cidrs.is_empty() { return Ok(cidrs); } } } Ok(Vec::new()) } fn parse_cidr_list(input: &str) -> Vec { input .lines() .flat_map(|line| line.split('#').next()) .flat_map(|line| line.split(|c: char| c == ',' || c.is_whitespace())) .map(str::trim) .filter(|entry| !entry.is_empty()) .map(ToString::to_string) .collect() } fn database_url_from_env() -> String { let value = read_env("DATABASE_URL", ""); if value.is_empty() { "postgres://postgres:postgres@127.0.0.1:5432/camfinder".to_string() } else { value } } fn default_worker_name() -> String { env::var("WORKER_NAME") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| { let host = env::var("HOSTNAME").unwrap_or_else(|_| "worker".to_string()); format!("{}-{}", host.trim(), std::process::id()) }) } fn read_env(name: &str, fallback: &str) -> String { env::var(name) .map(|value| value.trim().to_string()) .unwrap_or_else(|_| fallback.to_string()) } fn parse_socket_addr(name: &str, fallback: &str) -> Result { parse_socket_addr_literal(&read_env(name, fallback)) } fn parse_socket_addr_literal(value: &str) -> Result { value .trim() .parse() .map_err(|_| anyhow!("invalid socket address: {value}")) } fn parse_u16(name: &str, fallback: u16) -> Result { parse_number(name, fallback) } fn parse_u64(name: &str, fallback: u64) -> Result { parse_number(name, fallback) } fn parse_usize(name: &str, fallback: usize) -> Result { parse_number(name, fallback) } fn parse_u16_literal(value: &str) -> Result { value .trim() .parse() .map_err(|_| anyhow!("invalid u16 value: {value}")) } fn parse_u64_literal(value: &str) -> Result { value .trim() .parse() .map_err(|_| anyhow!("invalid u64 value: {value}")) } fn parse_usize_literal(value: &str) -> Result { value .trim() .parse() .map_err(|_| anyhow!("invalid usize value: {value}")) } fn next_value(args: &[String], index: &mut usize, flag: &str) -> Result { let value_index = *index + 1; let value = args .get(value_index) .ok_or_else(|| anyhow!("missing value for {flag}"))? .clone(); *index = value_index; Ok(value) } fn default_concurrency() -> usize { let cpus = std::thread::available_parallelism() .map(|n| n.get()) .unwrap_or(4); (cpus.saturating_mul(2)).clamp(4, 64) } fn default_chunk_size() -> usize { default_concurrency().saturating_mul(2).clamp(32, 256) } fn parse_number(name: &str, fallback: T) -> Result where T: std::str::FromStr + Copy, { match env::var(name) { Ok(value) => value .trim() .parse::() .map_err(|_| anyhow!("invalid value for {name}: {value}")), Err(_) => Ok(fallback), } }