Add VPN proxy connection handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user