251 lines
8.3 KiB
PowerShell
251 lines
8.3 KiB
PowerShell
param(
|
|
[string]$InstallRoot = "",
|
|
[switch]$ForceRemoveWindowsPacketFilter
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function New-Result {
|
|
param(
|
|
[bool]$Success,
|
|
[string]$Message,
|
|
[hashtable]$Details = @{}
|
|
)
|
|
|
|
[ordered]@{
|
|
success = $Success
|
|
message = $Message
|
|
details = $Details
|
|
} | ConvertTo-Json -Depth 8 -Compress
|
|
}
|
|
|
|
function Get-FullPath([string]$Path) {
|
|
return [System.IO.Path]::GetFullPath($Path).TrimEnd("\")
|
|
}
|
|
|
|
function Test-PathInside([string]$Path, [string]$Root) {
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
|
try {
|
|
$fullPath = Get-FullPath $Path
|
|
$fullRoot = Get-FullPath $Root
|
|
return $fullPath.StartsWith($fullRoot + "\", [StringComparison]::OrdinalIgnoreCase)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Assert-SafeInstallRoot([string]$Root) {
|
|
if ([string]::IsNullOrWhiteSpace($Root)) {
|
|
throw "InstallRoot is empty."
|
|
}
|
|
|
|
$full = Get-FullPath $Root
|
|
if ($full -match "^[A-Za-z]:\\?$") {
|
|
throw "Refusing to use drive root as InstallRoot: $full"
|
|
}
|
|
if ($full -match "\\Windows($|\\)" -or $full -match "\\ProgramData$" -or $full -match "\\Users$") {
|
|
throw "Refusing unsafe InstallRoot: $full"
|
|
}
|
|
|
|
$knownAppFiles = @(
|
|
(Join-Path $full "proxywarden.exe"),
|
|
(Join-Path $full "uninstall.exe"),
|
|
(Join-Path $full "bundled\cleanup\uninstall-managed-components.ps1")
|
|
)
|
|
foreach ($candidate in $knownAppFiles) {
|
|
if (Test-Path -LiteralPath $candidate) { return $full }
|
|
}
|
|
|
|
throw "InstallRoot does not look like a ProxyWarden install directory: $full"
|
|
}
|
|
|
|
function Resolve-SafeComponentDir([string]$Root, [string]$Leaf) {
|
|
$componentRoot = Join-Path $Root "components"
|
|
$path = Join-Path $componentRoot $Leaf
|
|
$full = Get-FullPath $path
|
|
$expectedParent = Get-FullPath $componentRoot
|
|
$actualLeaf = Split-Path -Leaf $full
|
|
|
|
if ($actualLeaf -ne $Leaf) {
|
|
throw "Unexpected component directory leaf: $full"
|
|
}
|
|
if (-not $full.StartsWith($expectedParent + "\", [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Component directory is outside ProxyWarden components root: $full"
|
|
}
|
|
|
|
return $full
|
|
}
|
|
|
|
function Read-ComponentMarker([string]$Dir) {
|
|
$markerPath = Join-Path $Dir "proxywarden-component.json"
|
|
if (-not (Test-Path -LiteralPath $markerPath)) { return $null }
|
|
try {
|
|
return Get-Content -LiteralPath $markerPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Get-MarkerBool($Marker, [string]$Name) {
|
|
if ($null -eq $Marker) { return $false }
|
|
$property = $Marker.PSObject.Properties[$Name]
|
|
if ($null -eq $property) { return $false }
|
|
return [bool]$property.Value
|
|
}
|
|
|
|
function Get-ServiceRecord([string]$Name) {
|
|
$escaped = $Name.Replace("'", "''")
|
|
return Get-CimInstance Win32_Service -Filter "Name='$escaped'" -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Get-ServiceImagePath($Record) {
|
|
if ($null -eq $Record -or [string]::IsNullOrWhiteSpace([string]$Record.PathName)) {
|
|
return $null
|
|
}
|
|
|
|
$pathName = ([string]$Record.PathName).Trim()
|
|
if ($pathName -match '^"([^"]+)"') { return $Matches[1] }
|
|
if ($pathName -match '^(.+?\.exe)\b') { return $Matches[1].Trim() }
|
|
return $pathName
|
|
}
|
|
|
|
function Stop-ServiceRecord($Record) {
|
|
if ($null -eq $Record) { return }
|
|
|
|
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
|
if ($null -ne $service -and $service.Status -ne "Stopped") {
|
|
Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
|
|
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
|
if ($null -ne $service) {
|
|
try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(12)) } catch {}
|
|
}
|
|
}
|
|
|
|
$recordAfterStop = Get-ServiceRecord $Record.Name
|
|
if ($null -ne $recordAfterStop -and [int]$recordAfterStop.ProcessId -gt 0) {
|
|
taskkill.exe /PID ([int]$recordAfterStop.ProcessId) /F | Out-Null
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
}
|
|
|
|
function Remove-ManagedService {
|
|
param(
|
|
[string[]]$Names,
|
|
[string]$InstallRoot,
|
|
[string]$UninstallExe = ""
|
|
)
|
|
|
|
$removed = @()
|
|
foreach ($name in $Names) {
|
|
$record = Get-ServiceRecord $name
|
|
if ($null -eq $record) { continue }
|
|
|
|
$imagePath = Get-ServiceImagePath $record
|
|
if (-not [string]::IsNullOrWhiteSpace($imagePath) -and -not (Test-PathInside $imagePath $InstallRoot)) {
|
|
continue
|
|
}
|
|
|
|
Stop-ServiceRecord $record
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($UninstallExe) -and (Test-Path -LiteralPath $UninstallExe)) {
|
|
Push-Location (Split-Path -Parent $UninstallExe)
|
|
try { & $UninstallExe uninstall | Out-Null } finally { Pop-Location }
|
|
}
|
|
|
|
$record = Get-ServiceRecord $name
|
|
if ($null -ne $record) {
|
|
sc.exe delete $name | Out-Null
|
|
}
|
|
$removed += $name
|
|
}
|
|
|
|
return $removed
|
|
}
|
|
|
|
function Remove-SafeDirectory([string]$Path, [string]$Root) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
|
if (-not (Test-PathInside $Path $Root)) {
|
|
throw "Refusing to remove directory outside InstallRoot: $Path"
|
|
}
|
|
Remove-Item -LiteralPath $Path -Recurse -Force
|
|
return $true
|
|
}
|
|
|
|
function Remove-ManagedFirewallRules {
|
|
$removed = @()
|
|
foreach ($name in @("ProxyWarden.ProxiFyre.Inbound", "ProxyWarden.ProxiFyre.Outbound")) {
|
|
$rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue
|
|
if ($null -eq $rule) { continue }
|
|
$rule | Remove-NetFirewallRule -ErrorAction Stop
|
|
$removed += $name
|
|
}
|
|
return $removed
|
|
}
|
|
|
|
function Get-InstalledProgram([string]$Pattern) {
|
|
$paths = @(
|
|
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
|
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
|
)
|
|
return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.DisplayName -match $Pattern } |
|
|
Select-Object -First 1 DisplayName, DisplayVersion, PSChildName, UninstallString, QuietUninstallString
|
|
}
|
|
|
|
function Resolve-MsiProductCode($Program, [string]$Label) {
|
|
if ($null -eq $Program) { return $null }
|
|
if ($Program.PSChildName -match "^\{[0-9A-Fa-f-]{36}\}$") {
|
|
return $Program.PSChildName
|
|
}
|
|
foreach ($candidate in @($Program.QuietUninstallString, $Program.UninstallString)) {
|
|
if ($candidate -match "\{[0-9A-Fa-f-]{36}\}") {
|
|
return $Matches[0]
|
|
}
|
|
}
|
|
throw "Could not resolve MSI product code for $Label."
|
|
}
|
|
|
|
function Uninstall-MsiProgram($Program, [string]$Label) {
|
|
$productCode = Resolve-MsiProductCode $Program $Label
|
|
if ([string]::IsNullOrWhiteSpace($productCode)) { return $false }
|
|
|
|
$logPath = Join-Path ([System.IO.Path]::GetTempPath()) "proxywarden-$Label-uninstall.log"
|
|
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList @("/x", $productCode, "/qn", "/norestart", "/L*v", $logPath) -Wait -PassThru -WindowStyle Hidden
|
|
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010 -and $process.ExitCode -ne 1605) {
|
|
throw "$Label uninstall exited with code $($process.ExitCode). MSI log: $logPath"
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
try {
|
|
$details = @{}
|
|
$root = Assert-SafeInstallRoot $InstallRoot
|
|
$details.installRoot = $root
|
|
|
|
$proxifyreDir = Resolve-SafeComponentDir $root "ProxiFyre"
|
|
$singboxDir = Resolve-SafeComponentDir $root "sing-box"
|
|
$proxifyreMarker = Read-ComponentMarker $proxifyreDir
|
|
$removePacketFilter = [bool]$ForceRemoveWindowsPacketFilter -or (Get-MarkerBool $proxifyreMarker "packetFilterInstalledByProxyWarden")
|
|
|
|
$details.removedProxiFyreServices = Remove-ManagedService -Names @("ProxiFyreService", "ProxiFyre") -InstallRoot $root -UninstallExe (Join-Path $proxifyreDir "ProxiFyre.exe")
|
|
$details.removedSingBoxServices = Remove-ManagedService -Names @("ProxyWardenSingBox") -InstallRoot $root -UninstallExe (Join-Path $singboxDir "ProxyWardenSingBox.exe")
|
|
$details.removedProxiFyreFirewallRules = Remove-ManagedFirewallRules
|
|
$details.removedProxiFyreDir = Remove-SafeDirectory $proxifyreDir $root
|
|
$details.removedSingBoxDir = Remove-SafeDirectory $singboxDir $root
|
|
|
|
if ($removePacketFilter) {
|
|
$packetFilter = Get-InstalledProgram "Windows Packet Filter|WinpkFilter|NDISAPI"
|
|
$details.removedWindowsPacketFilter = Uninstall-MsiProgram $packetFilter "windows-packet-filter"
|
|
} else {
|
|
$details.removedWindowsPacketFilter = $false
|
|
}
|
|
|
|
New-Result -Success $true -Message "ProxyWarden managed components cleanup completed." -Details $details
|
|
exit 0
|
|
} catch {
|
|
New-Result -Success $false -Message $_.Exception.Message -Details @{}
|
|
exit 1
|
|
}
|