Bump version and auto-generate sing-box HWIDs
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "proxywarden",
|
"name": "proxywarden",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "proxywarden",
|
"name": "proxywarden",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxywarden",
|
"name": "proxywarden",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Standalone Windows desktop proxy management app for ProxyWarden.",
|
"description": "Standalone Windows desktop proxy management app for ProxyWarden.",
|
||||||
|
|||||||
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
@@ -2314,7 +2314,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proxywarden"
|
name = "proxywarden"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
@@ -2324,6 +2324,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "proxywarden"
|
name = "proxywarden"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
description = "Standalone Windows desktop proxy management app for ProxyWarden."
|
description = "Standalone Windows desktop proxy management app for ProxyWarden."
|
||||||
authors = ["ProxyWarden"]
|
authors = ["ProxyWarden"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -20,3 +20,4 @@ tauri-plugin-dialog = "2.7.1"
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "socks"] }
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "socks"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|||||||
@@ -232,8 +232,6 @@ pub struct LocalSingBoxStatusResponse {
|
|||||||
pub struct LocalSingBoxConfigDto {
|
pub struct LocalSingBoxConfigDto {
|
||||||
pub subscription_display_url: Option<String>,
|
pub subscription_display_url: Option<String>,
|
||||||
pub has_subscription: bool,
|
pub has_subscription: bool,
|
||||||
pub device_hwid_display: Option<String>,
|
|
||||||
pub has_device_hwid: bool,
|
|
||||||
pub selected_server_tag: Option<String>,
|
pub selected_server_tag: Option<String>,
|
||||||
pub listen_host: String,
|
pub listen_host: String,
|
||||||
pub listen_port: u16,
|
pub listen_port: u16,
|
||||||
@@ -264,15 +262,6 @@ pub struct SubscriptionServerDto {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SaveSingBoxSubscriptionInputDto {
|
pub struct SaveSingBoxSubscriptionInputDto {
|
||||||
pub subscription_url: String,
|
pub subscription_url: String,
|
||||||
#[serde(default)]
|
|
||||||
pub device_hwid: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SaveSingBoxDeviceHwidInputDto {
|
|
||||||
#[serde(default)]
|
|
||||||
pub device_hwid: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -684,14 +673,6 @@ pub fn save_singbox_subscription(
|
|||||||
save_singbox_subscription_to_storage(&state.storage(), input, &SystemClock)
|
save_singbox_subscription_to_storage(&state.storage(), input, &SystemClock)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn save_singbox_device_hwid(
|
|
||||||
state: tauri::State<'_, CommandState>,
|
|
||||||
input: SaveSingBoxDeviceHwidInputDto,
|
|
||||||
) -> Result<LocalSingBoxStatusResponse, CommandError> {
|
|
||||||
save_singbox_device_hwid_to_storage(&state.storage(), input, &SystemClock)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_singbox_subscription(
|
pub async fn fetch_singbox_subscription(
|
||||||
state: tauri::State<'_, CommandState>,
|
state: tauri::State<'_, CommandState>,
|
||||||
@@ -1432,9 +1413,7 @@ pub fn save_singbox_subscription_to_storage(
|
|||||||
|
|
||||||
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
|
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
|
||||||
config.subscription_url = Some(subscription_url);
|
config.subscription_url = Some(subscription_url);
|
||||||
if let Some(device_hwid) = input.device_hwid {
|
ensure_device_hwid(&mut config);
|
||||||
config.device_hwid = normalize_device_hwid(&device_hwid)?;
|
|
||||||
}
|
|
||||||
config.updated_at = Some(clock.now());
|
config.updated_at = Some(clock.now());
|
||||||
storage
|
storage
|
||||||
.write_local_singbox_config(&config)
|
.write_local_singbox_config(&config)
|
||||||
@@ -1462,6 +1441,14 @@ pub fn fetch_singbox_subscription_with_fetcher(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let device_hwid_created = ensure_device_hwid(&mut config);
|
||||||
|
if device_hwid_created {
|
||||||
|
config.updated_at = Some(clock.now());
|
||||||
|
storage
|
||||||
|
.write_local_singbox_config(&config)
|
||||||
|
.map_err(storage_error)?;
|
||||||
|
}
|
||||||
|
|
||||||
let identity =
|
let identity =
|
||||||
subscription::SubscriptionFetchIdentity::with_device_hwid(config.device_hwid.as_deref());
|
subscription::SubscriptionFetchIdentity::with_device_hwid(config.device_hwid.as_deref());
|
||||||
let cache = fetcher
|
let cache = fetcher
|
||||||
@@ -1501,7 +1488,6 @@ pub fn forget_singbox_subscription_in_storage(
|
|||||||
) -> Result<LocalSingBoxStatusResponse, CommandError> {
|
) -> Result<LocalSingBoxStatusResponse, CommandError> {
|
||||||
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
|
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
|
||||||
config.subscription_url = None;
|
config.subscription_url = None;
|
||||||
config.device_hwid = None;
|
|
||||||
config.selected_server_tag = None;
|
config.selected_server_tag = None;
|
||||||
config.updated_at = Some(clock.now());
|
config.updated_at = Some(clock.now());
|
||||||
storage
|
storage
|
||||||
@@ -1514,24 +1500,6 @@ pub fn forget_singbox_subscription_in_storage(
|
|||||||
read_singbox_status(storage)
|
read_singbox_status(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_singbox_device_hwid_to_storage(
|
|
||||||
storage: &JsonStorage,
|
|
||||||
input: SaveSingBoxDeviceHwidInputDto,
|
|
||||||
clock: &impl Clock,
|
|
||||||
) -> Result<LocalSingBoxStatusResponse, CommandError> {
|
|
||||||
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
|
|
||||||
config.device_hwid = match input.device_hwid {
|
|
||||||
Some(device_hwid) => normalize_device_hwid(&device_hwid)?,
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
config.updated_at = Some(clock.now());
|
|
||||||
storage
|
|
||||||
.write_local_singbox_config(&config)
|
|
||||||
.map_err(storage_error)?;
|
|
||||||
|
|
||||||
read_singbox_status(storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_singbox_server_in_storage(
|
pub fn select_singbox_server_in_storage(
|
||||||
storage: &JsonStorage,
|
storage: &JsonStorage,
|
||||||
input: SelectSingBoxServerInputDto,
|
input: SelectSingBoxServerInputDto,
|
||||||
@@ -1734,27 +1702,17 @@ fn validate_subscription_url(subscription_url: &str) -> Result<(), CommandError>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_device_hwid(raw_hwid: &str) -> Result<Option<String>, CommandError> {
|
fn ensure_device_hwid(config: &mut LocalSingBoxConfig) -> bool {
|
||||||
let device_hwid = raw_hwid.trim();
|
if config
|
||||||
if device_hwid.is_empty() {
|
.device_hwid
|
||||||
return Ok(None);
|
.as_deref()
|
||||||
|
.is_some_and(|value| !value.trim().is_empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if device_hwid.chars().count() > 256 {
|
config.device_hwid = Some(uuid::Uuid::new_v4().hyphenated().to_string().to_uppercase());
|
||||||
return Err(CommandError::new(
|
true
|
||||||
"singbox_device_hwid_invalid",
|
|
||||||
"HWID устройства должен быть не длиннее 256 символов.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !device_hwid.chars().all(|ch| ch.is_ascii_graphic()) {
|
|
||||||
return Err(CommandError::new(
|
|
||||||
"singbox_device_hwid_invalid",
|
|
||||||
"HWID устройства может содержать только печатные ASCII-символы без пробелов.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(device_hwid.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse {
|
fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse {
|
||||||
@@ -4234,11 +4192,6 @@ impl From<&LocalSingBoxConfig> for LocalSingBoxConfigDto {
|
|||||||
.subscription_url
|
.subscription_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|value| !value.trim().is_empty()),
|
.is_some_and(|value| !value.trim().is_empty()),
|
||||||
device_hwid_display: config.device_hwid_display(),
|
|
||||||
has_device_hwid: config
|
|
||||||
.device_hwid
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|value| !value.trim().is_empty()),
|
|
||||||
selected_server_tag: config.selected_server_tag.clone(),
|
selected_server_tag: config.selected_server_tag.clone(),
|
||||||
listen_host: config.listen_host.clone(),
|
listen_host: config.listen_host.clone(),
|
||||||
listen_port: config.listen_port,
|
listen_port: config.listen_port,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ fn main() {
|
|||||||
commands::get_singbox_setup_status,
|
commands::get_singbox_setup_status,
|
||||||
commands::resolve_profile_preview,
|
commands::resolve_profile_preview,
|
||||||
commands::save_singbox_subscription,
|
commands::save_singbox_subscription,
|
||||||
commands::save_singbox_device_hwid,
|
|
||||||
commands::fetch_singbox_subscription,
|
commands::fetch_singbox_subscription,
|
||||||
commands::forget_singbox_subscription,
|
commands::forget_singbox_subscription,
|
||||||
commands::select_singbox_server,
|
commands::select_singbox_server,
|
||||||
|
|||||||
@@ -159,10 +159,6 @@ impl LocalSingBoxConfig {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(redact_subscription_url)
|
.map(redact_subscription_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_hwid_display(&self) -> Option<String> {
|
|
||||||
self.device_hwid.as_deref().map(redact_device_hwid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LocalSingBoxConfig {
|
impl Default for LocalSingBoxConfig {
|
||||||
@@ -278,27 +274,3 @@ pub fn redact_subscription_url(raw_url: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redact_device_hwid(raw_hwid: &str) -> String {
|
|
||||||
let trimmed = raw_hwid.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = trimmed.chars().count();
|
|
||||||
if length <= 8 {
|
|
||||||
return "***".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let prefix = trimmed.chars().take(4).collect::<String>();
|
|
||||||
let suffix = trimmed
|
|
||||||
.chars()
|
|
||||||
.rev()
|
|
||||||
.take(4)
|
|
||||||
.collect::<String>()
|
|
||||||
.chars()
|
|
||||||
.rev()
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
format!("{prefix}...{suffix}")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ProxyWarden",
|
"productName": "ProxyWarden",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"identifier": "ru.dokops.proxywarden.windows",
|
"identifier": "ru.dokops.proxywarden.windows",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -25,10 +25,9 @@ mod validation;
|
|||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
|
fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
|
||||||
generate_singbox_config_with_services, save_singbox_device_hwid_to_storage,
|
generate_singbox_config_with_services, save_singbox_subscription_to_storage,
|
||||||
save_singbox_subscription_to_storage, select_singbox_server_in_storage, Clock,
|
select_singbox_server_in_storage, Clock, SaveSingBoxSubscriptionInputDto,
|
||||||
SaveSingBoxDeviceHwidInputDto, SaveSingBoxSubscriptionInputDto, SelectSingBoxServerInputDto,
|
SelectSingBoxServerInputDto, SubscriptionFetcher,
|
||||||
SubscriptionFetcher,
|
|
||||||
};
|
};
|
||||||
use models::{
|
use models::{
|
||||||
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
|
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
|
||||||
@@ -50,7 +49,6 @@ fn saves_subscription_url_without_exposing_secret_query() {
|
|||||||
&storage,
|
&storage,
|
||||||
SaveSingBoxSubscriptionInputDto {
|
SaveSingBoxSubscriptionInputDto {
|
||||||
subscription_url: " https://sub.example.test/path?token=secret ".to_string(),
|
subscription_url: " https://sub.example.test/path?token=secret ".to_string(),
|
||||||
device_hwid: Some(" hwid-1234567890 ".to_string()),
|
|
||||||
},
|
},
|
||||||
&FixedClock,
|
&FixedClock,
|
||||||
)
|
)
|
||||||
@@ -63,58 +61,12 @@ fn saves_subscription_url_without_exposing_secret_query() {
|
|||||||
config.subscription_url,
|
config.subscription_url,
|
||||||
Some("https://sub.example.test/path?token=secret".to_string())
|
Some("https://sub.example.test/path?token=secret".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(config.device_hwid, Some("hwid-1234567890".to_string()));
|
assert_valid_generated_hwid(config.device_hwid.as_deref());
|
||||||
assert!(status.config.has_subscription);
|
assert!(status.config.has_subscription);
|
||||||
assert!(status.config.has_device_hwid);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
status.config.subscription_display_url,
|
status.config.subscription_display_url,
|
||||||
Some("https://sub.example.test/...".to_string())
|
Some("https://sub.example.test/...".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
status.config.device_hwid_display,
|
|
||||||
Some("hwid...7890".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
cleanup(&root);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn saves_and_clears_device_hwid_without_exposing_raw_value() {
|
|
||||||
let root = test_root("save-hwid");
|
|
||||||
let storage = JsonStorage::new(root.clone());
|
|
||||||
|
|
||||||
let status = save_singbox_device_hwid_to_storage(
|
|
||||||
&storage,
|
|
||||||
SaveSingBoxDeviceHwidInputDto {
|
|
||||||
device_hwid: Some(" device-abcdef1234 ".to_string()),
|
|
||||||
},
|
|
||||||
&FixedClock,
|
|
||||||
)
|
|
||||||
.expect("device hwid should be saved");
|
|
||||||
let config = storage
|
|
||||||
.read_local_singbox_config()
|
|
||||||
.expect("read local sing-box config");
|
|
||||||
|
|
||||||
assert_eq!(config.device_hwid, Some("device-abcdef1234".to_string()));
|
|
||||||
assert!(status.config.has_device_hwid);
|
|
||||||
assert_eq!(
|
|
||||||
status.config.device_hwid_display,
|
|
||||||
Some("devi...1234".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let status = save_singbox_device_hwid_to_storage(
|
|
||||||
&storage,
|
|
||||||
SaveSingBoxDeviceHwidInputDto { device_hwid: None },
|
|
||||||
&FixedClock,
|
|
||||||
)
|
|
||||||
.expect("device hwid should be cleared");
|
|
||||||
let config = storage
|
|
||||||
.read_local_singbox_config()
|
|
||||||
.expect("read local sing-box config");
|
|
||||||
|
|
||||||
assert_eq!(config.device_hwid, None);
|
|
||||||
assert!(!status.config.has_device_hwid);
|
|
||||||
assert_eq!(status.config.device_hwid_display, None);
|
|
||||||
|
|
||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
}
|
}
|
||||||
@@ -128,7 +80,6 @@ fn rejects_non_http_subscription_url() {
|
|||||||
&storage,
|
&storage,
|
||||||
SaveSingBoxSubscriptionInputDto {
|
SaveSingBoxSubscriptionInputDto {
|
||||||
subscription_url: "file:///C:/sub.txt".to_string(),
|
subscription_url: "file:///C:/sub.txt".to_string(),
|
||||||
device_hwid: None,
|
|
||||||
},
|
},
|
||||||
&FixedClock,
|
&FixedClock,
|
||||||
)
|
)
|
||||||
@@ -147,7 +98,6 @@ fn fetches_subscription_cache_and_selects_first_server() {
|
|||||||
&storage,
|
&storage,
|
||||||
SaveSingBoxSubscriptionInputDto {
|
SaveSingBoxSubscriptionInputDto {
|
||||||
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
|
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
|
||||||
device_hwid: None,
|
|
||||||
},
|
},
|
||||||
&FixedClock,
|
&FixedClock,
|
||||||
)
|
)
|
||||||
@@ -186,17 +136,21 @@ fn fetches_subscription_with_saved_device_hwid() {
|
|||||||
&storage,
|
&storage,
|
||||||
SaveSingBoxSubscriptionInputDto {
|
SaveSingBoxSubscriptionInputDto {
|
||||||
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
|
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
|
||||||
device_hwid: Some("hwid-abcdef1234".to_string()),
|
|
||||||
},
|
},
|
||||||
&FixedClock,
|
&FixedClock,
|
||||||
)
|
)
|
||||||
.expect("save subscription URL and HWID");
|
.expect("save subscription URL and generated HWID");
|
||||||
|
let expected_hwid = storage
|
||||||
|
.read_local_singbox_config()
|
||||||
|
.expect("read local sing-box config")
|
||||||
|
.device_hwid
|
||||||
|
.expect("generated HWID");
|
||||||
|
|
||||||
fetch_singbox_subscription_with_fetcher(
|
fetch_singbox_subscription_with_fetcher(
|
||||||
&storage,
|
&storage,
|
||||||
&HwidAssertingFetcher {
|
&HwidAssertingFetcher {
|
||||||
cache: sample_cache(),
|
cache: sample_cache(),
|
||||||
expected_hwid: Some("hwid-abcdef1234"),
|
expected_hwid,
|
||||||
},
|
},
|
||||||
&FixedClock,
|
&FixedClock,
|
||||||
)
|
)
|
||||||
@@ -205,6 +159,33 @@ fn fetches_subscription_with_saved_device_hwid() {
|
|||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_generates_device_hwid_for_existing_subscription_without_one() {
|
||||||
|
let root = test_root("fetch-generates-hwid");
|
||||||
|
let storage = JsonStorage::new(root.clone());
|
||||||
|
storage
|
||||||
|
.write_local_singbox_config(&LocalSingBoxConfig {
|
||||||
|
subscription_url: Some("https://sub.example.test/path?token=secret".to_string()),
|
||||||
|
device_hwid: None,
|
||||||
|
..LocalSingBoxConfig::default()
|
||||||
|
})
|
||||||
|
.expect("write local sing-box config");
|
||||||
|
|
||||||
|
fetch_singbox_subscription_with_fetcher(
|
||||||
|
&storage,
|
||||||
|
&GeneratedHwidAssertingFetcher(sample_cache()),
|
||||||
|
&FixedClock,
|
||||||
|
)
|
||||||
|
.expect("fetch subscription through mock");
|
||||||
|
let config = storage
|
||||||
|
.read_local_singbox_config()
|
||||||
|
.expect("read local sing-box config");
|
||||||
|
|
||||||
|
assert_valid_generated_hwid(config.device_hwid.as_deref());
|
||||||
|
|
||||||
|
cleanup(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selects_server_from_cached_subscription() {
|
fn selects_server_from_cached_subscription() {
|
||||||
let root = test_root("select-server");
|
let root = test_root("select-server");
|
||||||
@@ -271,7 +252,7 @@ fn generate_writes_config_and_local_singbox_target() {
|
|||||||
storage
|
storage
|
||||||
.write_local_singbox_config(&LocalSingBoxConfig {
|
.write_local_singbox_config(&LocalSingBoxConfig {
|
||||||
subscription_url: Some("https://sub.example.test/path".to_string()),
|
subscription_url: Some("https://sub.example.test/path".to_string()),
|
||||||
device_hwid: Some("hwid-abcdef1234".to_string()),
|
device_hwid: Some("C34C14C9-94BC-4918-B053-A249BC117A91".to_string()),
|
||||||
selected_server_tag: Some("nl-1".to_string()),
|
selected_server_tag: Some("nl-1".to_string()),
|
||||||
..LocalSingBoxConfig::default()
|
..LocalSingBoxConfig::default()
|
||||||
})
|
})
|
||||||
@@ -335,10 +316,11 @@ fn generate_requires_cached_subscription() {
|
|||||||
fn forget_subscription_clears_url_selection_and_cache() {
|
fn forget_subscription_clears_url_selection_and_cache() {
|
||||||
let root = test_root("forget-subscription");
|
let root = test_root("forget-subscription");
|
||||||
let storage = JsonStorage::new(root.clone());
|
let storage = JsonStorage::new(root.clone());
|
||||||
|
let existing_hwid = "C34C14C9-94BC-4918-B053-A249BC117A91".to_string();
|
||||||
storage
|
storage
|
||||||
.write_local_singbox_config(&LocalSingBoxConfig {
|
.write_local_singbox_config(&LocalSingBoxConfig {
|
||||||
subscription_url: Some("https://sub.example.test/path".to_string()),
|
subscription_url: Some("https://sub.example.test/path".to_string()),
|
||||||
device_hwid: Some("hwid-abcdef1234".to_string()),
|
device_hwid: Some(existing_hwid.clone()),
|
||||||
selected_server_tag: Some("nl-1".to_string()),
|
selected_server_tag: Some("nl-1".to_string()),
|
||||||
..LocalSingBoxConfig::default()
|
..LocalSingBoxConfig::default()
|
||||||
})
|
})
|
||||||
@@ -358,7 +340,7 @@ fn forget_subscription_clears_url_selection_and_cache() {
|
|||||||
|
|
||||||
assert!(!status.config.has_subscription);
|
assert!(!status.config.has_subscription);
|
||||||
assert_eq!(config.subscription_url, None);
|
assert_eq!(config.subscription_url, None);
|
||||||
assert_eq!(config.device_hwid, None);
|
assert_eq!(config.device_hwid, Some(existing_hwid));
|
||||||
assert_eq!(config.selected_server_tag, None);
|
assert_eq!(config.selected_server_tag, None);
|
||||||
assert_eq!(cache, None);
|
assert_eq!(cache, None);
|
||||||
|
|
||||||
@@ -380,7 +362,7 @@ impl SubscriptionFetcher for MockFetcher {
|
|||||||
|
|
||||||
struct HwidAssertingFetcher {
|
struct HwidAssertingFetcher {
|
||||||
cache: SubscriptionCache,
|
cache: SubscriptionCache,
|
||||||
expected_hwid: Option<&'static str>,
|
expected_hwid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubscriptionFetcher for HwidAssertingFetcher {
|
impl SubscriptionFetcher for HwidAssertingFetcher {
|
||||||
@@ -390,7 +372,10 @@ impl SubscriptionFetcher for HwidAssertingFetcher {
|
|||||||
identity: &subscription::SubscriptionFetchIdentity,
|
identity: &subscription::SubscriptionFetchIdentity,
|
||||||
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
|
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
|
||||||
assert_eq!(url, "https://sub.example.test/path?token=secret");
|
assert_eq!(url, "https://sub.example.test/path?token=secret");
|
||||||
assert_eq!(identity.device_hwid.as_deref(), self.expected_hwid);
|
assert_eq!(
|
||||||
|
identity.device_hwid.as_deref(),
|
||||||
|
Some(self.expected_hwid.as_str())
|
||||||
|
);
|
||||||
assert_eq!(identity.app_name, "ProxyWarden");
|
assert_eq!(identity.app_name, "ProxyWarden");
|
||||||
assert!(identity.user_agent.starts_with("ProxyWarden/"));
|
assert!(identity.user_agent.starts_with("ProxyWarden/"));
|
||||||
assert_eq!(identity.device_os, std::env::consts::OS);
|
assert_eq!(identity.device_os, std::env::consts::OS);
|
||||||
@@ -399,6 +384,28 @@ impl SubscriptionFetcher for HwidAssertingFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GeneratedHwidAssertingFetcher(SubscriptionCache);
|
||||||
|
|
||||||
|
impl SubscriptionFetcher for GeneratedHwidAssertingFetcher {
|
||||||
|
fn fetch_subscription(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
identity: &subscription::SubscriptionFetchIdentity,
|
||||||
|
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
|
||||||
|
assert_eq!(url, "https://sub.example.test/path?token=secret");
|
||||||
|
assert_valid_generated_hwid(identity.device_hwid.as_deref());
|
||||||
|
assert_eq!(identity.app_name, "ProxyWarden");
|
||||||
|
assert!(identity.user_agent.starts_with("ProxyWarden/"));
|
||||||
|
Ok(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_valid_generated_hwid(value: Option<&str>) {
|
||||||
|
let value = value.expect("generated HWID");
|
||||||
|
uuid::Uuid::parse_str(value).expect("HWID should be a UUID");
|
||||||
|
assert_eq!(value, value.to_ascii_uppercase());
|
||||||
|
}
|
||||||
|
|
||||||
struct MockChecker;
|
struct MockChecker;
|
||||||
|
|
||||||
impl SingBoxConfigChecker for MockChecker {
|
impl SingBoxConfigChecker for MockChecker {
|
||||||
|
|||||||
@@ -92,11 +92,6 @@ fn roundtrips_local_singbox_config_and_subscription_cache() {
|
|||||||
config.subscription_display_url(),
|
config.subscription_display_url(),
|
||||||
Some("https://sub.example.test/...".to_string())
|
Some("https://sub.example.test/...".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
config.device_hwid_display(),
|
|
||||||
Some("hwid...1234".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,18 +199,9 @@ export function getSingBoxSetupStatus(): Promise<SingBoxSetupStatus> {
|
|||||||
return invoke<SingBoxSetupStatus>('get_singbox_setup_status');
|
return invoke<SingBoxSetupStatus>('get_singbox_setup_status');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSingBoxSubscription(
|
export function saveSingBoxSubscription(subscriptionUrl: string): Promise<LocalSingBoxStatusResponse> {
|
||||||
subscriptionUrl: string,
|
|
||||||
deviceHwid?: string,
|
|
||||||
): Promise<LocalSingBoxStatusResponse> {
|
|
||||||
return invoke<LocalSingBoxStatusResponse>('save_singbox_subscription', {
|
return invoke<LocalSingBoxStatusResponse>('save_singbox_subscription', {
|
||||||
input: { subscriptionUrl, deviceHwid },
|
input: { subscriptionUrl },
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveSingBoxDeviceHwid(deviceHwid?: string): Promise<LocalSingBoxStatusResponse> {
|
|
||||||
return invoke<LocalSingBoxStatusResponse>('save_singbox_device_hwid', {
|
|
||||||
input: { deviceHwid },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
src/app/App.tsx
105
src/app/App.tsx
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { Cpu, FileCode2, Fingerprint, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
|
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
applyProfiles,
|
applyProfiles,
|
||||||
fetchSingBoxSubscription,
|
fetchSingBoxSubscription,
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
pingSingBoxServer,
|
pingSingBoxServer,
|
||||||
restartAsAdmin,
|
restartAsAdmin,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
saveSingBoxDeviceHwid,
|
|
||||||
saveSingBoxSubscription,
|
saveSingBoxSubscription,
|
||||||
saveTarget,
|
saveTarget,
|
||||||
selectSingBoxServer,
|
selectSingBoxServer,
|
||||||
@@ -45,7 +44,7 @@ import { serviceControlState } from './viewModel';
|
|||||||
|
|
||||||
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
|
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
|
||||||
type ProxiFyreAction = 'start' | 'stop' | 'restart' | 'install' | 'uninstall';
|
type ProxiFyreAction = 'start' | 'stop' | 'restart' | 'install' | 'uninstall';
|
||||||
type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'identity' | 'generate' | 'ping';
|
type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'generate' | 'ping';
|
||||||
type RouteMode = 'external' | 'local-singbox';
|
type RouteMode = 'external' | 'local-singbox';
|
||||||
type ServiceVisualState = 'active' | 'settling' | null;
|
type ServiceVisualState = 'active' | 'settling' | null;
|
||||||
type PanelId = 'summary' | 'proxifyre' | 'proxy';
|
type PanelId = 'summary' | 'proxifyre' | 'proxy';
|
||||||
@@ -185,7 +184,6 @@ export function App() {
|
|||||||
const [singBoxStatus, setSingBoxStatus] = useState<LocalSingBoxStatusResponse | null>(null);
|
const [singBoxStatus, setSingBoxStatus] = useState<LocalSingBoxStatusResponse | null>(null);
|
||||||
const [singBoxSetupStatus, setSingBoxSetupStatus] = useState<SingBoxSetupStatus | null>(null);
|
const [singBoxSetupStatus, setSingBoxSetupStatus] = useState<SingBoxSetupStatus | null>(null);
|
||||||
const [subscriptionInput, setSubscriptionInput] = useState('');
|
const [subscriptionInput, setSubscriptionInput] = useState('');
|
||||||
const [deviceHwidInput, setDeviceHwidInput] = useState('');
|
|
||||||
const [serverPings, setServerPings] = useState<Record<string, PingServerResponse>>({});
|
const [serverPings, setServerPings] = useState<Record<string, PingServerResponse>>({});
|
||||||
const [proxyCheck, setProxyCheck] = useState<ProxyTargetCheckResponse | null>(null);
|
const [proxyCheck, setProxyCheck] = useState<ProxyTargetCheckResponse | null>(null);
|
||||||
const [adminStatus, setAdminStatus] = useState<AdminStatusResponse | null>(null);
|
const [adminStatus, setAdminStatus] = useState<AdminStatusResponse | null>(null);
|
||||||
@@ -713,7 +711,6 @@ export function App() {
|
|||||||
|
|
||||||
async function syncSingBoxSubscription() {
|
async function syncSingBoxSubscription() {
|
||||||
const subscriptionUrl = subscriptionInput.trim();
|
const subscriptionUrl = subscriptionInput.trim();
|
||||||
const deviceHwid = deviceHwidInput.trim();
|
|
||||||
if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) {
|
if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) {
|
||||||
showNotice({
|
showNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
@@ -726,15 +723,12 @@ export function App() {
|
|||||||
setSingBoxAction('fetch');
|
setSingBoxAction('fetch');
|
||||||
try {
|
try {
|
||||||
if (subscriptionUrl) {
|
if (subscriptionUrl) {
|
||||||
await saveSingBoxSubscription(subscriptionUrl, deviceHwid || undefined);
|
await saveSingBoxSubscription(subscriptionUrl);
|
||||||
} else if (deviceHwid) {
|
|
||||||
await saveSingBoxDeviceHwid(deviceHwid);
|
|
||||||
}
|
}
|
||||||
const status = await fetchSingBoxSubscription();
|
const status = await fetchSingBoxSubscription();
|
||||||
setSingBoxStatus(status);
|
setSingBoxStatus(status);
|
||||||
setComponents((current) => upsertComponent(current, status.component));
|
setComponents((current) => upsertComponent(current, status.component));
|
||||||
setSubscriptionInput('');
|
setSubscriptionInput('');
|
||||||
setDeviceHwidInput('');
|
|
||||||
setServerPings({});
|
setServerPings({});
|
||||||
setProxyCheck(null);
|
setProxyCheck(null);
|
||||||
showNotice({
|
showNotice({
|
||||||
@@ -753,64 +747,6 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSingBoxDeviceHwidData() {
|
|
||||||
const deviceHwid = deviceHwidInput.trim();
|
|
||||||
if (!deviceHwid) {
|
|
||||||
showNotice({
|
|
||||||
kind: 'error',
|
|
||||||
title: 'HWID не указан',
|
|
||||||
text: 'Вставь hard ID устройства для заголовка X-Hwid.',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSingBoxAction('identity');
|
|
||||||
try {
|
|
||||||
const status = await saveSingBoxDeviceHwid(deviceHwid);
|
|
||||||
setSingBoxStatus(status);
|
|
||||||
setComponents((current) => upsertComponent(current, status.component));
|
|
||||||
setDeviceHwidInput('');
|
|
||||||
showNotice({
|
|
||||||
kind: 'success',
|
|
||||||
title: 'HWID сохранен',
|
|
||||||
text: status.config.deviceHwidDisplay
|
|
||||||
? `Для подписки будет отправляться X-Hwid: ${status.config.deviceHwidDisplay}`
|
|
||||||
: 'Для подписки будет отправляться X-Hwid.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
showNotice({
|
|
||||||
kind: 'error',
|
|
||||||
title: 'HWID не сохранен',
|
|
||||||
text: errorMessage(error),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setSingBoxAction(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function forgetSingBoxDeviceHwidData() {
|
|
||||||
setSingBoxAction('identity');
|
|
||||||
try {
|
|
||||||
const status = await saveSingBoxDeviceHwid(undefined);
|
|
||||||
setSingBoxStatus(status);
|
|
||||||
setComponents((current) => upsertComponent(current, status.component));
|
|
||||||
setDeviceHwidInput('');
|
|
||||||
showNotice({
|
|
||||||
kind: 'info',
|
|
||||||
title: 'HWID очищен',
|
|
||||||
text: 'Заголовок X-Hwid больше не будет отправляться при загрузке подписки.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
showNotice({
|
|
||||||
kind: 'error',
|
|
||||||
title: 'HWID не очищен',
|
|
||||||
text: errorMessage(error),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setSingBoxAction(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function forgetSingBoxSubscriptionData() {
|
async function forgetSingBoxSubscriptionData() {
|
||||||
setSingBoxAction('forget');
|
setSingBoxAction('forget');
|
||||||
setIsSingBoxMenuOpen(false);
|
setIsSingBoxMenuOpen(false);
|
||||||
@@ -823,7 +759,7 @@ export function App() {
|
|||||||
showNotice({
|
showNotice({
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
title: 'Подписка очищена',
|
title: 'Подписка очищена',
|
||||||
text: 'Ссылка, HWID, cache и выбранный сервер Local sing-box удалены.',
|
text: 'Ссылка, cache и выбранный сервер Local sing-box удалены.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice({
|
showNotice({
|
||||||
@@ -1492,38 +1428,6 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="subscription-line identity-line">
|
|
||||||
<span className="subscription-icon" aria-hidden="true">
|
|
||||||
<Fingerprint size={18} strokeWidth={1.9} />
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
value={deviceHwidInput}
|
|
||||||
onChange={(event) => setDeviceHwidInput(event.target.value)}
|
|
||||||
placeholder={singBoxStatus?.config.deviceHwidDisplay ?? 'X-Hwid hard ID'}
|
|
||||||
spellCheck={false}
|
|
||||||
aria-label="Hard ID устройства для X-Hwid"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void saveSingBoxDeviceHwidData()}
|
|
||||||
loading={singBoxAction === 'identity'}
|
|
||||||
loadingLabel="Сохраняю"
|
|
||||||
variant="neutral"
|
|
||||||
disabled={Boolean(singBoxAction) || !deviceHwidInput.trim()}
|
|
||||||
>
|
|
||||||
{singBoxStatus?.config.hasDeviceHwid ? 'Заменить' : 'Сохранить'}
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => void forgetSingBoxDeviceHwidData()}
|
|
||||||
disabled={Boolean(singBoxAction) || !singBoxStatus?.config.hasDeviceHwid}
|
|
||||||
label="Очистить hard ID устройства"
|
|
||||||
tooltip="Очистить HWID"
|
|
||||||
icon={<Trash2 size={18} strokeWidth={1.9} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="singbox-workspace-head">
|
<div className="singbox-workspace-head">
|
||||||
<span>Серверы подписки</span>
|
<span>Серверы подписки</span>
|
||||||
<div className="singbox-workspace-actions">
|
<div className="singbox-workspace-actions">
|
||||||
@@ -2662,7 +2566,6 @@ function singBoxDetailLines(
|
|||||||
`Файл: ${component?.path ?? 'не найден'}`,
|
`Файл: ${component?.path ?? 'не найден'}`,
|
||||||
`Конфиг: ${status?.generatedConfigPath ?? 'не создан'}`,
|
`Конфиг: ${status?.generatedConfigPath ?? 'не создан'}`,
|
||||||
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
|
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
|
||||||
`X-Hwid: ${status?.config.deviceHwidDisplay ?? (status?.config.hasDeviceHwid ? 'сохранен' : 'не задан')}`,
|
|
||||||
`Состав: ${setupDetails}`,
|
`Состав: ${setupDetails}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ export interface ComponentStatus {
|
|||||||
export interface LocalSingBoxConfig {
|
export interface LocalSingBoxConfig {
|
||||||
subscriptionDisplayUrl?: string;
|
subscriptionDisplayUrl?: string;
|
||||||
hasSubscription: boolean;
|
hasSubscription: boolean;
|
||||||
deviceHwidDisplay?: string;
|
|
||||||
hasDeviceHwid: boolean;
|
|
||||||
selectedServerTag?: string;
|
selectedServerTag?: string;
|
||||||
listenHost: string;
|
listenHost: string;
|
||||||
listenPort: number;
|
listenPort: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user