Refactor VPN proxy routing and session handling

This commit is contained in:
2026-07-07 22:07:17 +03:00
parent 59f2264a2e
commit 7dbf786c56
25 changed files with 1921 additions and 236 deletions

View File

@@ -1932,6 +1932,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -2396,6 +2397,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
@@ -3040,6 +3065,64 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
dependencies = [
"anyhow",
"glob",
"plist",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri-utils",
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -3618,6 +3701,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
]
[[package]]
@@ -4047,6 +4131,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -4080,13 +4173,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -4117,6 +4227,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -4129,6 +4245,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -4141,12 +4263,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -4159,6 +4293,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -4171,6 +4311,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -4183,6 +4329,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -4195,6 +4347,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"

View File

@@ -16,4 +16,5 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-dialog = "2.7.1"

View File

@@ -1,4 +1,3 @@
fn main() {
tauri_build::build();
}

View File

@@ -3,5 +3,5 @@
"identifier": "default",
"description": "Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.",
"windows": ["main"],
"permissions": ["core:default"]
"permissions": ["core:default", "dialog:allow-open"]
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.","local":true,"windows":["main"],"permissions":["core:default"]}}
{"default":{"identifier":"default","description":"Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open"]}}

View File

@@ -2191,6 +2191,72 @@
"type": "string",
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
}
]
},

View File

@@ -2191,6 +2191,72 @@
"type": "string",
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
}
]
},

View File

