Release v2.0.0
CI / Windows baseline (push) Canceled after 0s

This commit is contained in:
2026-09-10 20:59:52 +03:00
parent 9c987df6e9
commit efda8eb98f
142 changed files with 68308 additions and 9333 deletions
+5 -1
View File
@@ -48,7 +48,11 @@ impl ProxiFyreAdapter {
proxies.push(ProxiFyreProxy {
app_names,
socks5_proxy_endpoint: format!("{}:{}", target.host, target.port),
socks5_proxy_endpoint: if target.host.contains(':') {
format!("[{}]:{}", target.host, target.port)
} else {
format!("{}:{}", target.host, target.port)
},
supported_protocols: protocols_for_profile(profile),
});
}
+85 -117
View File
@@ -1,8 +1,8 @@
use crate::models::{LocalSingBoxConfig, SubscriptionCache, SubscriptionServer};
use crate::process::command_no_window;
use crate::process::run_fixed_process;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{env, fs, fs::OpenOptions, io::Write, path::Path};
use std::{env, fs, path::Path, time::Duration};
pub const SINGBOX_ADAPTER_ID: &str = "singbox";
pub const SINGBOX_OUTPUT_FILE: &str = "sing-box-config.json";
@@ -41,31 +41,24 @@ impl SingBoxAdapter {
where
C: SingBoxConfigChecker + ?Sized,
{
let selected_server = request
.config
.selected_server_id
.as_deref()
.and_then(|id| {
request
.subscription_cache
.servers
.iter()
.find(|server| server.id == id)
})
.or_else(|| {
let tag = request.config.selected_server_tag.as_deref()?;
request
.subscription_cache
.servers
.iter()
.find(|server| server.tag == tag)
})
.ok_or_else(|| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::MissingSelectedServer,
"Сервер Local sing-box не выбран или отсутствует в текущей подписке",
)
})?;
let selected_server = if let Some(id) = request.config.selected_server_id.as_deref() {
request
.subscription_cache
.servers
.iter()
.find(|server| server.id == id)
} else {
let mut matches = request.subscription_cache.servers.iter().filter(|server| {
Some(server.tag.as_str()) == request.config.selected_server_tag.as_deref()
});
matches.next().filter(|_| matches.next().is_none())
}
.ok_or_else(|| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::MissingSelectedServer,
"Сервер Local sing-box не выбран или отсутствует в текущей подписке",
)
})?;
let vpn_outbound = selected_outbound(
&request.subscription_cache.config,
selected_server,
@@ -213,70 +206,50 @@ impl SingBoxConfigChecker for SingBoxCommandChecker {
uuid::Uuid::new_v4().hyphenated()
));
{
let mut config_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&config_path)
.map_err(|error| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::CheckFailed,
format!(
"Не удалось создать временный конфиг sing-box '{}': {error}",
config_path.display()
),
)
})?;
let write_result = config_file.write_all(config_json.as_bytes());
drop(config_file);
if let Err(error) = write_result {
let _ = fs::remove_file(&config_path);
return Err(SingBoxConfigError::new(
SingBoxConfigErrorKind::CheckFailed,
format!(
"Не удалось записать временный конфиг sing-box '{}': {error}",
config_path.display()
),
));
struct TemporaryConfig(std::path::PathBuf);
impl Drop for TemporaryConfig {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
let output = command_no_window(binary_path)
.arg("check")
.arg("-c")
.arg(&config_path)
.output()
.map_err(|error| {
let _ = fs::remove_file(&config_path);
let _temporary = TemporaryConfig(config_path.clone());
crate::safe_fs::write_restricted_atomic(&config_path, config_json.as_bytes()).map_err(
|_| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::CheckFailed,
format!(
"Не удалось выполнить '{} check': {error}",
binary_path.display()
),
"Не удалось безопасно создать временный конфиг sing-box",
)
})?;
let _ = fs::remove_file(&config_path);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let message = command_message(&stdout, &stderr);
if !output.status.success() {
return Err(SingBoxConfigError::new(
},
)?;
// Checker output can contain credentials from the outbound. The bounded
// native process runner discards both streams instead of exposing them.
let status = run_fixed_process(
binary_path,
&[
"check".into(),
"-c".into(),
config_path.as_os_str().to_owned(),
],
Duration::from_secs(30),
)
.map_err(|error| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::CheckFailed,
format!("Проверка sing-box не прошла: {message}"),
));
if error.kind() == std::io::ErrorKind::TimedOut {
"Проверка sing-box превысила 30 секунд"
} else {
"Не удалось выполнить проверку sing-box"
},
)
})?;
if !status.success() {
return Err(SingBoxConfigError::new(SingBoxConfigErrorKind::CheckFailed,
"sing-box отклонил конфигурацию выбранного сервера. Обновите подписку или выберите другой сервер."));
}
Ok(SingBoxCheckResult {
checked: true,
success: true,
message: if message.is_empty() {
"Проверка sing-box прошла успешно".to_string()
} else {
message
},
message: "Проверка sing-box прошла успешно".to_string(),
})
}
}
@@ -295,32 +268,39 @@ fn selected_outbound(
"В cache подписки нет outbounds",
)
})?;
let outbound = outbounds
.iter()
.find(|outbound| {
let tag_matches = outbound
let outbound = if selected_server.id.starts_with("pw-") {
outbounds.iter().find(|outbound| {
crate::subscription::outbound_server_id(outbound) == selected_server.id
})
} else {
// Legacy endpoint IDs are readable only when they identify exactly one outbound.
let mut matches = outbounds.iter().filter(|outbound| {
outbound
.get("tag")
.and_then(Value::as_str)
.is_some_and(|tag| tag.trim() == selected_server.tag);
let server_matches = outbound
.get("server")
.and_then(Value::as_str)
.is_some_and(|server| server.eq_ignore_ascii_case(&selected_server.server));
let port_matches = outbound
.get("server_port")
.and_then(Value::as_u64)
.is_some_and(|port| port == u64::from(selected_server.server_port));
tag_matches && server_matches && port_matches
})
.ok_or_else(|| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::MissingSelectedOutbound,
format!(
"Outbound не найден: {} ({}:{})",
selected_server.tag, selected_server.server, selected_server.server_port
),
)
})?;
.is_some_and(|tag| {
crate::models::decode_percent_encoded_utf8(tag).trim() == selected_server.tag
})
&& outbound.get("type").and_then(Value::as_str)
== Some(selected_server.server_type.as_str())
&& outbound
.get("server")
.and_then(Value::as_str)
.is_some_and(|host| host.eq_ignore_ascii_case(&selected_server.server))
&& outbound.get("server_port").and_then(Value::as_u64)
== Some(u64::from(selected_server.server_port))
});
matches.next().filter(|_| matches.next().is_none())
}
.ok_or_else(|| {
SingBoxConfigError::new(
SingBoxConfigErrorKind::MissingSelectedOutbound,
format!(
"Outbound не найден: {} ({}:{})",
selected_server.tag, selected_server.server, selected_server.server_port
),
)
})?;
let outbound_type = outbound
.get("type")
.and_then(Value::as_str)
@@ -359,15 +339,3 @@ fn selected_outbound(
Ok(outbound)
}
fn command_message(stdout: &str, stderr: &str) -> String {
let stdout = stdout.trim();
let stderr = stderr.trim();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => String::new(),
(false, true) => stdout.to_string(),
(true, false) => stderr.to_string(),
(false, false) => format!("{stdout}\n{stderr}"),
}
}