@@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user