Bump version and auto-generate sing-box HWIDs

This commit is contained in:
2026-07-08 21:03:14 +03:00
parent 42b85cc8fa
commit f6b722b22a
13 changed files with 101 additions and 281 deletions

3
src-tauri/Cargo.lock generated
View File

@@ -2314,7 +2314,7 @@ dependencies = [
[[package]]
name = "proxywarden"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"base64 0.22.1",
"reqwest 0.12.28",
@@ -2324,6 +2324,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-dialog",
"url",
"uuid",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "proxywarden"
version = "1.0.0"
version = "1.0.1"
description = "Standalone Windows desktop proxy management app for ProxyWarden."
authors = ["ProxyWarden"]
edition = "2021"
@@ -20,3 +20,4 @@ tauri-plugin-dialog = "2.7.1"
base64 = "0.22"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "socks"] }
url = "2"
uuid = { version = "1", features = ["v4"] }

View File

@@ -232,8 +232,6 @@ pub struct LocalSingBoxStatusResponse {
pub struct LocalSingBoxConfigDto {
pub subscription_display_url: Option<String>,
pub has_subscription: bool,
pub device_hwid_display: Option<String>,
pub has_device_hwid: bool,
pub selected_server_tag: Option<String>,
pub listen_host: String,
pub listen_port: u16,
@@ -264,15 +262,6 @@ pub struct SubscriptionServerDto {
#[serde(rename_all = "camelCase")]
pub struct SaveSingBoxSubscriptionInputDto {
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)]
@@ -684,14 +673,6 @@ pub fn save_singbox_subscription(
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]
pub async fn fetch_singbox_subscription(
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)?;
config.subscription_url = Some(subscription_url);
if let Some(device_hwid) = input.device_hwid {
config.device_hwid = normalize_device_hwid(&device_hwid)?;
}
ensure_device_hwid(&mut config);
config.updated_at = Some(clock.now());
storage
.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 =
subscription::SubscriptionFetchIdentity::with_device_hwid(config.device_hwid.as_deref());
let cache = fetcher
@@ -1501,7 +1488,6 @@ pub fn forget_singbox_subscription_in_storage(
) -> Result<LocalSingBoxStatusResponse, CommandError> {
let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
config.subscription_url = None;
config.device_hwid = None;
config.selected_server_tag = None;
config.updated_at = Some(clock.now());
storage
@@ -1514,24 +1500,6 @@ pub fn forget_singbox_subscription_in_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(
storage: &JsonStorage,
input: SelectSingBoxServerInputDto,
@@ -1734,27 +1702,17 @@ fn validate_subscription_url(subscription_url: &str) -> Result<(), CommandError>
Ok(())
}
fn normalize_device_hwid(raw_hwid: &str) -> Result<Option<String>, CommandError> {
let device_hwid = raw_hwid.trim();
if device_hwid.is_empty() {
return Ok(None);
fn ensure_device_hwid(config: &mut LocalSingBoxConfig) -> bool {
if config
.device_hwid
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
return false;
}
if device_hwid.chars().count() > 256 {
return Err(CommandError::new(
"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()))
config.device_hwid = Some(uuid::Uuid::new_v4().hyphenated().to_string().to_uppercase());
true
}
fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse {
@@ -4234,11 +4192,6 @@ impl From<&LocalSingBoxConfig> for LocalSingBoxConfigDto {
.subscription_url
.as_deref()
.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(),
listen_host: config.listen_host.clone(),
listen_port: config.listen_port,

View File

@@ -51,7 +51,6 @@ fn main() {
commands::get_singbox_setup_status,
commands::resolve_profile_preview,
commands::save_singbox_subscription,
commands::save_singbox_device_hwid,
commands::fetch_singbox_subscription,
commands::forget_singbox_subscription,
commands::select_singbox_server,

View File

@@ -159,10 +159,6 @@ impl LocalSingBoxConfig {
.as_deref()
.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 {
@@ -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}")
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyWarden",
"version": "1.0.0",
"version": "1.0.1",
"identifier": "ru.dokops.proxywarden.windows",
"build": {
"beforeDevCommand": "npm run dev",

View File

@@ -25,10 +25,9 @@ mod validation;
use commands::{
fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
generate_singbox_config_with_services, save_singbox_device_hwid_to_storage,
save_singbox_subscription_to_storage, select_singbox_server_in_storage, Clock,
SaveSingBoxDeviceHwidInputDto, SaveSingBoxSubscriptionInputDto, SelectSingBoxServerInputDto,
SubscriptionFetcher,
generate_singbox_config_with_services, save_singbox_subscription_to_storage,
select_singbox_server_in_storage, Clock, SaveSingBoxSubscriptionInputDto,
SelectSingBoxServerInputDto, SubscriptionFetcher,
};
use models::{
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
@@ -50,7 +49,6 @@ fn saves_subscription_url_without_exposing_secret_query() {
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: " https://sub.example.test/path?token=secret ".to_string(),
device_hwid: Some(" hwid-1234567890 ".to_string()),
},
&FixedClock,
)
@@ -63,58 +61,12 @@ fn saves_subscription_url_without_exposing_secret_query() {
config.subscription_url,
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_device_hwid);
assert_eq!(
status.config.subscription_display_url,
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);
}
@@ -128,7 +80,6 @@ fn rejects_non_http_subscription_url() {
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: "file:///C:/sub.txt".to_string(),
device_hwid: None,
},
&FixedClock,
)
@@ -147,7 +98,6 @@ fn fetches_subscription_cache_and_selects_first_server() {
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
device_hwid: None,
},
&FixedClock,
)
@@ -186,17 +136,21 @@ fn fetches_subscription_with_saved_device_hwid() {
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: "https://sub.example.test/path?token=secret".to_string(),
device_hwid: Some("hwid-abcdef1234".to_string()),
},
&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(
&storage,
&HwidAssertingFetcher {
cache: sample_cache(),
expected_hwid: Some("hwid-abcdef1234"),
expected_hwid,
},
&FixedClock,
)
@@ -205,6 +159,33 @@ fn fetches_subscription_with_saved_device_hwid() {
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]
fn selects_server_from_cached_subscription() {
let root = test_root("select-server");
@@ -271,7 +252,7 @@ fn generate_writes_config_and_local_singbox_target() {
storage
.write_local_singbox_config(&LocalSingBoxConfig {
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()),
..LocalSingBoxConfig::default()
})
@@ -335,10 +316,11 @@ fn generate_requires_cached_subscription() {
fn forget_subscription_clears_url_selection_and_cache() {
let root = test_root("forget-subscription");
let storage = JsonStorage::new(root.clone());
let existing_hwid = "C34C14C9-94BC-4918-B053-A249BC117A91".to_string();
storage
.write_local_singbox_config(&LocalSingBoxConfig {
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()),
..LocalSingBoxConfig::default()
})
@@ -358,7 +340,7 @@ fn forget_subscription_clears_url_selection_and_cache() {
assert!(!status.config.has_subscription);
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!(cache, None);
@@ -380,7 +362,7 @@ impl SubscriptionFetcher for MockFetcher {
struct HwidAssertingFetcher {
cache: SubscriptionCache,
expected_hwid: Option<&'static str>,
expected_hwid: String,
}
impl SubscriptionFetcher for HwidAssertingFetcher {
@@ -390,7 +372,10 @@ impl SubscriptionFetcher for HwidAssertingFetcher {
identity: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
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!(identity.user_agent.starts_with("ProxyWarden/"));
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;
impl SingBoxConfigChecker for MockChecker {

View File

@@ -92,11 +92,6 @@ fn roundtrips_local_singbox_config_and_subscription_cache() {
config.subscription_display_url(),
Some("https://sub.example.test/...".to_string())
);
assert_eq!(
config.device_hwid_display(),
Some("hwid...1234".to_string())
);
cleanup(&root);
}