mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 05:25:14 +08:00
feat(official-sync): 支持本地代理下载官方资源
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user