Add VPN proxy connection handling

This commit is contained in:
2026-07-07 22:33:15 +03:00
parent 7dbf786c56
commit c5bdb10445
8 changed files with 1108 additions and 29 deletions

View File

@@ -28,9 +28,17 @@ use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
const PROXIFYRE_INSTALL_DIR: &str = r"C:\Tools\ProxiFyre";
const PROXIFYRE_RELEASE_API_URL: &str =
"https://api.github.com/repos/wiresock/proxifyre/releases/latest";
const NDISAPI_RELEASE_API_URL: &str =
"https://api.github.com/repos/wiresock/ndisapi/releases/latest";
const VC_REDIST_X64_URL: &str = "https://aka.ms/vc14/vc_redist.x64.exe";
const VC_REDIST_X86_URL: &str = "https://aka.ms/vc14/vc_redist.x86.exe";
#[derive(Debug, Clone)]
pub struct CommandState {
root: PathBuf,
@@ -110,6 +118,24 @@ pub struct SavedStateResponse {
pub generated_config_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxiFyreSetupStatusDto {
pub ready: bool,
pub missing_count: usize,
pub items: Vec<ProxiFyreSetupItemDto>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxiFyreSetupItemDto {
pub id: String,
pub name: String,
pub installed: bool,
pub version: Option<String>,
pub details: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInputDto {
@@ -384,6 +410,13 @@ pub async fn get_components(
.map_err(background_task_error)?
}
#[tauri::command]
pub async fn get_proxifyre_setup_status() -> Result<ProxiFyreSetupStatusDto, CommandError> {
tauri::async_runtime::spawn_blocking(build_proxifyre_setup_status)
.await
.map_err(background_task_error)
}
#[tauri::command]
pub fn resolve_profile_preview(
input: ProfileInputDto,
@@ -444,6 +477,23 @@ pub async fn stop_proxifyre_service() -> Result<ComponentStatusDto, CommandError
.map_err(background_task_error)?
}
#[tauri::command]
pub async fn install_proxifyre(
state: tauri::State<'_, CommandState>,
) -> Result<ComponentStatusDto, CommandError> {
let storage = state.storage();
tauri::async_runtime::spawn_blocking(move || install_proxifyre_component(&storage))
.await
.map_err(background_task_error)?
}
#[tauri::command]
pub async fn uninstall_proxifyre() -> Result<ComponentStatusDto, CommandError> {
tauri::async_runtime::spawn_blocking(uninstall_proxifyre_component)
.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)?;
@@ -1063,7 +1113,7 @@ fn write_elevated_service_script(
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| {
write_powershell_script(&script_path, &script).map_err(|error| {
CommandError::new(
action.error_code(),
format!(
@@ -1161,6 +1211,640 @@ exit 4
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProxiFyrePackageAction {
Install,
Uninstall,
}
impl ProxiFyrePackageAction {
fn error_code(self) -> &'static str {
match self {
ProxiFyrePackageAction::Install => "proxifyre_install_failed",
ProxiFyrePackageAction::Uninstall => "proxifyre_uninstall_failed",
}
}
fn label(self) -> &'static str {
match self {
ProxiFyrePackageAction::Install => "установить",
ProxiFyrePackageAction::Uninstall => "удалить",
}
}
fn file_label(self) -> &'static str {
match self {
ProxiFyrePackageAction::Install => "install",
ProxiFyrePackageAction::Uninstall => "uninstall",
}
}
}
fn install_proxifyre_component(storage: &JsonStorage) -> Result<ComponentStatusDto, CommandError> {
let generated_config_path = storage
.paths()
.generated_dir
.join("proxifyre-app-config.json");
let script = install_proxifyre_script(&generated_config_path);
run_elevated_package_script(
ProxiFyrePackageAction::Install,
script,
&storage.paths().state_dir,
)?;
let refreshed = detect_proxyfier_install();
let Some(detected) = refreshed.as_ref() else {
return Err(CommandError::new(
ProxiFyrePackageAction::Install.error_code(),
"Установка ProxiFyre завершилась, но приложение не найдено после проверки.",
));
};
Ok(ComponentStatusDto::from(
&proxyfier_component_from_detection(Some(detected)),
))
}
fn uninstall_proxifyre_component() -> Result<ComponentStatusDto, CommandError> {
let Some(detected) = detect_proxyfier_install() else {
let component = proxyfier_component_from_detection(None);
return Ok(ComponentStatusDto::from(&component));
};
ensure_safe_proxifyre_install_dir(&detected.install_dir)?;
let script = uninstall_proxifyre_script(&detected.install_dir, &detected.executable_path);
let artifact_dir = default_config_root().join("state");
run_elevated_package_script(ProxiFyrePackageAction::Uninstall, script, &artifact_dir)?;
let refreshed = detect_proxyfier_install();
if refreshed.is_some() {
return Err(CommandError::new(
ProxiFyrePackageAction::Uninstall.error_code(),
"Удаление ProxiFyre завершилось, но приложение все еще найдено на компьютере.",
));
}
let component = proxyfier_component_from_detection(None);
Ok(ComponentStatusDto::from(&component))
}
fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto {
let vc_runtime = detect_vc_runtime();
let packet_filter = detect_windows_packet_filter();
let proxifyre = detect_proxyfier_install();
let vc_runtime_item = setup_item_from_program(
"vc-runtime",
&format!("Microsoft Visual C++ Runtime ({})", runtime_arch_label()),
vc_runtime,
"Нужен для запуска ProxiFyre.exe. Установщик скачает официальный vc_redist от Microsoft.",
);
let packet_filter_item = setup_item_from_program(
"packet-filter",
"Windows Packet Filter",
packet_filter,
"Сетевой драйвер NT Kernel/WireSock, через который ProxiFyre перехватывает трафик приложений.",
);
let proxifyre_item = match proxifyre {
Some(detected) => ProxiFyreSetupItemDto {
id: "proxifyre".to_string(),
name: "ProxiFyre".to_string(),
installed: true,
version: Some(if detected.running {
"служба запущена".to_string()
} else {
"служба не запущена".to_string()
}),
details: detected.install_dir.display().to_string(),
},
None => ProxiFyreSetupItemDto {
id: "proxifyre".to_string(),
name: "ProxiFyre".to_string(),
installed: false,
version: None,
details: format!(
"Будет скачан с GitHub и распакован в {}.",
PROXIFYRE_INSTALL_DIR
),
},
};
let items = vec![vc_runtime_item, packet_filter_item, proxifyre_item];
let missing_count = items.iter().filter(|item| !item.installed).count();
ProxiFyreSetupStatusDto {
ready: missing_count == 0,
missing_count,
items,
}
}
fn setup_item_from_program(
id: &str,
name: &str,
program: Option<InstalledProgram>,
missing_details: &str,
) -> ProxiFyreSetupItemDto {
match program {
Some(program) => ProxiFyreSetupItemDto {
id: id.to_string(),
name: name.to_string(),
installed: true,
version: program.display_version,
details: program.display_name,
},
None => ProxiFyreSetupItemDto {
id: id.to_string(),
name: name.to_string(),
installed: false,
version: None,
details: missing_details.to_string(),
},
}
}
#[derive(Debug, Clone)]
struct InstalledProgram {
display_name: String,
display_version: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InstalledProgramJson {
display_name: Option<String>,
display_version: Option<String>,
}
fn detect_vc_runtime() -> Option<InstalledProgram> {
installed_program(&vc_runtime_registry_pattern())
}
fn detect_windows_packet_filter() -> Option<InstalledProgram> {
installed_program("Windows Packet Filter|WinpkFilter|NDISAPI")
}
fn installed_program(pattern: &str) -> Option<InstalledProgram> {
let script = format!(
r#"
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$program = Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
Where-Object {{ $_.DisplayName -match '{}' }} |
Select-Object -First 1 DisplayName, DisplayVersion
if ($null -ne $program) {{
$program | ConvertTo-Json -Compress
}}
"#,
escape_powershell_single(pattern)
);
let output = Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
script.as_str(),
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let payload = stdout.trim();
if payload.is_empty() || payload.eq_ignore_ascii_case("null") {
return None;
}
let parsed: InstalledProgramJson = serde_json::from_str(payload).ok()?;
let display_name = parsed.display_name?.trim().to_string();
if display_name.is_empty() {
return None;
}
Some(InstalledProgram {
display_name,
display_version: parsed
.display_version
.map(|version| version.trim().to_string())
.filter(|version| !version.is_empty()),
})
}
fn vc_runtime_registry_pattern() -> String {
let arch = runtime_arch_label();
if arch == "ARM64" {
return r"Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)".to_string();
}
format!(r"Microsoft Visual C\+\+.*Redistributable.*\({arch}\)")
}
fn runtime_arch_label() -> &'static str {
if cfg!(target_arch = "aarch64") {
"ARM64"
} else if cfg!(target_arch = "x86") {
"x86"
} else {
"x64"
}
}
fn run_elevated_package_script(
action: ProxiFyrePackageAction,
body: String,
artifact_dir: &Path,
) -> Result<(), CommandError> {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or(0);
fs::create_dir_all(artifact_dir).map_err(|error| {
CommandError::new(
action.error_code(),
format!(
"Не удалось создать папку для временных файлов ProxiFyre '{}': {error}",
artifact_dir.display()
),
)
})?;
let script_path = artifact_dir.join(format!(
"vpn-proxy-proxifyre-{}-{nonce}.ps1",
action.file_label()
));
let result_path = artifact_dir.join(format!(
"vpn-proxy-proxifyre-{}-{nonce}.log",
action.file_label()
));
let script = wrap_elevated_package_script(&body, &result_path);
write_powershell_script(&script_path, &script).map_err(|error| {
CommandError::new(
action.error_code(),
format!(
"Не удалось подготовить временный скрипт, чтобы {} ProxiFyre '{}': {error}",
action.label(),
script_path.display()
),
)
})?;
let launch_script = format!(
r#"
$ErrorActionPreference = 'Stop'
$resultPath = '{}'
try {{
$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
if ($null -eq $p) {{
Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
exit 1
}}
exit $p.ExitCode
}} catch {{
Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
exit 1
}}
"#,
escape_powershell_single(&result_path.display().to_string()),
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() => {
let _ = fs::remove_file(&result_path);
Ok(())
}
Ok(output) => {
let details = package_failure_details(&result_path, &output);
let _ = fs::remove_file(&result_path);
Err(CommandError::new(
action.error_code(),
format!(
"Не удалось {} ProxiFyre. Код elevated-команды: {}. {details}",
action.label(),
output.status.code().unwrap_or(-1),
),
))
}
Err(error) => Err(CommandError::new(
action.error_code(),
format!(
"Не удалось запросить права администратора, чтобы {} ProxiFyre: {error}",
action.label()
),
)),
}
}
pub(crate) fn wrap_elevated_package_script(body: &str, result_path: &Path) -> String {
let mut script = String::new();
script.push_str("$ErrorActionPreference = 'Stop'\n");
script.push_str(&format!(
"$resultPath = '{}'\n",
escape_powershell_single(&result_path.display().to_string())
));
script.push_str("try {\n");
script.push_str(body);
script.push_str(
r#"
Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
exit 0
} catch {
$message = ($_ | Out-String)
Set-Content -LiteralPath $resultPath -Value $message -Encoding UTF8
exit 1
}
"#,
);
script
}
fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> {
let mut bytes = Vec::with_capacity(script.len() + 3);
bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]);
bytes.extend_from_slice(script.as_bytes());
fs::write(path, bytes)
}
pub(crate) fn install_proxifyre_script(generated_config_path: &Path) -> String {
let mut script = String::new();
script.push_str(&format!(
"$targetDir = '{}'\n",
escape_powershell_single(PROXIFYRE_INSTALL_DIR)
));
script.push_str(&format!(
"$generatedConfigPath = '{}'\n",
escape_powershell_single(&generated_config_path.display().to_string())
));
script.push_str(&format!(
"$proxifyreReleaseApi = '{}'\n",
escape_powershell_single(PROXIFYRE_RELEASE_API_URL)
));
script.push_str(&format!(
"$ndisapiReleaseApi = '{}'\n",
escape_powershell_single(NDISAPI_RELEASE_API_URL)
));
script.push_str(&format!(
"$vcRedistX64Url = '{}'\n",
escape_powershell_single(VC_REDIST_X64_URL)
));
script.push_str(&format!(
"$vcRedistX86Url = '{}'\n",
escape_powershell_single(VC_REDIST_X86_URL)
));
script.push_str(
r#"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Get-NativeArchitecture {
$processor = Get-CimInstance Win32_Processor | Select-Object -First 1
if ($null -ne $processor -and $processor.Architecture -eq 12) { return 'ARM64' }
if ([Environment]::Is64BitOperatingSystem) { return 'x64' }
return 'x86'
}
function Invoke-Download([string]$uri, [string]$path) {
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
}
function Select-Asset($assets, [string]$pattern, [string]$label) {
$asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1
if ($null -eq $asset) { throw "Не найден подходящий asset для $label ($pattern)." }
return $asset
}
function Verify-AssetHash([string]$path, $asset) {
if ($asset.digest -match '^sha256:(.+)$') {
$expected = $Matches[1].ToLowerInvariant()
$actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "SHA256 не совпал для $($asset.name). Ожидалось $expected, получилось $actual."
}
}
}
function Assert-ExitCode($process, [string]$label) {
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
throw "$label завершился с кодом $($process.ExitCode)."
}
}
function Get-InstalledProgram([string]$pattern) {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -match $pattern } |
Select-Object -First 1
}
function Test-VcRuntime([string]$arch) {
$pattern = if ($arch -eq 'ARM64') {
'Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)'
} else {
"Microsoft Visual C\+\+.*Redistributable.*\($arch\)"
}
return $null -ne (Get-InstalledProgram $pattern)
}
function Test-WindowsPacketFilter {
return $null -ne (Get-InstalledProgram 'Windows Packet Filter|WinpkFilter|NDISAPI')
}
function Get-LogTail([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return '' }
return (Get-Content -LiteralPath $path -Tail 40 -ErrorAction SilentlyContinue) -join ' '
}
$arch = Get-NativeArchitecture
$workDir = Join-Path ([IO.Path]::GetTempPath()) 'vpn-proxy-proxifyre-install'
$extractDir = Join-Path $workDir 'proxifyre'
Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $workDir, $extractDir, $targetDir | Out-Null
if (-not (Test-VcRuntime $arch)) {
$vcRedistPath = Join-Path $workDir 'vc_redist.exe'
$vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url }
Invoke-Download $vcRedistUrl $vcRedistPath
$vcProcess = Start-Process -FilePath $vcRedistPath -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru -WindowStyle Hidden
if ($vcProcess.ExitCode -ne 0 -and $vcProcess.ExitCode -ne 3010 -and $vcProcess.ExitCode -ne 1638 -and -not (Test-VcRuntime $arch)) {
throw "Visual C++ Runtime завершился с кодом $($vcProcess.ExitCode)."
}
}
if (-not (Test-WindowsPacketFilter)) {
$ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
$ndisPattern = if ($arch -eq 'ARM64') { 'ARM64\.msi$' } elseif ($arch -eq 'x86') { 'x86\.msi$' } else { 'x64\.msi$' }
$ndisAsset = Select-Asset $ndisRelease.assets $ndisPattern 'Windows Packet Filter'
$ndisPath = Join-Path $workDir $ndisAsset.name
$ndisLogPath = Join-Path $workDir 'windows-packet-filter-install.log'
Invoke-Download $ndisAsset.browser_download_url $ndisPath
Verify-AssetHash $ndisPath $ndisAsset
$ndisProcess = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', $ndisPath, '/qn', '/norestart', '/L*v', $ndisLogPath) -Wait -PassThru -WindowStyle Hidden
if ($ndisProcess.ExitCode -ne 0 -and $ndisProcess.ExitCode -ne 3010 -and -not (Test-WindowsPacketFilter)) {
$ndisLogTail = Get-LogTail $ndisLogPath
throw "Windows Packet Filter завершился с кодом $($ndisProcess.ExitCode). MSI log: $ndisLogPath $ndisLogTail"
}
}
$proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
$proxifyrePattern = if ($arch -eq 'ARM64') { 'ARM64-signed\.zip$' } elseif ($arch -eq 'x86') { 'x86-signed\.zip$' } else { 'x64-signed\.zip$' }
$proxifyreAsset = Select-Asset $proxifyreRelease.assets $proxifyrePattern 'ProxiFyre'
$proxifyreZipPath = Join-Path $workDir $proxifyreAsset.name
Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath
Verify-AssetHash $proxifyreZipPath $proxifyreAsset
Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force
$proxifyreExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'ProxiFyre.exe' | Select-Object -First 1
if ($null -eq $proxifyreExe) { throw 'В архиве ProxiFyre не найден ProxiFyre.exe.' }
Copy-Item -Path (Join-Path $proxifyreExe.Directory.FullName '*') -Destination $targetDir -Recurse -Force
$configTarget = Join-Path $targetDir 'app-config.json'
if (Test-Path -LiteralPath $generatedConfigPath) {
Copy-Item -LiteralPath $generatedConfigPath -Destination $configTarget -Force
} elseif (-not (Test-Path -LiteralPath $configTarget)) {
$emptyConfig = '{"logLevel":"Info","bypassLan":true,"proxies":[]}'
Set-Content -LiteralPath $configTarget -Value $emptyConfig -Encoding UTF8
}
Push-Location $targetDir
try {
& .\ProxiFyre.exe stop | Out-Null
& .\ProxiFyre.exe uninstall | Out-Null
& .\ProxiFyre.exe install
if ($LASTEXITCODE -ne 0) { throw "ProxiFyre.exe install завершился с кодом $LASTEXITCODE." }
& .\ProxiFyre.exe start
if ($LASTEXITCODE -ne 0) {
Start-Service -Name 'ProxiFyreService' -ErrorAction Stop
}
} finally {
Pop-Location
}
"#,
);
script
}
fn uninstall_proxifyre_script(install_dir: &Path, executable_path: &Path) -> String {
let mut script = String::new();
script.push_str(&format!(
"$installDir = '{}'\n",
escape_powershell_single(&install_dir.display().to_string())
));
script.push_str(&format!(
"$exePath = '{}'\n",
escape_powershell_single(&executable_path.display().to_string())
));
script.push_str(
r#"
function Find-ProxiFyreService {
foreach ($name in @('ProxiFyreService', 'ProxiFyre')) {
$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
}
$service = Find-ProxiFyreService
if ($null -ne $service -and $service.Status -ne 'Stopped') {
try {
if ($service.CanStop) { Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue }
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service) { $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }
} catch {}
}
$service = Find-ProxiFyreService
if ($null -ne $service -and $service.Status -ne 'Stopped') {
$processId = Get-ServiceProcessId $service.Name
if ($processId -gt 0) {
taskkill.exe /PID $processId /F | Out-Null
Start-Sleep -Milliseconds 700
}
}
if (Test-Path -LiteralPath $exePath) {
Push-Location (Split-Path -Parent $exePath)
try {
& $exePath uninstall | Out-Null
} finally {
Pop-Location
}
}
$service = Find-ProxiFyreService
if ($null -ne $service) {
sc.exe delete $service.Name | Out-Null
}
Get-Process -Name 'ProxiFyre' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $installDir) {
Remove-Item -LiteralPath $installDir -Recurse -Force
}
"#,
);
script
}
fn ensure_safe_proxifyre_install_dir(path: &Path) -> Result<(), CommandError> {
let name = path
.file_name()
.and_then(|value| value.to_str())
.map(|value| value.to_ascii_lowercase())
.unwrap_or_default();
if path.parent().is_some() && name.contains("proxifyre") {
return Ok(());
}
Err(CommandError::new(
ProxiFyrePackageAction::Uninstall.error_code(),
format!(
"Отказываюсь рекурсивно удалять папку ProxiFyre с небезопасным путем: {}",
path.display()
),
))
}
fn apply_to_detected_proxyfier(
request: HelperApplyRequest<'_>,
detected: &DetectedProxyfier,
@@ -1280,6 +1964,55 @@ fn background_task_error(error: impl std::fmt::Display) -> CommandError {
)
}
fn package_failure_details(result_path: &Path, output: &Output) -> String {
let mut parts = Vec::new();
if let Ok(contents) = fs::read_to_string(result_path) {
let details = compact_error_text(&contents);
if !details.is_empty() && !details.eq_ignore_ascii_case("ok") {
parts.push(details);
}
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = compact_error_text(&stdout);
if !stdout.is_empty() {
parts.push(format!("stdout: {stdout}"));
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = compact_error_text(&stderr);
if !stderr.is_empty() {
parts.push(format!("stderr: {stderr}"));
}
if parts.is_empty() {
parts.push(
"Лог elevated-скрипта не создан. Обычно это значит, что окно UAC было отменено или Windows не дала запустить elevated PowerShell."
.to_string(),
);
}
parts.join(" ")
}
fn compact_error_text(value: &str) -> String {
let text = value
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ");
let max_chars = 1400;
if text.chars().count() <= max_chars {
return text;
}
let truncated = text.chars().take(max_chars).collect::<String>();
format!("{truncated}...")
}
fn parse_service_command_output(stdout: &[u8]) -> Option<ServiceCommandOutput> {
let stdout = String::from_utf8_lossy(stdout);
let payload = stdout

View File

@@ -34,12 +34,15 @@ fn main() {
commands::get_targets,
commands::save_target,
commands::get_components,
commands::get_proxifyre_setup_status,
commands::resolve_profile_preview,
commands::apply_profiles,
commands::get_logs,
commands::open_config_location,
commands::start_proxifyre_service,
commands::stop_proxifyre_service
commands::stop_proxifyre_service,
commands::install_proxifyre,
commands::uninstall_proxifyre
])
.run(tauri::generate_context!())
.expect("не удалось запустить клиент VPN Proxy для Windows");

View File

@@ -6,7 +6,7 @@
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"devUrl": "http://127.0.0.1:5173",
"frontendDist": "../dist"
},
"app": {

View File

@@ -32,6 +32,8 @@ use proxifyre::ProxiFyreAdapter;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command as ProcessCommand;
use std::time::{SystemTime, UNIX_EPOCH};
use storage::JsonStorage;
@@ -119,6 +121,40 @@ fn resolve_preview_returns_structured_apps_without_filesystem_scan() {
.any(|warning| warning.contains("Сканирование папок отложено")));
}
#[test]
#[cfg(windows)]
fn proxifyre_install_script_parses_as_powershell() {
let root = test_root("proxifyre-install-script");
fs::create_dir_all(&root).expect("test root should be created");
let script = commands::wrap_elevated_package_script(
&commands::install_proxifyre_script(&root.join("proxifyre-app-config.json")),
&root.join("install.log"),
);
let script_path = root.join("install.ps1");
let mut script_bytes = vec![0xEF, 0xBB, 0xBF];
script_bytes.extend_from_slice(script.as_bytes());
fs::write(&script_path, script_bytes).expect("script should be written");
let escaped_path = script_path.display().to_string().replace('\'', "''");
let parser = format!(
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
);
let output = ProcessCommand::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
.output()
.expect("powershell parser should run");
assert!(
output.status.success(),
"install script should parse\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
cleanup(&root);
}
#[test]
fn apply_generates_derived_config_and_records_activity_with_mock_helper() {
let root = test_root("apply");