mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 01:15:14 +08:00
feat(official-sync): 支持本地代理下载官方资源
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use bat_adapters::official::yostar_jp::PatchPlatform;
|
||||
use bat_infrastructure::{
|
||||
lexical_absolute, open_append_file, read_file_no_symlink, read_version_state,
|
||||
validate_output_root, validate_runtime_state_dir, write_file_atomic,
|
||||
OfficialFailedVersionRecord, OfficialServerInfoSource, OfficialUpdateConfig,
|
||||
OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService, OfficialUpdateStatus,
|
||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PRIVATE_FILE_MODE,
|
||||
resolve_curl_proxy, validate_output_root, validate_runtime_state_dir, write_file_atomic,
|
||||
CurlProxyConfig, CurlProxyMode, OfficialFailedVersionRecord, OfficialServerInfoSource,
|
||||
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
|
||||
OfficialUpdateStatus, OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState,
|
||||
PRIVATE_FILE_MODE,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
@@ -180,6 +181,7 @@ struct CliOptions {
|
||||
state_dir: PathBuf,
|
||||
output_explicit: bool,
|
||||
sync_option_explicit: bool,
|
||||
proxy_option_explicit: bool,
|
||||
interval: Duration,
|
||||
error_retry_interval: Duration,
|
||||
quiet_up_to_date: bool,
|
||||
@@ -201,6 +203,7 @@ impl Default for CliOptions {
|
||||
state_dir: PathBuf::from(DEFAULT_DAEMON_STATE_DIR),
|
||||
output_explicit: false,
|
||||
sync_option_explicit: false,
|
||||
proxy_option_explicit: false,
|
||||
interval: Duration::from_secs(DEFAULT_WATCH_INTERVAL_SECONDS),
|
||||
error_retry_interval: Duration::from_secs(DEFAULT_ERROR_RETRY_SECONDS),
|
||||
quiet_up_to_date: false,
|
||||
@@ -1686,6 +1689,7 @@ struct DaemonControlReport {
|
||||
fn run_daemon_restart(options: &CliOptions, command_name: &'static str) -> anyhow::Result<()> {
|
||||
let has_explicit_options = options.sync_option_explicit
|
||||
|| options.output_explicit
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config);
|
||||
if command_name == "reload" && !has_explicit_options && daemon_rpc_available(&options.state_dir)
|
||||
{
|
||||
@@ -1839,6 +1843,7 @@ fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bo
|
||||
&& !options.daemon
|
||||
&& !options.daemon_child
|
||||
&& !options.output_explicit
|
||||
&& !options.proxy_option_explicit
|
||||
&& options.config.server_info_source.is_none()
|
||||
&& options.config.connection_group.is_none()
|
||||
&& options.config.app_version.is_none()
|
||||
@@ -1847,6 +1852,7 @@ fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bo
|
||||
&& options.config.output_root == defaults.output_root
|
||||
&& options.config.snapshot_path.is_none()
|
||||
&& options.config.curl_command == defaults.curl_command
|
||||
&& options.config.curl_proxy == defaults.curl_proxy
|
||||
&& options.config.unzip_command == defaults.unzip_command
|
||||
&& !options.config.dry_run
|
||||
&& !options.config.plan
|
||||
@@ -2459,6 +2465,7 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result<bool> {
|
||||
&verified_resource_root,
|
||||
&config.curl_command,
|
||||
)
|
||||
.with_proxy_config(config.curl_proxy.clone())
|
||||
.verify_local_download_manifest()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let failures = verification
|
||||
@@ -2607,6 +2614,7 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
||||
"后台状态目录安全边界通过",
|
||||
),
|
||||
command_check("curl", &options.config.curl_command),
|
||||
proxy_check(&options.config.curl_proxy),
|
||||
command_check("unzip", &options.config.unzip_command),
|
||||
];
|
||||
|
||||
@@ -2814,6 +2822,14 @@ fn command_check(name: &'static str, command: &Path) -> DoctorCheck {
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_check(config: &CurlProxyConfig) -> DoctorCheck {
|
||||
DoctorCheck {
|
||||
name: "proxy",
|
||||
ok: true,
|
||||
message: resolve_curl_proxy(config).human_summary(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CleanStableReport {
|
||||
command: &'static str,
|
||||
@@ -3162,6 +3178,20 @@ fn daemon_child_args(options: &CliOptions) -> Vec<String> {
|
||||
}
|
||||
args.push("--curl".to_string());
|
||||
args.push(config.curl_command.to_string_lossy().to_string());
|
||||
match config.curl_proxy.mode() {
|
||||
CurlProxyMode::Auto if options.proxy_option_explicit => {
|
||||
args.push("--proxy".to_string());
|
||||
args.push("auto".to_string());
|
||||
}
|
||||
CurlProxyMode::Auto => {}
|
||||
CurlProxyMode::Disabled => {
|
||||
args.push("--no-proxy".to_string());
|
||||
}
|
||||
CurlProxyMode::Url(url) => {
|
||||
args.push("--proxy".to_string());
|
||||
args.push(url.clone());
|
||||
}
|
||||
}
|
||||
args.push("--unzip".to_string());
|
||||
args.push(config.unzip_command.to_string_lossy().to_string());
|
||||
if config.force {
|
||||
@@ -3712,6 +3742,15 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
|
||||
"--curl" => {
|
||||
options.config.curl_command = PathBuf::from(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
"--proxy" => {
|
||||
options.config.curl_proxy =
|
||||
parse_proxy_config(&next_option_value(&mut args, &flag)?)?;
|
||||
options.proxy_option_explicit = true;
|
||||
}
|
||||
"--no-proxy" => {
|
||||
options.config.curl_proxy = CurlProxyConfig::disabled();
|
||||
options.proxy_option_explicit = true;
|
||||
}
|
||||
"--unzip" => {
|
||||
options.config.unzip_command = PathBuf::from(next_option_value(&mut args, &flag)?);
|
||||
}
|
||||
@@ -3829,6 +3868,7 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
|
||||
CliCommand::Status | CliCommand::Stop | CliCommand::Logs => {
|
||||
if options.sync_option_explicit
|
||||
|| options.output_explicit
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -3841,7 +3881,7 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
|
||||
CliCommand::Doctor | CliCommand::CleanStable => {
|
||||
if options.sync_option_explicit {
|
||||
return Err(anyhow::anyhow!(
|
||||
"doctor/clean-stable 只接受 --output、--state-dir、--curl 和 --unzip 等诊断参数"
|
||||
"doctor/clean-stable 只接受 --output、--state-dir、--curl、--proxy 和 --unzip 等诊断参数"
|
||||
));
|
||||
}
|
||||
options.progress = false;
|
||||
@@ -3865,6 +3905,7 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
|
||||
CliCommand::Restart | CliCommand::Reload => {
|
||||
if (options.sync_option_explicit
|
||||
|| options.output_explicit
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config))
|
||||
&& !options.config.auto_discover
|
||||
&& options.config.server_info_source.is_none()
|
||||
@@ -3932,7 +3973,10 @@ fn ensure_command_not_set(command: CliCommand, next: &str) -> anyhow::Result<()>
|
||||
}
|
||||
|
||||
fn tools_are_non_default(config: &OfficialUpdateConfig) -> bool {
|
||||
config.curl_command != Path::new("curl") || config.unzip_command != Path::new("unzip")
|
||||
let defaults = OfficialUpdateConfig::default();
|
||||
config.curl_command != defaults.curl_command
|
||||
|| config.curl_proxy != defaults.curl_proxy
|
||||
|| config.unzip_command != defaults.unzip_command
|
||||
}
|
||||
|
||||
fn next_option_value(
|
||||
@@ -3943,6 +3987,19 @@ fn next_option_value(
|
||||
.ok_or_else(|| anyhow::anyhow!("{flag} 缺少参数值"))
|
||||
}
|
||||
|
||||
fn parse_proxy_config(value: &str) -> anyhow::Result<CurlProxyConfig> {
|
||||
let normalized = value.trim();
|
||||
if normalized.is_empty() {
|
||||
return Err(anyhow::anyhow!("--proxy 不能为空"));
|
||||
}
|
||||
|
||||
match normalized.to_ascii_lowercase().as_str() {
|
||||
"auto" | "env" => Ok(CurlProxyConfig::auto()),
|
||||
"none" | "direct" | "off" | "disabled" => Ok(CurlProxyConfig::disabled()),
|
||||
_ => Ok(CurlProxyConfig::url(normalized.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_usage(binary: &str) {
|
||||
eprintln!("BlueArchiveToolkit official resource sync");
|
||||
eprintln!();
|
||||
@@ -3989,6 +4046,8 @@ fn print_usage(binary: &str) {
|
||||
);
|
||||
eprintln!(" --snapshot <PATH> Override snapshot path (default: <output>/current/official-sync-snapshot.json)");
|
||||
eprintln!(" --curl <PATH> curl executable (default: curl)");
|
||||
eprintln!(" --proxy <URL|auto|none> curl proxy override (default: auto from env)");
|
||||
eprintln!(" --no-proxy Force direct curl connections");
|
||||
eprintln!(" --unzip <PATH> unzip executable (default: unzip)");
|
||||
eprintln!(" --dry-run Do not write sync state");
|
||||
eprintln!(" --plan Include planned URLs in dry-run");
|
||||
@@ -4157,6 +4216,26 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_proxy_options() {
|
||||
let options = parse(&["bat", "--proxy", "http://127.0.0.1:7890"]).unwrap();
|
||||
assert!(options.proxy_option_explicit);
|
||||
assert_eq!(
|
||||
options.config.curl_proxy.mode(),
|
||||
&CurlProxyMode::Url("http://127.0.0.1:7890".to_string())
|
||||
);
|
||||
|
||||
let options = parse(&["bat", "--proxy", "none"]).unwrap();
|
||||
assert_eq!(options.config.curl_proxy.mode(), &CurlProxyMode::Disabled);
|
||||
|
||||
let options = parse(&["bat", "--no-proxy"]).unwrap();
|
||||
assert_eq!(options.config.curl_proxy.mode(), &CurlProxyMode::Disabled);
|
||||
|
||||
let options = parse(&["bat", "--proxy", "auto"]).unwrap();
|
||||
assert!(options.proxy_option_explicit);
|
||||
assert_eq!(options.config.curl_proxy.mode(), &CurlProxyMode::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_watch_defaults_to_one_hour_and_quiet_up_to_date() {
|
||||
let options = parse(&["bat", "--auto-discover", "--watch", "--interval", "30m"]).unwrap();
|
||||
@@ -4285,6 +4364,8 @@ mod tests {
|
||||
"/tmp/daemon-state",
|
||||
"--curl",
|
||||
"/usr/bin/curl",
|
||||
"--proxy",
|
||||
"http://127.0.0.1:7890",
|
||||
"--unzip",
|
||||
"/usr/bin/unzip",
|
||||
"--interval",
|
||||
@@ -4309,6 +4390,9 @@ mod tests {
|
||||
assert!(args
|
||||
.windows(2)
|
||||
.any(|pair| pair == ["--platforms", "Windows,Android"]));
|
||||
assert!(args
|
||||
.windows(2)
|
||||
.any(|pair| pair == ["--proxy", "http://127.0.0.1:7890"]));
|
||||
assert!(args.windows(2).any(|pair| pair == ["--interval", "30m"]));
|
||||
assert!(args.windows(2).any(|pair| pair == ["--error-retry", "45s"]));
|
||||
assert!(args.contains(&"--quiet-up-to-date".to_string()));
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ pub mod resources;
|
||||
mod zip_validation;
|
||||
|
||||
pub use cas::FileSystemCasRepository;
|
||||
pub use curl_transfer::{
|
||||
redact_proxy_url, resolve_curl_proxy, CurlProxyConfig, CurlProxyMode, ResolvedCurlProxy,
|
||||
};
|
||||
pub use import::{
|
||||
BundleSource, ImportedResource, ResourceImportCategory, ResourceImportReport,
|
||||
ResourceImportService,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Official JP resource download execution.
|
||||
|
||||
use crate::curl_transfer::{run_curl_with_retry, CurlRetryError};
|
||||
use crate::curl_transfer::{run_curl_with_retry_with_proxy, CurlProxyConfig, CurlRetryError};
|
||||
use crate::official_pull::OfficialResourcePullPlan;
|
||||
use crate::path_security::{
|
||||
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target,
|
||||
@@ -441,6 +441,7 @@ impl OfficialLocalResourceState {
|
||||
pub struct OfficialResourcePullService {
|
||||
output_root: PathBuf,
|
||||
curl_command: PathBuf,
|
||||
curl_proxy: CurlProxyConfig,
|
||||
retry_attempts: usize,
|
||||
}
|
||||
|
||||
@@ -458,10 +459,17 @@ impl OfficialResourcePullService {
|
||||
Self {
|
||||
output_root: output_root.into(),
|
||||
curl_command: curl_command.into(),
|
||||
curl_proxy: CurlProxyConfig::default(),
|
||||
retry_attempts: DEFAULT_RETRY_ATTEMPTS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the proxy configuration for every `curl` transfer.
|
||||
pub fn with_proxy_config(mut self, curl_proxy: CurlProxyConfig) -> Self {
|
||||
self.curl_proxy = curl_proxy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the number of attempts for each `curl` transfer.
|
||||
pub fn with_retry_attempts(mut self, retry_attempts: usize) -> Self {
|
||||
self.retry_attempts = retry_attempts.max(1);
|
||||
@@ -709,8 +717,13 @@ impl OfficialResourcePullService {
|
||||
return Err(format!("拒绝拉取非官方 URL:{url}"));
|
||||
}
|
||||
|
||||
let output =
|
||||
run_curl_with_retry(&self.curl_command, url, None, self.retry_attempts, || {
|
||||
let output = run_curl_with_retry_with_proxy(
|
||||
&self.curl_command,
|
||||
url,
|
||||
None,
|
||||
self.retry_attempts,
|
||||
&self.curl_proxy,
|
||||
|| {
|
||||
let mut command = Command::new(&self.curl_command);
|
||||
command
|
||||
.arg("--fail")
|
||||
@@ -720,8 +733,9 @@ impl OfficialResourcePullService {
|
||||
.arg("--url")
|
||||
.arg(url);
|
||||
command
|
||||
})
|
||||
.map_err(|error| format!("curl 拉取失败 {url}:{error}"))?;
|
||||
},
|
||||
)
|
||||
.map_err(|error| format!("curl 拉取失败 {url}:{error}"))?;
|
||||
|
||||
Ok(output.stdout)
|
||||
}
|
||||
@@ -819,11 +833,12 @@ impl OfficialResourcePullService {
|
||||
destination: &Path,
|
||||
resume: bool,
|
||||
) -> Result<(), CurlRetryError> {
|
||||
run_curl_with_retry(
|
||||
run_curl_with_retry_with_proxy(
|
||||
&self.curl_command,
|
||||
url,
|
||||
Some(destination),
|
||||
self.retry_attempts,
|
||||
&self.curl_proxy,
|
||||
|| {
|
||||
let mut command = Command::new(&self.curl_command);
|
||||
command
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! artifacts into a temporary directory, extracts only the data needed to read
|
||||
//! `GameMainConfig`, and does not install or execute the official launcher.
|
||||
|
||||
use crate::curl_transfer::run_curl_with_retry;
|
||||
use crate::curl_transfer::{run_curl_with_retry_with_proxy, CurlProxyConfig};
|
||||
use crate::official_launcher::launcher_package_url;
|
||||
use crate::official_launcher::OfficialLauncherBootstrapService;
|
||||
use crate::official_launcher::{
|
||||
@@ -49,6 +49,7 @@ pub struct OfficialGameMainConfigBootstrap {
|
||||
pub struct OfficialGameMainConfigBootstrapService {
|
||||
launcher: OfficialLauncherBootstrapService,
|
||||
curl_command: PathBuf,
|
||||
curl_proxy: CurlProxyConfig,
|
||||
unzip_command: PathBuf,
|
||||
}
|
||||
|
||||
@@ -73,10 +74,18 @@ impl OfficialGameMainConfigBootstrapService {
|
||||
Self {
|
||||
launcher,
|
||||
curl_command,
|
||||
curl_proxy: CurlProxyConfig::default(),
|
||||
unzip_command,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the proxy configuration for launcher and package `curl` transfers.
|
||||
pub fn with_proxy_config(mut self, curl_proxy: CurlProxyConfig) -> Self {
|
||||
self.launcher = self.launcher.with_proxy_config(curl_proxy.clone());
|
||||
self.curl_proxy = curl_proxy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fetches the latest official game ZIP by re-querying the official
|
||||
/// launcher API, extracts `resources.assets`, and decrypts `GameMainConfig`.
|
||||
pub fn fetch_bootstrap(&self) -> Result<OfficialGameMainConfigBootstrap, String> {
|
||||
@@ -116,11 +125,12 @@ impl OfficialGameMainConfigBootstrapService {
|
||||
}
|
||||
|
||||
fn download_file(&self, url: &str, destination: &Path) -> Result<(), String> {
|
||||
run_curl_with_retry(
|
||||
run_curl_with_retry_with_proxy(
|
||||
&self.curl_command,
|
||||
url,
|
||||
Some(destination),
|
||||
DEFAULT_BOOTSTRAP_DOWNLOAD_RETRY_ATTEMPTS,
|
||||
&self.curl_proxy,
|
||||
|| {
|
||||
let mut command = Command::new(&self.curl_command);
|
||||
command
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
//! discover the latest Windows client package and manifest, while resource
|
||||
//! updates still come from the game client's server-info flow.
|
||||
|
||||
use crate::curl_transfer::run_curl_with_retry as run_curl_command_with_retry;
|
||||
use crate::curl_transfer::{
|
||||
run_curl_with_retry_with_proxy as run_curl_command_with_retry, CurlProxyConfig,
|
||||
};
|
||||
use bat_adapters::official::launcher::{YostarJpLauncherManifestFile, YOSTAR_JP_GAME_TAG};
|
||||
use md5::{Digest, Md5};
|
||||
use serde::Deserialize;
|
||||
@@ -80,6 +82,7 @@ pub struct YostarJpLauncherRemoteManifest {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OfficialLauncherBootstrapService {
|
||||
curl_command: String,
|
||||
curl_proxy: CurlProxyConfig,
|
||||
launcher_version: String,
|
||||
retry_attempts: usize,
|
||||
}
|
||||
@@ -97,11 +100,18 @@ impl OfficialLauncherBootstrapService {
|
||||
) -> Self {
|
||||
Self {
|
||||
curl_command: curl_command.into(),
|
||||
curl_proxy: CurlProxyConfig::default(),
|
||||
launcher_version: launcher_version.into(),
|
||||
retry_attempts: DEFAULT_LAUNCHER_RETRY_ATTEMPTS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the proxy configuration for launcher `curl` transfers.
|
||||
pub fn with_proxy_config(mut self, curl_proxy: CurlProxyConfig) -> Self {
|
||||
self.curl_proxy = curl_proxy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the number of attempts for each launcher `curl` transfer.
|
||||
pub fn with_retry_attempts(mut self, retry_attempts: usize) -> Self {
|
||||
self.retry_attempts = retry_attempts.max(1);
|
||||
@@ -263,6 +273,7 @@ impl OfficialLauncherBootstrapService {
|
||||
url,
|
||||
None,
|
||||
self.retry_attempts,
|
||||
&self.curl_proxy,
|
||||
&mut build_command,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! callers: discover current official state, compare remote markers, audit the
|
||||
//! local manifest, repair/download when needed, then return a structured report.
|
||||
|
||||
use crate::curl_transfer::{resolve_curl_proxy, CurlProxyConfig};
|
||||
use crate::path_security::{
|
||||
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target,
|
||||
read_file_no_symlink, validate_output_root, write_file_atomic, STATE_FILE_MODE,
|
||||
@@ -79,6 +80,8 @@ pub struct OfficialUpdateConfig {
|
||||
pub snapshot_path: Option<PathBuf>,
|
||||
/// Curl command used by the current infrastructure downloader.
|
||||
pub curl_command: PathBuf,
|
||||
/// Proxy selection used by all official `curl` transfers.
|
||||
pub curl_proxy: CurlProxyConfig,
|
||||
/// Unzip command used when a metadata change requires GameMainConfig parsing.
|
||||
pub unzip_command: PathBuf,
|
||||
/// Dry run reports decisions and optional plan URLs without writing sync state.
|
||||
@@ -105,6 +108,7 @@ impl Default for OfficialUpdateConfig {
|
||||
output_root: PathBuf::from("./bat-resources"),
|
||||
snapshot_path: None,
|
||||
curl_command: PathBuf::from("curl"),
|
||||
curl_proxy: CurlProxyConfig::default(),
|
||||
unzip_command: PathBuf::from("unzip"),
|
||||
dry_run: false,
|
||||
plan: false,
|
||||
@@ -827,6 +831,10 @@ impl OfficialUpdateService {
|
||||
));
|
||||
check_shutdown_requested(&mut should_cancel)?;
|
||||
validate_update_paths(config).map_err(anyhow::Error::msg)?;
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"proxy",
|
||||
resolve_curl_proxy(&config.curl_proxy).human_summary(),
|
||||
));
|
||||
|
||||
let _lock = if config.dry_run {
|
||||
progress(OfficialUpdateProgress::new("lock", "试运行:跳过状态锁"));
|
||||
@@ -862,7 +870,8 @@ impl OfficialUpdateService {
|
||||
let fetcher = OfficialResourcePullService::with_curl_command(
|
||||
&active_resource_root,
|
||||
&config.curl_command,
|
||||
);
|
||||
)
|
||||
.with_proxy_config(config.curl_proxy.clone());
|
||||
let snapshot_path = snapshot_path_for(config, &active_resource_root);
|
||||
let bootstrap_cache_path = config.bootstrap_cache_path();
|
||||
|
||||
@@ -878,6 +887,7 @@ impl OfficialUpdateService {
|
||||
Some(resolve_bootstrap(
|
||||
&config.launcher_version,
|
||||
&config.curl_command,
|
||||
&config.curl_proxy,
|
||||
&config.unzip_command,
|
||||
&bootstrap_cache_path,
|
||||
!config.dry_run,
|
||||
@@ -1256,7 +1266,8 @@ impl OfficialUpdateService {
|
||||
let staging_fetcher = OfficialResourcePullService::with_curl_command(
|
||||
&publish_plan.staging_path,
|
||||
&config.curl_command,
|
||||
);
|
||||
)
|
||||
.with_proxy_config(config.curl_proxy.clone());
|
||||
report.staging_path = Some(publish_plan.staging_path.clone());
|
||||
report.snapshot_path = staging_snapshot_path.clone();
|
||||
report.download_manifest = staging_fetcher.download_manifest_path();
|
||||
@@ -2214,6 +2225,7 @@ fn game_main_config_snapshot(config: &YostarJpGameMainConfig) -> GameMainConfigS
|
||||
fn resolve_bootstrap(
|
||||
launcher_version: &str,
|
||||
curl_command: &Path,
|
||||
curl_proxy: &CurlProxyConfig,
|
||||
unzip_command: &Path,
|
||||
cache_path: &Path,
|
||||
write_cache: bool,
|
||||
@@ -2224,7 +2236,8 @@ fn resolve_bootstrap(
|
||||
let launcher = OfficialLauncherBootstrapService::with_curl_command(
|
||||
launcher_version,
|
||||
curl_command.to_string_lossy().to_string(),
|
||||
);
|
||||
)
|
||||
.with_proxy_config(curl_proxy.clone());
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"launcher",
|
||||
"正在拉取官方启动器游戏配置和远端 manifest",
|
||||
@@ -2272,7 +2285,8 @@ fn resolve_bootstrap(
|
||||
launcher_version,
|
||||
curl_command.to_path_buf(),
|
||||
unzip_command.to_path_buf(),
|
||||
);
|
||||
)
|
||||
.with_proxy_config(curl_proxy.clone());
|
||||
let bootstrap = bootstrapper.fetch_bootstrap().map_err(anyhow::Error::msg)?;
|
||||
check_shutdown_requested(should_cancel)?;
|
||||
let launcher_metadata = LauncherMetadataSnapshot {
|
||||
|
||||
Reference in New Issue
Block a user