private/Invoke-WinPEDriverPackageDownload.ps1
|
#Requires -PSEdition Core function Invoke-WinPEDriverPackageDownload { <# .SYNOPSIS Downloads and verifies a WinPE driver package file .DESCRIPTION Downloads a file with curl.exe, supports resumable transfers, and optionally verifies its SHA256 hash. Unless Force is specified, an existing destination is reused when no hash is supplied or when its hash matches. The function can also copy another file from the destination directory when that file has the expected hash. The function rejects empty responses, checksum mismatches, HTML responses, HTTP 202 responses, and recognized web application firewall challenge pages. Failed or invalid downloads produce terminating errors and remove the destination and temporary response header file when applicable. .PARAMETER Uri Specifies the non-empty URI of the file to download. .PARAMETER DestinationPath Specifies the non-empty destination path, including the file name. The parent directory is created when it does not exist. .PARAMETER SearchUri Specifies an optional URI to send as the HTTP referer. .PARAMETER ExpectedSHA256 Specifies the expected SHA256 hash. Whitespace is ignored and comparison is case-insensitive. .PARAMETER Force Downloads the file even when an existing destination or alternate cache file could be reused. .EXAMPLE PS> Invoke-WinPEDriverPackageDownload -Uri 'https://example.com/winpe-driver.cab' -DestinationPath 'C:\Drivers\winpe-driver.cab' Downloads the package unless the destination file already exists. .EXAMPLE PS> Invoke-WinPEDriverPackageDownload -Uri 'https://example.com/winpe-driver.cab' -DestinationPath 'C:\Drivers\winpe-driver.cab' -ExpectedSHA256 '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF' -Force Downloads the package even when a matching cached file exists and verifies its hash. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.IO.FileInfo. Returns the downloaded, existing, or reused destination file. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires curl.exe, network access for uncached files, and a writable destination directory. This helper does not support WhatIf or Confirm. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Uri, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DestinationPath, [Parameter()] [string]$SearchUri, [Parameter()] [string]$ExpectedSHA256, [Parameter()] [switch]$Force ) $downloadDir = Split-Path -Path $DestinationPath -Parent $destinationLeaf = Split-Path -Path $DestinationPath -Leaf $normalizedExpectedSHA256 = $null if (-not (Test-Path -Path $downloadDir)) { New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null } if (-not [string]::IsNullOrWhiteSpace($ExpectedSHA256)) { $normalizedExpectedSHA256 = ($ExpectedSHA256 -replace '\s+', '').ToUpperInvariant() } $getVerifiedCachedFile = { param([string]$Path) if (-not (Test-Path -Path $Path -PathType Leaf)) { return $null } if (-not $normalizedExpectedSHA256) { return Get-Item -Path $Path } $actualHash = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToUpperInvariant() if ($actualHash -eq $normalizedExpectedSHA256) { return Get-Item -Path $Path } return $null } # Skip if file already exists and hash matches if (-not $Force) { $verifiedCachedFile = & $getVerifiedCachedFile $DestinationPath if ($verifiedCachedFile) { if ($normalizedExpectedSHA256) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] '$destinationLeaf' already downloaded and SHA256 verified. Skipping." } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] '$destinationLeaf' already exists (no checksum to verify). Skipping." } return $verifiedCachedFile } if ($normalizedExpectedSHA256) { $alternateCachedFile = Get-ChildItem -Path $downloadDir -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -ne $DestinationPath } | ForEach-Object { $cachedFile = & $getVerifiedCachedFile $_.FullName if ($cachedFile) { $cachedFile } } | Select-Object -First 1 if ($alternateCachedFile) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Reusing cached file '$($alternateCachedFile.Name)' for '$destinationLeaf'." Copy-Item -Path $alternateCachedFile.FullName -Destination $DestinationPath -Force return Get-Item -Path $DestinationPath } if (Test-Path -Path $DestinationPath -PathType Leaf) { Write-Warning "[$(Get-Date -Format s)] SHA256 mismatch for '$destinationLeaf'. Re-downloading." } } } Write-HostDateTimeDarkGray -Message "Source: $Uri" Write-HostDateTimeDarkGray -Message "Output: $DestinationPath" Write-Verbose "[$($MyInvocation.MyCommand.Name)] Downloading '$(Split-Path $Uri -Leaf)'" $curlHeaderPath = Join-Path $downloadDir "$destinationLeaf.headers.tmp" if (Test-Path -Path $curlHeaderPath -PathType Leaf) { Remove-Item -Path $curlHeaderPath -Force -ErrorAction SilentlyContinue } $curlArgs = @( '--location', '--fail', '--retry', '5', '--continue-at', '-', '--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' ) if ($SearchUri) { $curlArgs += '--referer', $SearchUri } $curlArgs += '--dump-header', $curlHeaderPath $curlArgs += '--output', $DestinationPath, $Uri curl.exe @curlArgs if ($LASTEXITCODE -ne 0) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("curl.exe failed (exit $LASTEXITCODE) downloading '$Uri'"), 'DownloadFailed', [System.Management.Automation.ErrorCategory]::ConnectionError, $Uri ) ) } $downloadStatusCode = $null $downloadContentType = $null if (Test-Path -Path $curlHeaderPath -PathType Leaf) { $headerLines = Get-Content -Path $curlHeaderPath -ErrorAction SilentlyContinue $statusLine = $headerLines | Where-Object { $_ -match '^HTTP/\d(?:\.\d)?\s+(\d{3})' } | Select-Object -Last 1 if ($statusLine -and $statusLine -match '^HTTP/\d(?:\.\d)?\s+(\d{3})') { $downloadStatusCode = [int]$Matches[1] } $contentTypeLine = $headerLines | Where-Object { $_ -match '^(?i)Content-Type\s*:' } | Select-Object -Last 1 if ($contentTypeLine) { $downloadContentType = ($contentTypeLine -replace '^(?i)Content-Type\s*:\s*', '').Trim().ToLowerInvariant() } } $challengeMarkerDetected = $false if (Test-Path -Path $DestinationPath -PathType Leaf) { $challengeProbe = Get-Content -Path $DestinationPath -Raw -ErrorAction SilentlyContinue if ($challengeProbe -and ( $challengeProbe -match 'AwsWafIntegration' -or $challengeProbe -match 'challenge\.js' -or $challengeProbe -match "verify that you're not a robot" -or $challengeProbe -match 'JavaScript is disabled' )) { $challengeMarkerDetected = $true } } if ($challengeMarkerDetected -or $downloadStatusCode -eq 202 -or ($downloadContentType -and $downloadContentType -like 'text/html*')) { Remove-Item -Path $DestinationPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $curlHeaderPath -Force -ErrorAction SilentlyContinue $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Download blocked by Intel AWS WAF challenge for '$destinationLeaf'."), 'WafChallenge', [System.Management.Automation.ErrorCategory]::ConnectionError, $Uri ) ) } if ((Get-Item -Path $DestinationPath -ErrorAction SilentlyContinue).Length -eq 0) { Remove-Item -Path $DestinationPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $curlHeaderPath -Force -ErrorAction SilentlyContinue $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Downloaded file is empty from '$Uri'. The URL may have changed."), 'EmptyDownload', [System.Management.Automation.ErrorCategory]::ConnectionError, $Uri ) ) } if ($normalizedExpectedSHA256) { $actualHash = (Get-FileHash -Path $DestinationPath -Algorithm SHA256).Hash.ToUpperInvariant() if ($actualHash -ne $normalizedExpectedSHA256) { Remove-Item -Path $DestinationPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $curlHeaderPath -Force -ErrorAction SilentlyContinue $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("SHA256 mismatch for '$destinationLeaf'. Expected: $normalizedExpectedSHA256, Actual: $actualHash"), 'ChecksumMismatch', [System.Management.Automation.ErrorCategory]::SecurityError, $DestinationPath ) ) } Write-Verbose "[$($MyInvocation.MyCommand.Name)] SHA256 verified for '$destinationLeaf'" } Remove-Item -Path $curlHeaderPath -Force -ErrorAction SilentlyContinue Get-Item -Path $DestinationPath } |