feat(official-sync): 支持本地代理下载官方资源

This commit is contained in:
2026-07-16 00:12:27 +08:00
parent 169822abaf
commit c123f6c0dc
10 changed files with 481 additions and 40 deletions
+273 -2
View File
@@ -1,6 +1,186 @@
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const PROXY_ENV_PRIORITY: [&str; 6] = [
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
"HTTP_PROXY",
"http_proxy",
];
const NO_PROXY_ENV_PRIORITY: [&str; 2] = ["NO_PROXY", "no_proxy"];
/// Proxy selection mode for official HTTP transfers executed through `curl`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CurlProxyMode {
/// Detect proxy settings from the current process environment.
Auto,
/// Force direct connections and ignore proxy environment variables.
Disabled,
/// Force a specific proxy URL.
Url(String),
}
/// Proxy configuration for official HTTP transfers executed through `curl`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurlProxyConfig {
mode: CurlProxyMode,
}
impl CurlProxyConfig {
/// Creates automatic proxy detection.
pub fn auto() -> Self {
Self {
mode: CurlProxyMode::Auto,
}
}
/// Creates a direct-connection configuration.
pub fn disabled() -> Self {
Self {
mode: CurlProxyMode::Disabled,
}
}
/// Creates a forced proxy configuration.
pub fn url(url: impl Into<String>) -> Self {
Self {
mode: CurlProxyMode::Url(url.into()),
}
}
/// Returns the proxy mode.
pub fn mode(&self) -> &CurlProxyMode {
&self.mode
}
}
impl Default for CurlProxyConfig {
fn default() -> Self {
Self::auto()
}
}
/// Resolved proxy settings for one `curl` command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedCurlProxy {
/// Whether a proxy URL will be passed to `curl`.
pub enabled: bool,
/// Whether direct mode was explicitly requested.
pub disabled: bool,
/// Proxy URL, when one is selected.
pub url: Option<String>,
/// Where this proxy decision came from.
pub source: String,
/// `NO_PROXY`/`no_proxy` value preserved for `curl`, when present.
pub no_proxy: Option<String>,
}
impl ResolvedCurlProxy {
/// Returns a human-readable summary with credentials redacted.
pub fn human_summary(&self) -> String {
if self.disabled {
return format!("代理已关闭;来源={}", self.source);
}
if let Some(url) = self.url.as_ref() {
let no_proxy = self
.no_proxy
.as_ref()
.map(|value| format!("no_proxy={value}"))
.unwrap_or_default();
return format!(
"使用代理 {};来源={}{}",
redact_proxy_url(url),
self.source,
no_proxy
);
}
format!("未检测到代理;来源={}", self.source)
}
}
/// Resolves the proxy setting that should be applied to `curl`.
pub fn resolve_curl_proxy(config: &CurlProxyConfig) -> ResolvedCurlProxy {
let pairs = proxy_env_pairs();
resolve_curl_proxy_from_pairs(config, &pairs)
}
/// Redacts user info from a proxy URL for diagnostics and logs.
pub fn redact_proxy_url(url: &str) -> String {
let Some((scheme, rest)) = url.split_once("://") else {
return url.to_string();
};
let Some(at_index) = rest.find('@') else {
return url.to_string();
};
format!("{scheme}://<redacted>@{}", &rest[at_index + 1..])
}
fn proxy_env_pairs() -> Vec<(String, String)> {
PROXY_ENV_PRIORITY
.iter()
.chain(NO_PROXY_ENV_PRIORITY.iter())
.filter_map(|name| {
env::var(name)
.ok()
.map(|value| ((*name).to_string(), value))
})
.collect()
}
fn resolve_curl_proxy_from_pairs(
config: &CurlProxyConfig,
pairs: &[(String, String)],
) -> ResolvedCurlProxy {
match config.mode() {
CurlProxyMode::Auto => {
let no_proxy = first_env_value(pairs, &NO_PROXY_ENV_PRIORITY).map(|(_, value)| value);
if let Some((name, url)) = first_env_value(pairs, &PROXY_ENV_PRIORITY) {
ResolvedCurlProxy {
enabled: true,
disabled: false,
url: Some(url),
source: name,
no_proxy,
}
} else {
ResolvedCurlProxy {
enabled: false,
disabled: false,
url: None,
source: "auto".to_string(),
no_proxy,
}
}
}
CurlProxyMode::Disabled => ResolvedCurlProxy {
enabled: false,
disabled: true,
url: None,
source: "--no-proxy".to_string(),
no_proxy: None,
},
CurlProxyMode::Url(url) => ResolvedCurlProxy {
enabled: true,
disabled: false,
url: Some(url.clone()),
source: "--proxy".to_string(),
no_proxy: first_env_value(pairs, &NO_PROXY_ENV_PRIORITY).map(|(_, value)| value),
},
}
}
fn first_env_value(pairs: &[(String, String)], priority: &[&str]) -> Option<(String, String)> {
priority.iter().find_map(|name| {
pairs
.iter()
.find(|(candidate, value)| candidate == name && !value.trim().is_empty())
.map(|(candidate, value)| (candidate.clone(), value.clone()))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CurlFailureKind {
HttpForbidden,
@@ -194,18 +374,22 @@ impl std::fmt::Display for CurlRetryError {
impl std::error::Error for CurlRetryError {}
pub(crate) fn run_curl_with_retry(
pub(crate) fn run_curl_with_retry_with_proxy(
curl_command: &Path,
url: &str,
destination: Option<&Path>,
attempts: usize,
proxy_config: &CurlProxyConfig,
mut build_command: impl FnMut() -> Command,
) -> Result<Output, CurlRetryError> {
let attempts = attempts.max(1);
let mut failures = Vec::new();
let proxy = resolve_curl_proxy(proxy_config);
for attempt in 1..=attempts {
let failure = match build_command().output() {
let mut command = build_command();
apply_curl_proxy(&mut command, &proxy);
let failure = match command.output() {
Ok(output) if output.status.success() => return Ok(output),
Ok(output) => CurlFailure::from_output(url, destination, &output),
Err(error) => CurlFailure::process_failed(url, destination, curl_command, error),
@@ -224,6 +408,31 @@ pub(crate) fn run_curl_with_retry(
Err(CurlRetryError { attempts: failures })
}
fn apply_curl_proxy(command: &mut Command, proxy: &ResolvedCurlProxy) {
if proxy.disabled {
remove_proxy_env(command);
command.arg("--noproxy").arg("*");
return;
}
if let Some(url) = proxy.url.as_ref() {
remove_proxy_env(command);
command.arg("--proxy").arg(url);
if let Some(no_proxy) = proxy.no_proxy.as_ref() {
command.arg("--noproxy").arg(no_proxy);
}
}
}
fn remove_proxy_env(command: &mut Command) {
for name in PROXY_ENV_PRIORITY
.iter()
.chain(NO_PROXY_ENV_PRIORITY.iter())
{
command.env_remove(name);
}
}
fn classify_failure(exit_code: Option<i32>, http_status: Option<u16>) -> CurlFailureKind {
if exit_code == Some(22) {
return match http_status {
@@ -305,4 +514,66 @@ mod tests {
assert_eq!(failure.kind, CurlFailureKind::HttpServerError);
assert!(failure.retryable());
}
#[test]
fn auto_proxy_prefers_https_and_preserves_no_proxy() {
let pairs = vec![
(
"HTTP_PROXY".to_string(),
"http://proxy-http:8080".to_string(),
),
(
"HTTPS_PROXY".to_string(),
"http://proxy-https:7890".to_string(),
),
("NO_PROXY".to_string(), "localhost,127.0.0.1".to_string()),
];
let resolved = resolve_curl_proxy_from_pairs(&CurlProxyConfig::auto(), &pairs);
assert!(resolved.enabled);
assert!(!resolved.disabled);
assert_eq!(resolved.url.as_deref(), Some("http://proxy-https:7890"));
assert_eq!(resolved.source, "HTTPS_PROXY");
assert_eq!(resolved.no_proxy.as_deref(), Some("localhost,127.0.0.1"));
}
#[test]
fn disabled_proxy_ignores_environment() {
let pairs = vec![(
"HTTPS_PROXY".to_string(),
"http://proxy-https:7890".to_string(),
)];
let resolved = resolve_curl_proxy_from_pairs(&CurlProxyConfig::disabled(), &pairs);
assert!(!resolved.enabled);
assert!(resolved.disabled);
assert!(resolved.url.is_none());
assert_eq!(resolved.source, "--no-proxy");
}
#[test]
fn explicit_proxy_wins_and_redacts_credentials() {
let pairs = vec![(
"HTTPS_PROXY".to_string(),
"http://environment:7890".to_string(),
)];
let resolved = resolve_curl_proxy_from_pairs(
&CurlProxyConfig::url("http://user:secret@127.0.0.1:7890"),
&pairs,
);
assert!(resolved.enabled);
assert_eq!(
resolved.url.as_deref(),
Some("http://user:secret@127.0.0.1:7890")
);
assert_eq!(resolved.source, "--proxy");
assert_eq!(
redact_proxy_url(resolved.url.as_deref().unwrap()),
"http://<redacted>@127.0.0.1:7890"
);
}
}