@@ -1,3 +1,8 @@
#[cfg(not(test))]
use crate::adapters::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
use crate::models::{
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol,
ProxyProtocol, Target,
@@ -7,11 +12,6 @@ use crate::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
#[cfg(not(test))]
use crate::adapters::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
use serde::{Deserialize, Serialize};
pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre";
@@ -45,7 +45,10 @@ impl ProxiFyreAdapter {
if app_names.is_empty() {
return Err(ProxyRouterError::new(
ProxyRouterErrorKind::EmptyProfileItems,
format!("В профиле '{}' нет приложений для маршрутизации", profile.id),
format!(
"В профиле '{}' нет приложений для маршрутизации",
profile.id
),
));
}

View File

@@ -206,7 +206,10 @@ impl SingBoxConfigChecker for SingBoxCommandChecker {
let _ = fs::remove_file(&config_path);
SingBoxConfigError::new(
SingBoxConfigErrorKind::CheckFailed,
format!("Не удалось выполнить '{} check': {error}", binary_path.display()),
format!(
"Не удалось выполнить '{} check': {error}",
binary_path.display()
),
)
})?;
let _ = fs::remove_file(&config_path);
@@ -320,14 +323,20 @@ fn ensure_local_singbox_target(
else {
return Err(SingBoxConfigError::new(
SingBoxConfigErrorKind::MissingRequiredComponent,
format!("Локальная цель '{}' требует состояние компонента sing-box", target.id),
format!(
"Локальная цель '{}' требует состояние компонента sing-box",
target.id
),
));
};
if !component_is_running(status) {
return Err(SingBoxConfigError::new(
SingBoxConfigErrorKind::RequiredComponentNotRunning,
format!("Локальная цель '{}' требует установленный и запущенный sing-box", target.id),
format!(
"Локальная цель '{}' требует установленный и запущенный sing-box",
target.id
),
));
}

View File

@@ -5,13 +5,6 @@ use crate::adapters::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
#[cfg(test)]
use crate::proxifyre::ProxiFyreAdapter;
#[cfg(test)]
use crate::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
use crate::component_detection::{
detect_proxyfier_install, detect_proxyfier_install_with_host,
proxyfier_component_from_detection, DetectedProxyfier, ProxyfierDetectionHost,
@@ -22,9 +15,17 @@ use crate::models::{
ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, Target,
TargetInput, TargetKind,
};
#[cfg(test)]
use crate::proxifyre::ProxiFyreAdapter;
#[cfg(test)]
use crate::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
use crate::storage::{default_config_root, JsonStorage};
use crate::validation::{normalize_profile, normalize_target, ValidationError};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -101,6 +102,14 @@ pub struct StatusResponse {
pub generated_config_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SavedStateResponse {
pub profiles: Vec<ProfileDto>,
pub targets: Vec<TargetDto>,
pub generated_config_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInputDto {
@@ -321,8 +330,20 @@ where
}
#[tauri::command]
pub fn get_status(state: tauri::State<'_, CommandState>) -> Result<StatusResponse, CommandError> {
build_status(&state.storage())
pub async fn get_status(
state: tauri::State<'_, CommandState>,
) -> Result<StatusResponse, CommandError> {
let storage = state.storage();
tauri::async_runtime::spawn_blocking(move || build_status(&storage))
.await
.map_err(background_task_error)?
}
#[tauri::command]
pub fn get_saved_state(
state: tauri::State<'_, CommandState>,
) -> Result<SavedStateResponse, CommandError> {
read_saved_state(&state.storage())
}
#[tauri::command]
@@ -354,10 +375,13 @@ pub fn save_target(
}
#[tauri::command]
pub fn get_components(
pub async fn get_components(
state: tauri::State<'_, CommandState>,
) -> Result<Vec<ComponentStatusDto>, CommandError> {
read_components(&state.storage())
let storage = state.storage();
tauri::async_runtime::spawn_blocking(move || read_components(&storage))
.await
.map_err(background_task_error)?
}
#[tauri::command]
@@ -389,7 +413,10 @@ pub fn get_logs(
#[tauri::command]
pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<String, CommandError> {
let storage = state.storage();
let generated_path = storage.paths().generated_dir.join("proxifyre-app-config.json");
let generated_path = storage
.paths()
.generated_dir
.join("proxifyre-app-config.json");
let config_path = detect_proxyfier_install()
.and_then(|detected| detected.config_path)
.filter(|path| path.exists())
@@ -403,6 +430,20 @@ pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<Str
Ok(config_path.display().to_string())
}
#[tauri::command]
pub async fn start_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Start))
.await
.map_err(background_task_error)?
}
#[tauri::command]
pub async fn stop_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Stop))
.await
.map_err(background_task_error)?
}
pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandError> {
let profiles = storage.read_profiles().map_err(storage_error)?;
let targets = storage.read_targets().map_err(storage_error)?;
@@ -426,7 +467,11 @@ pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandErro
routed_app_count,
active_target: active_target.map(TargetDto::from),
components: components.iter().map(ComponentStatusDto::from).collect(),
recent_activity: activity.iter().take(10).map(ActivityEntryDto::from).collect(),
recent_activity: activity
.iter()
.take(10)
.map(ActivityEntryDto::from)
.collect(),
generated_config_path: storage
.paths()
.generated_dir
@@ -501,6 +546,19 @@ pub fn read_activity(storage: &JsonStorage) -> Result<Vec<ActivityEntryDto>, Com
.map(|entries| entries.iter().map(ActivityEntryDto::from).collect())
}
pub fn read_saved_state(storage: &JsonStorage) -> Result<SavedStateResponse, CommandError> {
Ok(SavedStateResponse {
profiles: read_profiles(storage)?,
targets: read_targets(storage)?,
generated_config_path: storage
.paths()
.generated_dir
.join("proxifyre-app-config.json")
.display()
.to_string(),
})
}
pub fn resolve_preview(
input: ProfileInputDto,
) -> Result<ResolveProfilePreviewResponse, CommandError> {
@@ -608,7 +666,10 @@ fn default_components() -> Vec<ComponentStatus> {
version: None,
path: None,
problems: Vec::new(),
actions: vec!["Открыть журнал".to_string(), "Скопировать диагностику".to_string()],
actions: vec![
"Открыть журнал".to_string(),
"Скопировать диагностику".to_string(),
],
},
ComponentStatus {
id: ComponentId::Proxyfier,
@@ -720,6 +781,386 @@ fn open_folder(path: &Path) -> Result<String, CommandError> {
Ok(path.display().to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ServiceControlAction {
Start,
Stop,
}
impl ServiceControlAction {
fn error_code(self) -> &'static str {
match self {
ServiceControlAction::Start => "proxifyre_service_start_failed",
ServiceControlAction::Stop => "proxifyre_service_stop_failed",
}
}
fn label(self) -> &'static str {
match self {
ServiceControlAction::Start => "запустить",
ServiceControlAction::Stop => "остановить",
}
}
}
fn control_proxifyre_service(
action: ServiceControlAction,
) -> Result<ComponentStatusDto, CommandError> {
let Some(detected) = detect_proxyfier_install() else {
return Err(CommandError::new(
"proxifyre_not_found",
"ProxiFyre не найден на компьютере.",
));
};
run_proxifyre_service_command(action, &service_name_candidates(&detected))?;
let refreshed = detect_proxyfier_install();
let component = proxyfier_component_from_detection(refreshed.as_ref());
Ok(ComponentStatusDto::from(&component))
}
fn service_name_candidates(detected: &DetectedProxyfier) -> Vec<String> {
let mut names = Vec::new();
if let Some(service_name) = &detected.service_name {
names.push(service_name.clone());
}
for service_name in ["ProxiFyreService", "ProxiFyre"] {
if !names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(service_name))
{
names.push(service_name.to_string());
}
}
names
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceCommandOutput {
success: bool,
code: String,
service_name: Option<String>,
status: Option<String>,
process_id: Option<u32>,
}
fn run_proxifyre_service_command(
action: ServiceControlAction,
service_names: &[String],
) -> Result<(), CommandError> {
let names = service_names
.iter()
.map(|name| format!("'{}'", escape_powershell_single(name)))
.collect::<Vec<_>>()
.join(", ");
let action_name = match action {
ServiceControlAction::Start => "start",
ServiceControlAction::Stop => "stop",
};
let script = format!(
r#"
$ErrorActionPreference = 'Stop'
$names = @({names})
$action = '{action_name}'
$service = $null
function Find-ProxiFyreService {{
foreach ($name in $names) {{
$candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($null -ne $candidate) {{ return $candidate }}
}}
return Get-Service |
Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
Select-Object -First 1
}}
function Get-ServiceProcessId([string]$name) {{
$escapedName = $name.Replace("'", "''")
$record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
if ($null -eq $record) {{ return 0 }}
return [int]$record.ProcessId
}}
function Get-ServiceStatus([string]$name) {{
$current = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($null -eq $current) {{ return $null }}
return $current.Status.ToString()
}}
function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{
[PSCustomObject]@{{
success = $success
code = $code
serviceName = if ($null -ne $service) {{ $service.Name }} else {{ $null }}
status = $status
processId = $processId
}} | ConvertTo-Json -Compress
exit 0
}}
$service = Find-ProxiFyreService
if ($null -eq $service) {{
Write-ServiceResult $false 'service_not_found' $null 0
}}
$status = $service.Status.ToString()
$processId = Get-ServiceProcessId $service.Name
if ($action -eq 'start') {{
if ($status -eq 'Running') {{
Write-ServiceResult $true 'already_running' $status $processId
}}
try {{
Start-Service -Name $service.Name -ErrorAction Stop
$service = Get-Service -Name $service.Name
$service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15))
}} catch {{
Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $service.Name) (Get-ServiceProcessId $service.Name)
}}
Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $service.Name)
}}
if ($status -eq 'Stopped') {{
Write-ServiceResult $true 'already_stopped' $status $processId
}}
try {{
if ($service.CanStop) {{
Stop-Service -Name $service.Name -Force -ErrorAction Stop
}}
}} catch {{}}
try {{
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
$null = & sc.exe stop $service.Name 2>$null
}}
}} catch {{}}
try {{
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
}}
}} catch {{}}
$status = Get-ServiceStatus $service.Name
$processId = Get-ServiceProcessId $service.Name
if ($status -ne 'Stopped' -and $processId -gt 0) {{
try {{
$null = & taskkill.exe /PID $processId /F 2>$null
Start-Sleep -Milliseconds 700
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service) {{
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
}}
}} catch {{}}
}}
$status = Get-ServiceStatus $service.Name
$processId = Get-ServiceProcessId $service.Name
if ($status -eq 'Stopped') {{
Write-ServiceResult $true 'stopped' $status $processId
}}
Write-ServiceResult $false 'stop_failed' $status $processId
"#
);
let output = Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
script.as_str(),
])
.output()
.map_err(|error| {
CommandError::new(
action.error_code(),
format!("Не удалось {} службу ProxiFyre: {error}", action.label()),
)
})?;
let result = parse_service_command_output(&output.stdout).ok_or_else(|| {
CommandError::new(
action.error_code(),
service_script_failed_message(action, output.status.code()),
)
})?;
if result.success {
return Ok(());
}
if matches!(result.code.as_str(), "start_failed" | "stop_failed") {
run_elevated_proxifyre_service_command(action, service_names, &result)?;
return Ok(());
}
Err(CommandError::new(
action.error_code(),
service_command_failed_message(action, &result),
))
}
fn run_elevated_proxifyre_service_command(
action: ServiceControlAction,
service_names: &[String],
direct_result: &ServiceCommandOutput,
) -> Result<(), CommandError> {
let script_path = write_elevated_service_script(action, service_names)?;
let launch_script = format!(
"$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
escape_powershell_single(&script_path.display().to_string())
);
let output = Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
launch_script.as_str(),
])
.output();
let _ = fs::remove_file(&script_path);
match output {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => Err(CommandError::new(
action.error_code(),
elevated_service_failed_message(action, direct_result, output.status.code()),
)),
Err(error) => Err(CommandError::new(
action.error_code(),
format!(
"Не удалось запросить права администратора, чтобы {} службу ProxiFyre: {error}",
action.label()
),
)),
}
}
fn write_elevated_service_script(
action: ServiceControlAction,
service_names: &[String],
) -> Result<PathBuf, CommandError> {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or(0);
let script_path = env::temp_dir().join(format!("vpn-proxy-proxifyre-service-{nonce}.ps1"));
let script = elevated_service_script(action, service_names);
fs::write(&script_path, script).map_err(|error| {
CommandError::new(
action.error_code(),
format!(
"Не удалось подготовить временный скрипт для управления ProxiFyre '{}': {error}",
script_path.display()
),
)
})?;
Ok(script_path)
}
fn elevated_service_script(action: ServiceControlAction, service_names: &[String]) -> String {
let names = service_names
.iter()
.map(|name| format!("'{}'", escape_powershell_single(name)))
.collect::<Vec<_>>()
.join(", ");
let action_name = match action {
ServiceControlAction::Start => "start",
ServiceControlAction::Stop => "stop",
};
format!(
r#"
$ErrorActionPreference = 'SilentlyContinue'
$names = @({names})
$action = '{action_name}'
$service = $null
foreach ($name in $names) {{
$service = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($null -ne $service) {{ break }}
}}
if ($null -eq $service) {{
$service = Get-Service |
Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
Select-Object -First 1
}}
if ($null -eq $service) {{ exit 2 }}
function Get-ServiceProcessId([string]$name) {{
$escapedName = $name.Replace("'", "''")
$record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
if ($null -eq $record) {{ return 0 }}
return [int]$record.ProcessId
}}
if ($action -eq 'start') {{
if ($service.Status -eq 'Running') {{ exit 0 }}
Start-Service -Name $service.Name -ErrorAction SilentlyContinue
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service) {{
try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
if ($service.Status -eq 'Running') {{ exit 0 }}
}}
exit 3
}}
if ($service.Status -eq 'Stopped') {{ exit 0 }}
if ($service.CanStop) {{
Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
}}
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
$null = & sc.exe stop $service.Name 2>$null
}}
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
}}
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
$processId = Get-ServiceProcessId $service.Name
if ($processId -gt 0) {{
$null = & taskkill.exe /PID $processId /F 2>$null
Start-Sleep -Milliseconds 700
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service) {{
try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
}}
}}
}}
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -eq $service -or $service.Status -eq 'Stopped') {{ exit 0 }}
exit 4
"#
)
}
fn apply_to_detected_proxyfier(
request: HelperApplyRequest<'_>,
detected: &DetectedProxyfier,
@@ -832,6 +1273,104 @@ fn storage_error(error: std::io::Error) -> CommandError {
CommandError::new("storage_error", error.to_string())
}
fn background_task_error(error: impl std::fmt::Display) -> CommandError {
CommandError::new(
"background_task_failed",
format!("Фоновая проверка не завершилась: {error}"),
)
}
fn parse_service_command_output(stdout: &[u8]) -> Option<ServiceCommandOutput> {
let stdout = String::from_utf8_lossy(stdout);
let payload = stdout
.lines()
.rev()
.map(str::trim)
.find(|line| line.starts_with('{') && line.ends_with('}'))?;
serde_json::from_str(payload).ok()
}
fn service_script_failed_message(action: ServiceControlAction, exit_code: Option<i32>) -> String {
let exit_code = exit_code
.map(|code| format!(" Код выхода PowerShell: {code}."))
.unwrap_or_default();
format!(
"Не удалось {} службу ProxiFyre: команда управления службой не вернула корректный результат.{exit_code}",
action.label()
)
}
fn service_command_failed_message(
action: ServiceControlAction,
result: &ServiceCommandOutput,
) -> String {
let service_name = result
.service_name
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or("ProxiFyre");
let status = result
.status
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or("неизвестен");
let pid = result
.process_id
.filter(|value| *value > 0)
.map(|value| format!(", PID: {value}"))
.unwrap_or_default();
match result.code.as_str() {
"service_not_found" => "Служба ProxiFyre не найдена.".to_string(),
"start_failed" => format!(
"Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
),
"stop_failed" => format!(
"Не удалось остановить службу {service_name} даже после принудительной попытки. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
),
_ => format!(
"Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
action.label()
),
}
}
fn elevated_service_failed_message(
action: ServiceControlAction,
direct_result: &ServiceCommandOutput,
exit_code: Option<i32>,
) -> String {
let service_name = direct_result
.service_name
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or("ProxiFyre");
let status = direct_result
.status
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or("неизвестен");
let pid = direct_result
.process_id
.filter(|value| *value > 0)
.map(|value| format!(", PID: {value}"))
.unwrap_or_default();
let exit_code = exit_code
.map(|code| format!(" Код elevated-команды: {code}."))
.unwrap_or_default();
format!(
"Не удалось {} службу {service_name} даже после запроса прав администратора. До запроса UAC статус был: {status}{pid}.{exit_code} Если появлялось окно UAC, проверь, что оно было подтверждено.",
action.label()
)
}
fn escape_powershell_single(value: &str) -> String {
value.replace('\'', "''")
}
fn validation_error(errors: Vec<ValidationError>) -> CommandError {
CommandError::with_details(
"validation_error",

View File

@@ -94,9 +94,7 @@ pub fn detect_proxyfier_install_with_host(
.next()
}
pub fn proxyfier_component_from_detection(
detected: Option<&DetectedProxyfier>,
) -> ComponentStatus {
pub fn proxyfier_component_from_detection(detected: Option<&DetectedProxyfier>) -> ComponentStatus {
match detected {
Some(proxyfier) => detected_proxyfier_component(proxyfier),
None => missing_proxyfier_component(),
@@ -115,7 +113,7 @@ fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatu
vec![
"Применить сгенерированный конфиг".to_string(),
"Открыть папку конфига".to_string(),
"Перезапустить".to_string(),
"Остановить".to_string(),
]
} else {
vec![
@@ -255,7 +253,8 @@ fn push_env_candidate(
fn push_candidate(candidates: &mut Vec<ProxyfierCandidate>, candidate: ProxyfierCandidate) {
if !candidates.iter().any(|existing| {
existing.engine == candidate.engine && same_path(&existing.install_dir, &candidate.install_dir)
existing.engine == candidate.engine
&& same_path(&existing.install_dir, &candidate.install_dir)
}) {
candidates.push(candidate);
}

View File

@@ -1,8 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod activity;
mod component_detection;
mod commands;
mod component_detection;
mod models;
mod storage;
mod validation;
@@ -24,10 +24,12 @@ pub(crate) mod proxy_router {
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(commands::CommandState::default())
.invoke_handler(tauri::generate_handler![
commands::get_status,
commands::get_profiles,
commands::get_saved_state,
commands::save_profile,
commands::get_targets,
commands::save_target,
@@ -35,7 +37,9 @@ fn main() {
commands::resolve_profile_preview,
commands::apply_profiles,
commands::get_logs,
commands::open_config_location
commands::open_config_location,
commands::start_proxifyre_service,
commands::stop_proxifyre_service
])
.run(tauri::generate_context!())
.expect("не удалось запустить клиент VPN Proxy для Windows");

View File

@@ -45,12 +45,7 @@ fn slug(value: &str, fallback: &str) -> String {
}
fn process_name(value: &str) -> String {
let base = value
.trim()
.rsplit(['\\', '/'])
.next()
.unwrap_or("")
.trim();
let base = value.trim().rsplit(['\\', '/']).next().unwrap_or("").trim();
base.strip_suffix(".exe")
.or_else(|| base.strip_suffix(".EXE"))
.unwrap_or(base)
@@ -62,7 +57,10 @@ pub fn parse_protocol(value: &str) -> Result<Protocol, ValidationError> {
match value.trim().to_ascii_uppercase().as_str() {
"TCP" => Ok(Protocol::Tcp),
"UDP" => Ok(Protocol::Udp),
_ => Err(error("protocols", format!("Неподдерживаемый протокол: {value}"))),
_ => Err(error(
"protocols",
format!("Неподдерживаемый протокол: {value}"),
)),
}
}
@@ -71,7 +69,10 @@ pub fn parse_profile_item_type(value: &str) -> Result<ProfileItemType, Validatio
"process" => Ok(ProfileItemType::Process),
"folder" => Ok(ProfileItemType::Folder),
"exe" => Ok(ProfileItemType::Exe),
_ => Err(error("items.type", format!("Неподдерживаемый тип элемента: {value}"))),
_ => Err(error(
"items.type",
format!("Неподдерживаемый тип элемента: {value}"),
)),
}
}
@@ -149,8 +150,8 @@ pub fn normalize_profile(input: ProfileInput) -> ValidationResult<Profile> {
continue;
}
let recursive = matches!(item_type, ProfileItemType::Folder)
&& raw_item.recursive.unwrap_or(true);
let recursive =
matches!(item_type, ProfileItemType::Folder) && raw_item.recursive.unwrap_or(true);
items.push(ProfileItem {
item_type,
value,

View File

@@ -1,9 +1,9 @@
#[path = "../src/activity.rs"]
mod activity;
#[path = "../src/component_detection.rs"]
mod component_detection;
#[path = "../src/commands.rs"]
mod commands;
#[path = "../src/component_detection.rs"]
mod component_detection;
#[path = "../src/models.rs"]
mod models;
#[path = "../src/adapters/proxifyre.rs"]
@@ -17,9 +17,9 @@ mod validation;
use commands::{
apply_profiles_with_services, build_status, resolve_component_statuses, resolve_preview,
save_profile_to_storage, save_target_to_storage, Clock, CommandError,
DetectedProxyApplyHelper, HelperApplyRequest, HelperApplyResult, ProfileInputDto,
ProfileItemInputDto, ProxyApplyHelper, TargetInputDto,
save_profile_to_storage, save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper,
HelperApplyRequest, HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper,
TargetInputDto,
};
use component_detection::{
DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry,
@@ -240,8 +240,8 @@ fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
})
.expect("detected helper should apply");
let applied = fs::read_to_string(install_dir.join("app-config.json"))
.expect("read applied app-config");
let applied =
fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config");
assert!(result.success);
assert!(result.changed);
@@ -276,7 +276,9 @@ fn detected_proxy_apply_helper_ignores_plain_proxifier_install() {
assert!(result.success);
assert!(result.changed);
assert_eq!(result.action, "proxifyre.stage-generated-config");
assert!(result.message.contains("совместимая установка ProxiFyre не найдена"));
assert!(result
.message
.contains("совместимая установка ProxiFyre не найдена"));
cleanup(&root);
}
@@ -358,7 +360,10 @@ impl ProxyfierDetectionHost for DetectionHost {
}
fn normalize_path(path: &Path) -> String {
path.display().to_string().replace('/', "\\").to_ascii_lowercase()
path.display()
.to_string()
.replace('/', "\\")
.to_ascii_lowercase()
}
fn test_root(name: &str) -> PathBuf {

View File

@@ -7,7 +7,7 @@ use component_detection::{
detect_proxyfier_install_with_host, proxyfier_component_from_detection, ProxyfierDetectionHost,
ProxyfierEngine, RegistryInstallEntry,
};
use models::{ComponentState};
use models::ComponentState;
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
@@ -25,7 +25,10 @@ fn detects_existing_proxifyre_from_registry_install_location() {
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
assert_eq!(detected.install_dir, PathBuf::from(r"C:\Tools\ProxiFyre"));
assert_eq!(detected.config_path, Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")));
assert_eq!(
detected.config_path,
Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json"))
);
assert!(detected.running);
let component = proxyfier_component_from_detection(Some(&detected));
@@ -56,7 +59,10 @@ fn env_override_can_point_to_portable_proxifyre_install() {
.expect("env override should be checked before common paths");
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
assert_eq!(detected.executable_path, PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe"));
assert_eq!(
detected.executable_path,
PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe")
);
}
#[test]
@@ -118,17 +124,16 @@ impl ProxyfierDetectionHost for MockHost {
}
fn path_exists(&self, path: &Path) -> bool {
self.paths.contains(&normalize_path(&path.display().to_string()))
self.paths
.contains(&normalize_path(&path.display().to_string()))
}
fn process_running(&self, process_name: &str) -> bool {
self.processes
.contains(&process_name.to_ascii_lowercase())
self.processes.contains(&process_name.to_ascii_lowercase())
}
fn service_running(&self, service_name: &str) -> bool {
self.services
.contains(&service_name.to_ascii_lowercase())
self.services.contains(&service_name.to_ascii_lowercase())
}
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {

View File

@@ -124,7 +124,5 @@ fn rejects_malformed_target_fields() {
assert!(error.iter().any(|item| item.field == "host"));
assert!(error.iter().any(|item| item.field == "port"));
assert!(error.iter().any(|item| item.field == "protocol"));
assert!(error
.iter()
.any(|item| item.field == "requires_component"));
assert!(error.iter().any(|item| item.field == "requires_component"));
}

View File

@@ -1,9 +1,9 @@
#[path = "../src/models.rs"]
mod models;
#[path = "../src/adapters/proxy_router.rs"]
mod proxy_router;
#[path = "../src/adapters/proxifyre.rs"]
mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"]
mod proxy_router;
use models::{
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol,
@@ -33,7 +33,10 @@ fn generates_proxifyre_config_for_discord_external_socks5_target() {
assert!(config.bypass_lan);
assert_eq!(config.proxies.len(), 1);
assert_eq!(config.proxies[0].app_names, vec!["Discord"]);
assert_eq!(config.proxies[0].socks5_proxy_endpoint, "192.168.50.111:8080");
assert_eq!(
config.proxies[0].socks5_proxy_endpoint,
"192.168.50.111:8080"
);
assert_eq!(config.proxies[0].supported_protocols, vec!["TCP", "UDP"]);
}
@@ -57,6 +60,32 @@ fn skips_disabled_profiles_when_generating_proxifyre_config() {
assert!(config.proxies.is_empty());
}
#[test]
fn includes_folder_paths_when_generating_proxifyre_config() {
let adapter = ProxiFyreAdapter::default();
let mut profile = discord_profile("home-gateway");
profile.items.push(ProfileItem {
item_type: ProfileItemType::Folder,
value: r"C:\Games\MyGame".to_string(),
recursive: true,
});
let profiles = vec![profile];
let targets = vec![external_socks5_target()];
let components = Vec::new();
let generated = adapter
.generate_config(ProxyRouterRequest::new(&profiles, &targets, &components))
.expect("folder paths should be accepted by ProxiFyre config generation");
let config: ProxiFyreConfig =
serde_json::from_str(&generated.contents).expect("generated config json");
assert_eq!(generated.routed_apps, 2);
assert_eq!(
config.proxies[0].app_names,
vec!["Discord", r"C:\Games\MyGame"]
);
}
#[test]
fn blocks_local_singbox_target_when_required_component_is_missing() {
let adapter = ProxiFyreAdapter::default();

View File

@@ -1,9 +1,9 @@
#[path = "../src/models.rs"]
mod models;
#[path = "../src/adapters/proxy_router.rs"]
mod proxy_router;
#[path = "../src/adapters/proxifyre.rs"]
mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"]
mod proxy_router;
#[path = "../src/adapters/singbox.rs"]
mod singbox;
@@ -14,8 +14,8 @@ use models::{
use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig};
use proxy_router::{ProxyRouterAdapter, ProxyRouterRequest};
use singbox::{
SingBoxAdapter, SingBoxCheckResult, SingBoxConfig, SingBoxConfigChecker,
SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGenerationRequest, SINGBOX_OUTPUT_FILE,
SingBoxAdapter, SingBoxCheckResult, SingBoxConfig, SingBoxConfigChecker, SingBoxConfigError,
SingBoxConfigErrorKind, SingBoxGenerationRequest, SINGBOX_OUTPUT_FILE,
};
use std::{
cell::RefCell,
@@ -116,7 +116,10 @@ fn blocks_local_singbox_config_when_component_is_not_running() {
)
.expect_err("local sing-box target requires running component");
assert_eq!(error.kind, SingBoxConfigErrorKind::RequiredComponentNotRunning);
assert_eq!(
error.kind,
SingBoxConfigErrorKind::RequiredComponentNotRunning
);
assert!(checker.calls.borrow().is_empty());
}
@@ -129,11 +132,7 @@ fn propagates_failed_singbox_check_as_structured_error() {
let error = adapter
.generate_config(
SingBoxGenerationRequest::new(
&targets,
&components,
Some(Path::new("sing-box.exe")),
),
SingBoxGenerationRequest::new(&targets, &components, Some(Path::new("sing-box.exe"))),
&checker,
)
.expect_err("failed sing-box check should block generated config");
@@ -156,7 +155,10 @@ fn external_proxifyre_apply_does_not_require_singbox_component() {
serde_json::from_str(&generated.contents).expect("generated proxifyre json");
assert_eq!(config.proxies.len(), 1);
assert_eq!(config.proxies[0].socks5_proxy_endpoint, "192.168.50.111:8080");
assert_eq!(
config.proxies[0].socks5_proxy_endpoint,
"192.168.50.111:8080"
);
}
struct RecordingChecker {

View File

@@ -45,7 +45,10 @@ fn roundtrips_profiles_targets_components_and_activity() {
assert_eq!(storage.read_profiles().expect("read profiles"), profiles);
assert_eq!(storage.read_targets().expect("read targets"), targets);
assert_eq!(storage.read_components().expect("read components"), components);
assert_eq!(
storage.read_components().expect("read components"),
components
);
assert_eq!(storage.read_activity().expect("read activity"), activity);
cleanup(&root);