diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e08030d..cd9c0d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,20 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Install rustfmt/clippy + run: rustup component add rustfmt clippy + - name: Cache Rust artifacts uses: Swatinem/rust-cache@v2 + - name: Check formatting (advisory) + continue-on-error: true + run: cargo fmt --all -- --check + + - name: Run clippy (advisory) + continue-on-error: true + run: cargo clippy --workspace --all-targets + - name: Run tests run: cargo test --workspace --all-targets diff --git a/README.md b/README.md index 14a9c9f..07b79a2 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ Usage: proxychains4 [OPTIONS] [ARGS...] Options: -f, --file Configuration file path --group Proxy group name (uses [ProxyList:]) + --list-groups List available proxy groups and exit + --check Validate config and print selected proxies, then exit -q, --quiet Quiet mode (suppress output) -v, --verbose Verbose mode (debug output) -h, --help Show help @@ -241,6 +243,25 @@ Group usage example: proxychains4 --group jp curl https://ifconfig.me ``` +Validation and discovery: +```bash +proxychains4 -f ./proxychains.conf --list-groups +proxychains4 -f ./proxychains.conf --group jp --check +``` + +--- + +## Testing & Quality + +Recommended checks before release: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets +cargo test --workspace --all-targets +cargo build --release --workspace +``` + ### Configuration File Locations The configuration file is searched in the following order: diff --git a/proxychains-bin/src/main.rs b/proxychains-bin/src/main.rs index 98e920c..f3d1664 100644 --- a/proxychains-bin/src/main.rs +++ b/proxychains-bin/src/main.rs @@ -39,8 +39,19 @@ struct Args { #[arg(long, value_name = "GROUP")] group: Option, + /// List available proxy groups from config and exit + #[arg(long)] + list_groups: bool, + + /// Validate config and print selected proxies without running command + #[arg(long)] + check: bool, + /// The command to run - #[arg(required = true, trailing_var_arg = true)] + #[arg( + required_unless_present_any = ["list_groups", "check"], + trailing_var_arg = true + )] command: Vec, } @@ -66,6 +77,16 @@ fn main() { .expect("Failed to set tracing subscriber"); } + if args.list_groups { + if let Err(e) = list_groups(&args) { + if !args.quiet { + eprintln!("proxychains: {}", e); + } + process::exit(1); + } + process::exit(0); + } + // Parse configuration let config = load_config(&args); @@ -95,6 +116,11 @@ fn main() { } } + if args.check { + print_check_summary(&config, &args); + process::exit(0); + } + // Execute the command with platform-specific injection match execute_command(&args, &config) { Ok(status) => { @@ -111,6 +137,10 @@ fn main() { /// Load configuration from file or environment fn load_config(args: &Args) -> Result { + build_parser(args).parse().map_err(|e| e.to_string()) +} + +fn build_parser(args: &Args) -> ConfigParser { let mut parser = ConfigParser::new(); if let Some(ref path) = args.config { @@ -121,7 +151,43 @@ fn load_config(args: &Args) -> Result { parser = parser.with_group(group.clone()); } - parser.parse().map_err(|e| e.to_string()) + parser +} + +fn list_groups(args: &Args) -> Result<(), String> { + let parser = build_parser(args); + let mut groups = parser.list_proxy_groups().map_err(|e| e.to_string())?; + groups.sort(); + if groups.is_empty() { + println!("No [ProxyList] groups found in config"); + return Ok(()); + } + for group in groups { + println!("{}", group); + } + Ok(()) +} + +fn print_check_summary(config: &Config, args: &Args) { + println!("Config check: OK"); + println!("Chain type: {}", config.chain_type); + println!("Proxy count: {}", config.proxy_count()); + println!("Proxy DNS: {}", config.proxy_dns); + println!( + "Selected group: {}", + args.group.as_deref().unwrap_or("default/all") + ); + for (idx, proxy) in config.proxies.iter().enumerate() { + let auth = if proxy.user.is_some() { "auth" } else { "no-auth" }; + println!( + " {}. {} {}:{} ({})", + idx + 1, + proxy.proxy_type, + proxy.ip, + proxy.port, + auth + ); + } } /// Set proxychains-specific environment variables @@ -350,4 +416,13 @@ mod tests { assert_eq!(args.group, Some("jp".to_string())); assert_eq!(args.command, vec!["wget", "http://example.com"]); } + + #[test] + fn test_args_list_groups_without_command() { + let args = Args::try_parse_from(["proxychains4", "--list-groups"]); + assert!(args.is_ok()); + let args = args.unwrap(); + assert!(args.list_groups); + assert!(args.command.is_empty()); + } } diff --git a/proxychains/src/config/parser.rs b/proxychains/src/config/parser.rs index 5cec4a7..bb45bf2 100644 --- a/proxychains/src/config/parser.rs +++ b/proxychains/src/config/parser.rs @@ -198,6 +198,53 @@ impl ConfigParser { Ok(config) } + /// List available proxy groups in the configuration file. + /// + /// Returns group names found in `[ProxyList]` and `[ProxyList:]` + /// sections. The unnamed default section is represented as `default`. + pub fn list_proxy_groups(&self) -> Result> { + let path = self + .find_config_file() + .ok_or_else(|| Error::Config("Could not find configuration file".to_string()))?; + self.list_proxy_groups_from_file(path) + } + + /// List available proxy groups from a specific file. + pub fn list_proxy_groups_from_file>(&self, path: P) -> Result> { + let path = path.as_ref(); + let file = fs::File::open(path)?; + let reader = BufReader::new(file); + let mut groups: Vec = Vec::new(); + + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + if !trimmed.starts_with('[') || !trimmed.ends_with(']') { + continue; + } + + let section = trimmed + .trim_start_matches('[') + .trim_end_matches(']') + .trim(); + if !section.to_lowercase().starts_with("proxylist") { + continue; + } + + let group_name = section + .split_once(':') + .map(|(_, g)| g.trim().to_lowercase()) + .filter(|g| !g.is_empty()) + .unwrap_or_else(|| "default".to_string()); + + if !groups.iter().any(|g| g == &group_name) { + groups.push(group_name); + } + } + + Ok(groups) + } + /// Parse a configuration line fn parse_config_line(&self, line: &str, config: &mut Config) -> Result<()> { let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect(); @@ -444,4 +491,30 @@ socks5 127.0.0.1 1080 let _ = fs::remove_file(path); } + + #[test] + fn test_list_proxy_groups() { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("proxychains_groups_{}.conf", ts)); + let content = r#" +[ProxyList] +socks5 127.0.0.1 1080 +[ProxyList:JP] +socks5 127.0.0.2 1080 +[ProxyList:us] +socks5 127.0.0.3 1080 +"#; + fs::write(&path, content).unwrap(); + + let mut groups = ConfigParser::new() + .list_proxy_groups_from_file(path.clone()) + .unwrap(); + groups.sort(); + assert_eq!(groups, vec!["default", "jp", "us"]); + + let _ = fs::remove_file(path); + } }