Fix Windows host agent installer reliability (related to #654)
The download endpoint had a dangerous fallback that silently served the wrong binary when the requested platform/arch combination was missing. If a Docker image shipped without Windows binaries, the installer would receive a Linux ELF instead of a Windows PE, causing ERROR_BAD_EXE_FORMAT. Changes: - Download handler now operates in strict mode when platform+arch are specified, returning 404 instead of serving mismatched binaries - PowerShell installer validates PE header (MZ signature) - PowerShell installer verifies PE machine type matches requested arch - PowerShell installer fetches and verifies SHA256 checksums - PowerShell installer shows diagnostic info: OS arch, download URL, file size for better troubleshooting This prevents silent failures and provides clear error messages when binaries are missing or corrupted.
This commit is contained in:
parent
2b7492ac59
commit
16c29463f9
2 changed files with 76 additions and 8 deletions
|
|
@ -3298,9 +3298,10 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
|
||||||
archParam := strings.TrimSpace(req.URL.Query().Get("arch"))
|
archParam := strings.TrimSpace(req.URL.Query().Get("arch"))
|
||||||
|
|
||||||
searchPaths := make([]string, 0, 12)
|
searchPaths := make([]string, 0, 12)
|
||||||
|
strictMode := platformParam != "" && archParam != ""
|
||||||
|
|
||||||
// Try platform-specific binary first
|
// Try platform-specific binary first
|
||||||
if platformParam != "" && archParam != "" {
|
if strictMode {
|
||||||
searchPaths = append(searchPaths,
|
searchPaths = append(searchPaths,
|
||||||
filepath.Join("/opt/pulse/bin", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
|
filepath.Join("/opt/pulse/bin", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
|
||||||
filepath.Join("/opt/pulse", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
|
filepath.Join("/opt/pulse", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
|
||||||
|
|
@ -3308,7 +3309,7 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if platformParam != "" {
|
if platformParam != "" && !strictMode {
|
||||||
searchPaths = append(searchPaths,
|
searchPaths = append(searchPaths,
|
||||||
filepath.Join("/opt/pulse/bin", "pulse-host-agent-"+platformParam),
|
filepath.Join("/opt/pulse/bin", "pulse-host-agent-"+platformParam),
|
||||||
filepath.Join("/opt/pulse", "pulse-host-agent-"+platformParam),
|
filepath.Join("/opt/pulse", "pulse-host-agent-"+platformParam),
|
||||||
|
|
@ -3316,12 +3317,14 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default locations (host architecture)
|
// Default locations (host architecture) - only when no platform/arch specified
|
||||||
searchPaths = append(searchPaths,
|
if !strictMode && platformParam == "" {
|
||||||
filepath.Join("/opt/pulse/bin", "pulse-host-agent"),
|
searchPaths = append(searchPaths,
|
||||||
"/opt/pulse/pulse-host-agent",
|
filepath.Join("/opt/pulse/bin", "pulse-host-agent"),
|
||||||
filepath.Join("/app", "pulse-host-agent"),
|
"/opt/pulse/pulse-host-agent",
|
||||||
)
|
filepath.Join("/app", "pulse-host-agent"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for _, candidate := range searchPaths {
|
for _, candidate := range searchPaths {
|
||||||
if candidate == "" {
|
if candidate == "" {
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,12 @@ switch ($osArch) {
|
||||||
}
|
}
|
||||||
$downloadUrl = "$PulseUrl/download/pulse-host-agent?platform=windows&arch=$arch"
|
$downloadUrl = "$PulseUrl/download/pulse-host-agent?platform=windows&arch=$arch"
|
||||||
|
|
||||||
|
Write-Info "System Information:"
|
||||||
|
Write-Host " OS Architecture: $osArch"
|
||||||
|
Write-Host " Download Architecture: $arch"
|
||||||
|
Write-Host " Download URL: $downloadUrl"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
Write-Info "Downloading agent binary from $downloadUrl..."
|
Write-Info "Downloading agent binary from $downloadUrl..."
|
||||||
try {
|
try {
|
||||||
# Create install directory
|
# Create install directory
|
||||||
|
|
@ -156,6 +162,65 @@ try {
|
||||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
||||||
Write-Success "Downloaded agent to $agentPath"
|
Write-Success "Downloaded agent to $agentPath"
|
||||||
|
|
||||||
|
# Validate PE header
|
||||||
|
$fileBytes = [System.IO.File]::ReadAllBytes($agentPath)
|
||||||
|
$fileSizeMB = [math]::Round($fileBytes.Length / 1MB, 2)
|
||||||
|
Write-Info "File size: $fileSizeMB MB ($($fileBytes.Length) bytes)"
|
||||||
|
|
||||||
|
if ($fileBytes.Length -lt 64) {
|
||||||
|
throw "Downloaded file is too small ($($fileBytes.Length) bytes) - expected Windows PE executable"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for MZ signature (PE header)
|
||||||
|
if ($fileBytes[0] -ne 0x4D -or $fileBytes[1] -ne 0x5A) {
|
||||||
|
$firstBytes = ($fileBytes[0..15] | ForEach-Object { $_.ToString("X2") }) -join " "
|
||||||
|
throw "Downloaded file is not a valid Windows executable (missing MZ signature). First bytes: $firstBytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get PE header offset (at 0x3C)
|
||||||
|
$peOffset = [BitConverter]::ToUInt32($fileBytes, 0x3C)
|
||||||
|
if ($peOffset -ge $fileBytes.Length - 6) {
|
||||||
|
throw "Invalid PE header offset in downloaded file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check PE signature
|
||||||
|
if ($fileBytes[$peOffset] -ne 0x50 -or $fileBytes[$peOffset+1] -ne 0x45) {
|
||||||
|
throw "Downloaded file has invalid PE signature"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check machine type (should be 0x8664 for x64, 0xAA64 for ARM64)
|
||||||
|
$machineType = [BitConverter]::ToUInt16($fileBytes, $peOffset + 4)
|
||||||
|
$expectedMachine = switch ($arch) {
|
||||||
|
'amd64' { 0x8664 }
|
||||||
|
'arm64' { 0xAA64 }
|
||||||
|
'386' { 0x014C }
|
||||||
|
default { 0x0000 }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($machineType -ne $expectedMachine) {
|
||||||
|
$machineStr = "0x" + $machineType.ToString("X4")
|
||||||
|
$expectedStr = "0x" + $expectedMachine.ToString("X4")
|
||||||
|
throw "Downloaded binary is for wrong architecture (got $machineStr, expected $expectedStr for $arch)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Success "Verified PE executable for $osArch architecture"
|
||||||
|
|
||||||
|
# Verify checksum
|
||||||
|
Write-Info "Verifying checksum..."
|
||||||
|
$checksumUrl = "$PulseUrl/download/pulse-host-agent.sha256?platform=windows&arch=$arch"
|
||||||
|
try {
|
||||||
|
$expectedChecksum = (Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing).Content.Trim().Split()[0]
|
||||||
|
$actualChecksum = (Get-FileHash -Path $agentPath -Algorithm SHA256).Hash.ToLower()
|
||||||
|
|
||||||
|
if ($actualChecksum -ne $expectedChecksum.ToLower()) {
|
||||||
|
throw "Checksum mismatch! Expected: $expectedChecksum, Got: $actualChecksum"
|
||||||
|
}
|
||||||
|
Write-Success "Checksum verified: $actualChecksum"
|
||||||
|
} catch {
|
||||||
|
Write-Warning "Could not verify checksum: $_"
|
||||||
|
Write-Info "Continuing anyway (PE header was validated)"
|
||||||
|
}
|
||||||
|
|
||||||
$agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
|
$agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
|
||||||
if ($Token) {
|
if ($Token) {
|
||||||
$agentArgs += @("--token", "`"$Token`"")
|
$agentArgs += @("--token", "`"$Token`"")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue