diff --git a/infrastructure/src/curl_transfer.rs b/infrastructure/src/curl_transfer.rs index c462737..ab2920b 100644 --- a/infrastructure/src/curl_transfer.rs +++ b/infrastructure/src/curl_transfer.rs @@ -423,8 +423,13 @@ fn apply_curl_proxy(command: &mut Command, proxy: &ResolvedCurlProxy) { } if let Some(url) = proxy.url.as_ref() { + // 通过环境变量而非 --proxy 参数把代理传给 curl:代理 URL 可能带凭据, + // 放进 argv 会出现在 curl 子进程的 /proc//cmdline(0444,世界可读)。 + // 先清除所有继承的代理变量,再设 ALL_PROXY/all_proxy(覆盖 http 与 https, + // 大小写两种写法都设,避免 curl 只读其中一种)。no_proxy 列表不含凭据,仍走参数。 remove_proxy_env(command); - command.arg("--proxy").arg(url); + command.env("ALL_PROXY", url); + command.env("all_proxy", url); if let Some(no_proxy) = proxy.no_proxy.as_ref() { command.arg("--noproxy").arg(no_proxy); } @@ -600,4 +605,72 @@ mod tests { "http://127.0.0.1:7890" ); } + + fn command_args(command: &Command) -> Vec { + command + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect() + } + + fn command_env(command: &Command, key: &str) -> Option> { + command + .get_envs() + .find(|(name, _)| *name == key) + .map(|(_, value)| value.map(|value| value.to_string_lossy().to_string())) + } + + #[test] + fn apply_curl_proxy_passes_url_via_env_not_argv() { + let proxy = ResolvedCurlProxy { + enabled: true, + disabled: false, + url: Some("http://user:secret@127.0.0.1:7897".to_string()), + source: "--proxy".to_string(), + no_proxy: Some("localhost,127.0.0.1".to_string()), + }; + let mut command = Command::new("curl"); + apply_curl_proxy(&mut command, &proxy); + + // 凭据不得出现在 curl 的 argv。 + let args = command_args(&command); + assert!(!args.iter().any(|arg| arg.contains("secret"))); + assert!(!args.iter().any(|arg| arg == "--proxy")); + // no_proxy 列表不含凭据,仍以参数传入。 + assert!(args + .windows(2) + .any(|pair| pair == ["--noproxy", "localhost,127.0.0.1"])); + + // 代理 URL 经 ALL_PROXY/all_proxy 环境变量下传。 + assert_eq!( + command_env(&command, "ALL_PROXY"), + Some(Some("http://user:secret@127.0.0.1:7897".to_string())) + ); + assert_eq!( + command_env(&command, "all_proxy"), + Some(Some("http://user:secret@127.0.0.1:7897".to_string())) + ); + // 继承的 scheme 专用代理变量被清除,避免覆盖 ALL_PROXY。 + assert_eq!(command_env(&command, "HTTPS_PROXY"), Some(None)); + assert_eq!(command_env(&command, "https_proxy"), Some(None)); + } + + #[test] + fn apply_curl_proxy_disabled_forces_direct() { + let proxy = ResolvedCurlProxy { + enabled: false, + disabled: true, + url: None, + source: "--no-proxy".to_string(), + no_proxy: None, + }; + let mut command = Command::new("curl"); + apply_curl_proxy(&mut command, &proxy); + + let args = command_args(&command); + assert!(args.windows(2).any(|pair| pair == ["--noproxy", "*"])); + // 直连模式清除所有继承的代理变量,且不设置任何代理。 + assert_eq!(command_env(&command, "ALL_PROXY"), Some(None)); + assert_eq!(command_env(&command, "HTTPS_PROXY"), Some(None)); + } }