@@ -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 " ,