From 9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a Mon Sep 17 00:00:00 2001 From: Dokril Date: Thu, 9 Jul 2026 14:20:35 +0300 Subject: [PATCH] Harden ProxiFyre installer downloads --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands.rs | 132 +++++++++++++++++++++++++++++-- src-tauri/tauri.conf.json | 2 +- src-tauri/tests/command_tests.rs | 29 +++++++ 7 files changed, 160 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index c061d4a..07a5fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxywarden", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxywarden", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@fontsource-variable/jetbrains-mono": "^5.2.8", "@tauri-apps/api": "^2.0.0", diff --git a/package.json b/package.json index 990c0bd..e1f48ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxywarden", - "version": "1.0.1", + "version": "1.0.2", "private": true, "type": "module", "description": "Standalone Windows desktop proxy management app for ProxyWarden.", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7527af8..9a9c7fd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2314,7 +2314,7 @@ dependencies = [ [[package]] name = "proxywarden" -version = "1.0.1" +version = "1.0.2" dependencies = [ "base64 0.22.1", "percent-encoding", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 92e8600..c751914 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxywarden" -version = "1.0.1" +version = "1.0.2" description = "Standalone Windows desktop proxy management app for ProxyWarden." authors = ["ProxyWarden"] edition = "2021" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 74733b0..059faa4 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -3607,8 +3607,126 @@ pub fn install_proxifyre_script(generated_config_path: &Path) -> String { return 'x86' } - function Invoke-Download([string]$uri, [string]$path) { - Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'proxywarden' } + function Get-SafeUriForLog([string]$uri) { + try { + $parsed = [Uri]$uri + $port = if ($parsed.IsDefaultPort) { '' } else { ":$($parsed.Port)" } + return "$($parsed.Scheme)://$($parsed.Host)$port$($parsed.AbsolutePath)" + } catch { + return '' + } + } + + function Invoke-ReleaseApi([string]$uri, [string]$label) { + $safeUri = Get-SafeUriForLog $uri + $headers = @{ 'User-Agent' = 'proxywarden'; 'Accept' = 'application/vnd.github+json' } + $lastError = $null + + foreach ($attempt in 1..3) { + try { + return Invoke-RestMethod -Uri $uri -Headers $headers -TimeoutSec 60 -MaximumRedirection 10 + } catch { + $lastError = $_.Exception.Message + if ($attempt -lt 3) { + Start-Sleep -Seconds ([Math]::Min(10, $attempt * 2)) + } + } + } + + throw "Не удалось получить metadata для $label ($safeUri): $lastError" + } + + function Complete-Download([string]$partialPath, [string]$path, [string]$label) { + if (-not (Test-Path -LiteralPath $partialPath)) { + throw "${label}: файл не был создан." + } + + $item = Get-Item -LiteralPath $partialPath + if ($item.Length -le 0) { + throw "${label}: скачанный файл пустой." + } + + Move-Item -LiteralPath $partialPath -Destination $path -Force + } + + function Invoke-WebClientDownload([string]$uri, [string]$partialPath) { + $client = New-Object System.Net.WebClient + try { + $client.Headers.Add('User-Agent', 'proxywarden') + $client.Headers.Add('Accept', 'application/octet-stream,*/*') + $client.DownloadFile($uri, $partialPath) + } finally { + $client.Dispose() + } + } + + function Invoke-CurlDownload([string]$uri, [string]$partialPath) { + $curl = Get-Command 'curl.exe' -ErrorAction SilentlyContinue + if ($null -eq $curl) { + throw 'curl.exe не найден.' + } + + $curlOutput = & $curl.Source --silent --show-error --fail --location --retry 2 --retry-delay 2 --connect-timeout 30 --max-time 180 --user-agent 'proxywarden' --output $partialPath --url $uri 2>&1 + if ($LASTEXITCODE -ne 0) { + $curlMessage = ($curlOutput | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($curlMessage)) { + throw "curl.exe завершился с кодом $LASTEXITCODE." + } + + throw "curl.exe завершился с кодом ${LASTEXITCODE}: $curlMessage" + } + } + + function Invoke-Download([string]$uri, [string]$path, [string]$label) { + $safeUri = Get-SafeUriForLog $uri + $partialPath = "$path.part" + $headers = @{ 'User-Agent' = 'proxywarden'; 'Accept' = 'application/octet-stream,*/*' } + $webRequestError = $null + $webClientError = $null + $curlError = $null + + foreach ($attempt in 1..3) { + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + try { + Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $partialPath -Headers $headers -TimeoutSec 180 -MaximumRedirection 10 + Complete-Download $partialPath $path $label + return + } catch { + $webRequestError = $_.Exception.Message + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + if ($attempt -lt 3) { + Start-Sleep -Seconds ([Math]::Min(10, $attempt * 2)) + } + } + } + + try { + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + Invoke-WebClientDownload $uri $partialPath + Complete-Download $partialPath $path $label + return + } catch { + $webClientError = $_.Exception.Message + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + } + + try { + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + Invoke-CurlDownload $uri $partialPath + Complete-Download $partialPath $path $label + return + } catch { + $curlError = $_.Exception.Message + Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue + } + + $errors = @() + if (-not [string]::IsNullOrWhiteSpace($webRequestError)) { $errors += "Invoke-WebRequest: $webRequestError" } + if (-not [string]::IsNullOrWhiteSpace($webClientError)) { $errors += "WebClient: $webClientError" } + if (-not [string]::IsNullOrWhiteSpace($curlError)) { $errors += "curl.exe: $curlError" } + $details = if ($errors.Count -gt 0) { $errors -join ' | ' } else { 'неизвестная ошибка' } + + throw "Не удалось скачать $label ($safeUri): $details" } function Select-Asset($assets, [string]$pattern, [string]$label) { @@ -3672,7 +3790,7 @@ pub fn install_proxifyre_script(generated_config_path: &Path) -> String { if (-not (Test-VcRuntime $arch)) { $vcRedistPath = Join-Path $workDir 'vc_redist.exe' $vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url } - Invoke-Download $vcRedistUrl $vcRedistPath + Invoke-Download $vcRedistUrl $vcRedistPath 'Microsoft Visual C++ Runtime' $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)." @@ -3680,12 +3798,12 @@ pub fn install_proxifyre_script(generated_config_path: &Path) -> String { } if (-not (Test-WindowsPacketFilter)) { - $ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'proxywarden' } + $ndisRelease = Invoke-ReleaseApi $ndisapiReleaseApi 'Windows Packet Filter' $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 + Invoke-Download $ndisAsset.browser_download_url $ndisPath 'Windows Packet Filter' 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)) { @@ -3694,11 +3812,11 @@ pub fn install_proxifyre_script(generated_config_path: &Path) -> String { } } - $proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'proxywarden' } + $proxifyreRelease = Invoke-ReleaseApi $proxifyreReleaseApi 'ProxiFyre' $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 + Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath 'ProxiFyre' Verify-AssetHash $proxifyreZipPath $proxifyreAsset Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f9ae2a7..125947f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyWarden", - "version": "1.0.1", + "version": "1.0.2", "identifier": "ru.dokops.proxywarden.windows", "build": { "beforeDevCommand": "npm run dev", diff --git a/src-tauri/tests/command_tests.rs b/src-tauri/tests/command_tests.rs index f0029d6..38f6556 100644 --- a/src-tauri/tests/command_tests.rs +++ b/src-tauri/tests/command_tests.rs @@ -251,6 +251,35 @@ fn proxifyre_install_script_parses_as_powershell() { cleanup(&root); } +#[test] +fn proxifyre_install_script_uses_resilient_download_helpers() { + let root = test_root("proxifyre-install-script-downloads"); + let script = commands::install_proxifyre_script(&root.join("proxifyre-app-config.json")); + + assert!(script.contains("function Get-SafeUriForLog([string]$uri)")); + assert!(script.contains("function Invoke-ReleaseApi([string]$uri, [string]$label)")); + assert!( + script.contains("function Invoke-Download([string]$uri, [string]$path, [string]$label)") + ); + assert!(script.contains("foreach ($attempt in 1..3)")); + assert!(script.contains("Invoke-WebClientDownload $uri $partialPath")); + assert!(script.contains("Invoke-CurlDownload $uri $partialPath")); + assert!(script.contains("--user-agent 'proxywarden' --output $partialPath --url $uri")); + assert!(script.contains("Move-Item -LiteralPath $partialPath -Destination $path -Force")); + assert!(script + .contains("Invoke-Download $vcRedistUrl $vcRedistPath 'Microsoft Visual C++ Runtime'")); + assert!(script.contains("Invoke-ReleaseApi $ndisapiReleaseApi 'Windows Packet Filter'")); + assert!(script.contains( + "Invoke-Download $ndisAsset.browser_download_url $ndisPath 'Windows Packet Filter'" + )); + assert!(script.contains("Invoke-ReleaseApi $proxifyreReleaseApi 'ProxiFyre'")); + assert!(script.contains( + "Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath 'ProxiFyre'" + )); + + cleanup(&root); +} + #[test] fn singbox_runner_preserves_installer_args_with_spaces() { let script = commands::singbox_installer_runner_script(