Switch to using the new JSON Windows download API

* This simplifies things quite a bit since we were converting HTML to XML to JSON before.
* Also drop the custom User Agent for now.
  We'll have to see how long it takes Microsoft to go back to filtering out PowerShell...
* Closes #98.
This commit is contained in:
Pete Batard
2024-11-19 22:42:39 +00:00
parent 5402fc6222
commit ce405fa35c

272
Fido.ps1
View File

@@ -1,5 +1,5 @@
# #
# Fido v1.61 - Feature ISO Downloader, for retail Windows images and UEFI Shell # Fido v1.62 - ISO Downloader, for Microsoft Windows and UEFI Shell
# Copyright © 2019-2024 Pete Batard <pete@akeo.ie> # Copyright © 2019-2024 Pete Batard <pete@akeo.ie>
# Command line support: Copyright © 2021 flx5 # Command line support: Copyright © 2021 flx5
# ConvertTo-ImageSource: Copyright © 2016 Chris Carter # ConvertTo-ImageSource: Copyright © 2016 Chris Carter
@@ -24,7 +24,7 @@
#region Parameters #region Parameters
param( param(
# (Optional) The title to display on the application window. # (Optional) The title to display on the application window.
[string]$AppTitle = "Fido - Feature ISO Downloader", [string]$AppTitle = "Fido - ISO Downloader",
# (Optional) '|' separated UI localization strings. # (Optional) '|' separated UI localization strings.
[string]$LocData, [string]$LocData,
# (Optional) Forced locale # (Optional) Forced locale
@@ -346,17 +346,6 @@ function ConvertTo-ImageSource
} }
} }
function Throw-Error([object]$Req, [string]$Alt)
{
$Err = $(GetElementById -Request $Req -Id "errorModalMessage").innerText -replace "<[^>]+>" -replace "\s+", " "
if (!$Err) {
$Err = $Alt
} else {
$Err = [System.Text.Encoding]::UTF8.GetString([byte[]][char[]]$Err)
}
throw $Err
}
# Translate a message string # Translate a message string
function Get-Translation([string]$Text) function Get-Translation([string]$Text)
{ {
@@ -398,14 +387,14 @@ function Get-Arch
} }
} }
# Some PowerShells don't have Microsoft.mshtml assembly (comes with MS Office?) # Convert a Microsoft arch type code to a formal architecture name
# so we can't use ParsedHtml or IHTMLDocument[2|3] features there... function Get-Arch-From-Type([int]$Type)
function GetElementById([object]$Request, [string]$Id)
{ {
try { switch($Type) {
return $Request.ParsedHtml.IHTMLDocument3_GetElementByID($Id) 0 { return "x86" }
} catch { 1 { return "x64" }
return $Request.AllElements | ? {$_.id -eq $Id} 2 { return "ARM64" }
default { return "Unknown"}
} }
} }
@@ -422,17 +411,6 @@ function Error([string]$ErrorMessage)
$script:ExitCode = 2 $script:ExitCode = 2
} }
} }
function Get-RandomDate()
{
[DateTime]$Min = "1/1/2008"
[DateTime]$Max = [DateTime]::Now
$RandomGen = new-object random
$RandomTicks = [Convert]::ToInt64( ($Max.ticks * 1.0 - $Min.Ticks * 1.0 ) * $RandomGen.NextDouble() + $Min.Ticks * 1.0 )
$Date = new-object DateTime($RandomTicks)
return $Date.ToString("yyyyMMdd")
}
#endregion #endregion
#region Form #region Form
@@ -466,15 +444,8 @@ $MaxStage = 4
$SessionId = @($null) * 2 $SessionId = @($null) * 2
$ExitCode = 100 $ExitCode = 100
$Locale = $Locale $Locale = $Locale
$RequestData = @{} $OrgId = "y6jn8c31"
# This GUID applies to all visitors, regardless of their locale $ProfileId = "606624d44113"
$RequestData["GetLangs"] = @("a8f8f489-4c7f-463a-9ca6-5cff94d8d041", "getskuinformationbyproductedition" )
# This GUID applies to visitors of the en-US download page. Other locales may get a different GUID.
$RequestData["GetLinks"] = @("6e2a1789-ef16-4f27-a296-74ef7ef5d96b", "GetProductDownloadLinksBySku" )
# Create a semi-random Linux User-Agent string
$FirefoxVersion = Get-Random -Minimum 110 -Maximum 135
$FirefoxDate = Get-RandomDate
$UserAgent = "Mozilla/5.0 (X11; Linux i586; rv:$FirefoxVersion.0) Gecko/$FirefoxDate Firefox/$FirefoxVersion.0"
$Verbosity = 1 $Verbosity = 1
if ($Debug) { if ($Debug) {
$Verbosity = 5 $Verbosity = 5
@@ -527,13 +498,7 @@ function Check-Locale
if ($Verbosity -ge 2) { if ($Verbosity -ge 2) {
Write-Host Querying $url Write-Host Querying $url
} }
# Looks Microsoft are filtering our script according to the first query it performs with the spoofed user agent.
# So, to continue this pointless cat and mouse game, we simply add an extra first query with the default user agent.
# Also: "Hi Microsoft. You sure have A LOT OF RESOURCES TO WASTE to have assigned folks of yours to cripple scripts
# that merely exist because you have chosen to make the user experience from your download website utterly subpar.
# And while I am glad senpai noticed me (UwU), I feel compelled to ask: Don't you guys have better things to do?"
Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 $url | Out-Null Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 $url | Out-Null
Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 -UserAgent $UserAgent $url | Out-Null
} catch { } catch {
# Of course PowerShell 7 had to BREAK $_.Exception.Status on timeouts... # Of course PowerShell 7 had to BREAK $_.Exception.Status on timeouts...
if ($_.Exception.Status -eq "Timeout" -or $_.Exception.GetType().Name -eq "TaskCanceledException") { if ($_.Exception.Status -eq "Timeout" -or $_.Exception.GetType().Name -eq "TaskCanceledException") {
@@ -543,6 +508,28 @@ function Check-Locale
} }
} }
function Get-Code-715-123130-Message
{
try {
$url = "https://www.microsoft.com/" + $QueryLocale + "/software-download/windows11"
if ($Verbosity -ge 2) {
Write-Host Querying $url
}
$r = Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 $url
# PowerShell 7 forces us to parse the HTML ourselves
$r = $r -replace "`n" -replace "`r"
$pattern = '.*<input id="msg-01" type="hidden" value="(.*?)"/>.*'
$msg = [regex]::Match($r, $pattern).Groups[1].Value
$msg = $msg -replace "&lt;", "<" -replace "<[^>]+>" -replace "\s+", " " -replace "\?\?\?", "-"
if ($msg -eq $null) {
throw
}
} catch {
$msg = "You are (temporarily) banned from using this Microsoft service. Please try againg later. Refer to message code 715-123130 and"
}
return $msg
}
# Return an array of releases (e.g. 20H2, 21H1, ...) for the selected Windows version # Return an array of releases (e.g. 20H2, 21H1, ...) for the selected Windows version
function Get-Windows-Releases([int]$SelectedVersion) function Get-Windows-Releases([int]$SelectedVersion)
{ {
@@ -584,55 +571,49 @@ function Get-Windows-Languages([int]$SelectedVersion, [object]$SelectedEdition)
foreach ($EditionId in $SelectedEdition) { foreach ($EditionId in $SelectedEdition) {
$SessionId[$SessionIndex] = [guid]::NewGuid() $SessionId[$SessionIndex] = [guid]::NewGuid()
# Microsoft download protection now requires the sessionId to be whitelisted through vlscppe.microsoft.com/tags # Microsoft download protection now requires the sessionId to be whitelisted through vlscppe.microsoft.com/tags
$url = "https://vlscppe.microsoft.com/tags?org_id=y6jn8c31&session_id=" + $SessionId[$SessionIndex] $url = "https://vlscppe.microsoft.com/tags"
$url += "?org_id=" + $OrgId
$url += "&session_id=" + $SessionId[$SessionIndex]
if ($Verbosity -ge 2) { if ($Verbosity -ge 2) {
Write-Host Querying $url Write-Host Querying $url
} }
try { try {
Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 -UserAgent $UserAgent $url | Out-Null Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -MaximumRedirection 0 $url | Out-Null
} catch { } catch {
Error($_.Exception.Message) Error($_.Exception.Message)
return @() return @()
} }
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html" $url = "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition"
$url += "?pageId=" + $RequestData["GetLangs"][0] $url += "?profile=" + $ProfileId
$url += "&host=www.microsoft.com"
$url += "&segments=software-download," + $WindowsVersions[$SelectedVersion][0][1]
$url += "&query=&action=" + $RequestData["GetLangs"][1]
$url += "&sessionId=" + $SessionId[$SessionIndex]
$url += "&productEditionId=" + $EditionId $url += "&productEditionId=" + $EditionId
$url += "&sdVersion=2" $url += "&SKU=undefined"
$url += "&friendlyFileName=undefined"
$url += "&Locale=" + $QueryLocale
$url += "&sessionID=" + $SessionId[$SessionIndex]
if ($Verbosity -ge 2) { if ($Verbosity -ge 2) {
Write-Host Querying $url Write-Host Querying $url
} }
try { try {
$r = Invoke-WebRequest -Method Post -UseBasicParsing -TimeoutSec $DefaultTimeout -UserAgent $UserAgent -SessionVariable "Session" $url $r = Invoke-RestMethod -UseBasicParsing -TimeoutSec $DefaultTimeout -SessionVariable "Session" $url
if ($r -eq $null) {
throw "Could not retrieve languages from server"
}
if ($Verbosity -ge 5) { if ($Verbosity -ge 5) {
Write-Host "==============================================================================" Write-Host "=============================================================================="
Write-Host $r Write-Host ($r | ConvertTo-Json)
Write-Host "==============================================================================" Write-Host "=============================================================================="
} }
if ($r -match "errorModalMessage") { if ($r.Errors) {
Throw-Error -Req $r -Alt "Could not retrieve languages from server" throw $r.Errors[0].Value
} }
$r = $r -replace "`n" -replace "`r" foreach ($Sku in $r.Skus) {
$pattern = '.*<select id="product-languages"[^>]*>(.*)</select>.*' if (!$languages.Contains($Sku.Language)) {
$html = [regex]::Match($r, $pattern).Groups[1].Value $languages[$Sku.Language] = @{ DisplayName = $Sku.LocalizedLanguage; Data = @() }
# Go through an XML conversion to keep all PowerShells happy...
$html = $html.Replace("selected value", "value")
$html = "<options>" + $html + "</options>"
$xml = [xml]$html
foreach ($var in $xml.options.option) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
if (!$languages.Contains($json.language)) {
$languages[$json.language] = @{ DisplayName = $var.InnerText; Data = @() }
}
$languages[$json.language].Data += @{ SessionIndex = $SessionIndex; SkuId = $json.id }
} }
$languages[$Sku.Language].Data += @{ SessionIndex = $SessionIndex; SkuId = $Sku.Id }
} }
if ($languages.Length -eq 0) { if ($languages.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not parse languages" throw "Could not parse languages"
} }
} catch { } catch {
Error($_.Exception.Message) Error($_.Exception.Message)
@@ -682,65 +663,48 @@ function Get-Windows-Download-Links([int]$SelectedVersion, [int]$SelectedRelease
$archs += $sep + $arch $archs += $sep + $arch
$sep = ", " $sep = ", "
} }
$links += @(New-Object PsObject -Property @{ Type = $archs; Url = $link }) $links += @(New-Object PsObject -Property @{ Arch = $archs; Url = $link })
} catch { } catch {
Error($_.Exception.Message) Error($_.Exception.Message)
return @() return @()
} }
} else { } else {
foreach ($Entry in $SelectedLanguage.Data) { foreach ($Entry in $SelectedLanguage.Data) {
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html" $url = "https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku"
$url += "?pageId=" + $RequestData["GetLinks"][0] $url += "?profile=" + $ProfileId
$url += "&host=www.microsoft.com" $url += "&productEditionId=undefined"
$url += "&segments=software-download," + $WindowsVersions[$SelectedVersion][0][1] $url += "&SKU=" + $Entry.SkuId
$url += "&query=&action=" + $RequestData["GetLinks"][1] $url += "&friendlyFileName=undefined"
$url += "&sessionId=" + $SessionId[$Entry.SessionIndex] $url += "&Locale=" + $QueryLocale
$url += "&skuId=" + $Entry.SkuId $url += "&sessionID=" + $SessionId[$Entry.SessionIndex]
$url += "&language=" + $SelectedLanguage.Name
$url += "&sdVersion=2"
if ($Verbosity -ge 2) { if ($Verbosity -ge 2) {
Write-Host Querying $url Write-Host Querying $url
} }
try { try {
# Must add a referer for this request, else Microsoft's servers will deny it # Must add a referer for this request, else Microsoft's servers may deny it
$ref = "https://www.microsoft.com/software-download/windows11" $ref = "https://www.microsoft.com/software-download/windows11"
$r = Invoke-WebRequest -Method Post -Headers @{ "Referer" = $ref } -UseBasicParsing -TimeoutSec $DefaultTimeout -UserAgent $UserAgent -WebSession $Session $url $r = Invoke-RestMethod -Headers @{ "Referer" = $ref } -UseBasicParsing -TimeoutSec $DefaultTimeout -SessionVariable "Session" $url
if ($r -eq $null) {
throw "Could not retrieve architectures from server"
}
if ($Verbosity -ge 5) { if ($Verbosity -ge 5) {
Write-Host "==============================================================================" Write-Host "=============================================================================="
Write-Host $r Write-Host ($r | ConvertTo-Json)
Write-Host "==============================================================================" Write-Host "=============================================================================="
} }
if ($r -match "errorModalMessage") { if ($r.Errors) {
$Alt = [regex]::Match($r.Content, '<p id="errorModalMessage">(.+?)<\/p>').Groups[1].Value -replace "<[^>]+>" -replace "\s+", " " -replace "\?\?\?", "-" if ( $r.Errors[0].Type -eq 9) {
$Alt = [System.Text.Encoding]::UTF8.GetString([byte[]][char[]]$Alt) $msg = Get-Code-715-123130-Message
if (!$Alt) { throw $msg + $SessionId[$Entry.SessionIndex] + "."
$Alt = "Could not retrieve architectures from server" } else {
} elseif ($Alt -match "715-123130") { throw $r.Errors[0].Value
$Alt += " " + $SessionId[$Entry.SessionIndex] + "."
} }
Throw-Error -Req $r -Alt $Alt
}
$pattern = '(?s)(<input.*?></input>)'
ForEach-Object { [regex]::Matches($r, $pattern) } | ForEach-Object { $html += $_.Groups[1].value }
# Need to fix the HTML and JSON data so that it is well-formed
$html = $html.Replace("class=product-download-hidden", "")
$html = $html.Replace("type=hidden", "")
$html = $html.Replace("&nbsp;", " ")
$html = $html.Replace("IsoX86", "&quot;x86&quot;")
$html = $html.Replace("IsoX64", "&quot;x64&quot;")
# As usual Microsoft's left hand is completely uncoordinated with right hand
$html = $html.Replace("Unknown", "&quot;ARM64&quot;")
$html = "<inputs>" + $html + "</inputs>"
$xml = [xml]$html
foreach ($var in $xml.inputs.input) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
$links += @(New-Object PsObject -Property @{ Type = $json.DownloadType; Url = $json.Uri })
} }
foreach ($ProductDownloadOption in $r.ProductDownloadOptions) {
$links += @(New-Object PsObject -Property @{ Arch = (Get-Arch-From-Type $ProductDownloadOption.DownloadType); Url = $ProductDownloadOption.Uri })
} }
if ($links.Length -eq 0) { if ($links.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not retrieve ISO download links" throw "Could not retrieve ISO download links"
} }
} catch { } catch {
Error($_.Exception.Message) Error($_.Exception.Message)
@@ -751,7 +715,7 @@ function Get-Windows-Download-Links([int]$SelectedVersion, [int]$SelectedRelease
$i = 0 $i = 0
$script:SelectedIndex = 0 $script:SelectedIndex = 0
foreach($link in $links) { foreach($link in $links) {
if ($link.Type -eq $PlatformArch) { if ($link.Arch -eq $PlatformArch) {
$script:SelectedIndex = $i $script:SelectedIndex = $i
} }
$i++ $i++
@@ -921,12 +885,12 @@ if ($Cmd) {
$i = 0 $i = 0
foreach ($link in $links) { foreach ($link in $links) {
if ($Arch -eq "List") { if ($Arch -eq "List") {
Write-Host " -" $link.Type Write-Host " -" $link.Arch
} elseif ((!$Arch -and $script:SelectedIndex -eq $i) -or ($Arch -and $link.Type -match $Arch)) { } elseif ((!$Arch -and $script:SelectedIndex -eq $i) -or ($Arch -and $link.Arch -match $Arch)) {
if (!$Arch -and $Verbosity -ge 1) { if (!$Arch -and $Verbosity -ge 1) {
Write-Host "No architecture specified (-Arch). Defaulting to '$($link.Type)'." Write-Host "No architecture specified (-Arch). Defaulting to '$($link.Arch)'."
} }
$Selected += ", [" + $link.Type + "]" $Selected += ", [" + $link.Arch + "]"
$winLink = $link $winLink = $link
break; break;
} }
@@ -1032,7 +996,7 @@ $Continue.add_click({
if ($links.Length -eq 0) { if ($links.Length -eq 0) {
break break
} }
$script:Architecture = Add-Entry $Stage "Architecture" $links "Type" $script:Architecture = Add-Entry $Stage "Architecture" $links "Arch"
if ($PipeName) { if ($PipeName) {
$XMLForm.Height += $dh / 2; $XMLForm.Height += $dh / 2;
$Margin = $Continue.Margin $Margin = $Continue.Margin
@@ -1104,8 +1068,8 @@ exit $ExitCode
# SIG # Begin signature block # SIG # Begin signature block
# MIItPAYJKoZIhvcNAQcCoIItLTCCLSkCAQExDzANBglghkgBZQMEAgEFADB5Bgor # MIItPAYJKoZIhvcNAQcCoIItLTCCLSkCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDFXTWq2lpU/6fR # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB0klYG7kMpXYlu
# Ct1W7yWDg6b1THz8JSMDTQ5AW7weJ6CCEkAwggVvMIIEV6ADAgECAhBI/JO0YFWU # 6Vmn7G7O3iGdiVPXhDhg2Qn662fa+aCCEkAwggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
@@ -1207,23 +1171,23 @@ exit $ExitCode
# MS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgRVYgUjM2 # MS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgRVYgUjM2
# AhA3xQo8HaADcccNx8YmkC/lMA0GCWCGSAFlAwQCAQUAoHwwEAYKKwYBBAGCNwIB # AhA3xQo8HaADcccNx8YmkC/lMA0GCWCGSAFlAwQCAQUAoHwwEAYKKwYBBAGCNwIB
# DDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # DDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJOGS5HWMc2rDXvPKvX2nFz6 # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIMda0hrcJMlznUby0JMTJZii
# Kgz3N3nMaspPXtcMKTojMA0GCSqGSIb3DQEBAQUABIICAJ74B4pGm7vaEYuhs3NT # YrlB+XbYbN64MDk3LZaPMA0GCSqGSIb3DQEBAQUABIICAKK5L6bwuDw5ehUQPObq
# DVwFMiBWnu7mul6K6Van7Itz0bECfra3r9wpUvcsAoOKEqZ9VX1AmJ/zoIQhQgq3 # jol9xpRSY++pCqutnYyjg+rUWlkbizUiUOCZlUPwKhEOLy84RyhEXHwdVKYovTZE
# gPMhbPLeJuIwnjExCLHSwaJ9tXbETIMX50vMBOt6aLsVgnzT2VJTC5UTzvwcW/Lm # NMODe1cUOuYKmsVuOT6YalqSVx2OZamhwuuniBZZE2oiNskos2MkuzuAi8MIE3Er
# Gaucxnm1bFMNmTMdeOZFLPZBB+WPaJ3dT2lgNU/usB0DyF+i5ilFPFUzesrMDzcV # 3YwzvsFRrEeyr46yxGPGAMdU8oibriANnvhyCuAAg30OeIp8dWU53o51ncjqsuBL
# x+ijkEOz1jxNxXGbTotPvXjMPkUnBIaYsTmeKhrzn085p8O5l0eUAqjEnYPvAq/n # uCTdgXcHYHI5g9C/5pgLHHGWf77Gy2XZSnUaquXo1BAaPigmOYO26UIG3/bQ8WhN
# WSvpohmXZa3V7pWRum5XPZ2fe4cs6GjMYmdmD+8ovW4G6KVbNkf75JFKJZ82Y9lq # ahdGCIRU40fJI/WeYCCAS8xc+x6SuyZym49DdKqVwEDzy4bCzzr8x7f8AB/I7sgH
# qn4VO0r5vV4ffD0erFF/KGTavhvz/VlG2par1O/jfa6ue9yPaLLpWiynThz9/lGI # yXWU8hKqjCdxtmi68BA0Ab49dVXGW7htMvdGsZH0nETjQBhZoVYttl3VcaCWNTO3
# zbvmAGJuIxCjRcA/Dp/QRkl1a8+VvXtaopd3hQTwgHhlXhnOLKzDKuLoJd9CsDPL # IdQiEHA5ikWX3j/Jwo5XP+kodHxFf3PRE0Y1TIDh+RrhiC0tOND8HK4JlHBZZe2o
# 0krYSqps1PD7sAdpr4rx4Gkv+iT9vLJYD+N9KSpfW8BmrHKaKp2BnK79ERxo4fQm # sC+MFC2F7kfAUcFFaMPDIJ2vo/EjOC7fdItEJpVP+sY0CucleMXUHWN+JqT4qGTo
# bagOlVCX/FBmSYU+HW52fb2b9fwS7xVupOoruHiIksjTy7YWNgck3CW4WVKxp35y # zAppkvZdYsN1Btj0vtCbvB3snSRtskkwjcmjWLMuVS8LE9F23uwz7zYhiIU5JT4j
# i6SEO4u+1ivx9uhIYQnDJ4mEcezLU1QBFoc5UG6IW1QgbnYVbBwhlrp/poF90NHp # htAQ0WP/98991HpLVkOzrFe2yUPKUpOlGXM8EjW+z8bqXeV+plr0Fel7l118gKwi
# TC2UDXb1m+JfMPdjaOBZdothoYIXOjCCFzYGCisGAQQBgjcDAwExghcmMIIXIgYJ # 84aPf60L+QL51ro5wTFweNWkoYIXOjCCFzYGCisGAQQBgjcDAwExghcmMIIXIgYJ
# KoZIhvcNAQcCoIIXEzCCFw8CAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0B # KoZIhvcNAQcCoIIXEzCCFw8CAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0B
# CRABBKBpBGcwZQIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIBw/qIiW # CRABBKBpBGcwZQIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIKv6jXxT
# ZirjZAJAqvzJLoq2PiAMMR2U/b8N3B7swhynAhEArbB7KWdDox91SH7wp5yr5hgP # SwYS1e/Id/A50U1BrZQmL4ZM3oinw9cfDogGAhEA4JCHEEKpszHYqo9JnStu0RgP
# MjAyNDExMTgwMTQzNDFaoIITAzCCBrwwggSkoAMCAQICEAuuZrxaun+Vh8b56QTj # MjAyNDExMTkyMjM3NDJaoIITAzCCBrwwggSkoAMCAQICEAuuZrxaun+Vh8b56QTj
# MwQwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD # MwQwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD
# ZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYg # ZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYg
# U0hBMjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yNDA5MjYwMDAwMDBaFw0zNTExMjUy # U0hBMjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yNDA5MjYwMDAwMDBaFw0zNTExMjUy
@@ -1328,20 +1292,20 @@ exit $ExitCode
# ATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG # ATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG
# A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh # A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh
# bXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTjMwQwDQYJYIZIAWUDBAIBBQCggdEwGgYJ # bXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTjMwQwDQYJYIZIAWUDBAIBBQCggdEwGgYJ
# KoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDExMTgw # KoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDExMTky
# MTQzNDFaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFNvThe5i29I+e+T2cUhQhyTV # MjM3NDJaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFNvThe5i29I+e+T2cUhQhyTV
# hltFMC8GCSqGSIb3DQEJBDEiBCCLPWAm+aNyc4DrqvDwqzC7ow3BipFMugVd26EO # hltFMC8GCSqGSIb3DQEJBDEiBCDJR/9SrHl3jTtyDm/69Jj4rIadIbkK8kPggAhw
# cUUgxTA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCB2dp+o8mMvH0MLOiMwrtZWdf7X # EixSTDA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCB2dp+o8mMvH0MLOiMwrtZWdf7X
# c9sF1mW5BZOYQ4+a2zANBgkqhkiG9w0BAQEFAASCAgA1D9F+ChwLLlPfrnjVN7y8 # c9sF1mW5BZOYQ4+a2zANBgkqhkiG9w0BAQEFAASCAgBCDEmF9G2nc30BRigzIqkp
# Raa08Q1yJoUiIPZw/m4rknXNCjyL768gux2rys7Y0ofhaTL8qB2U5605DNmkuDAw # 6C2LwvDVD4WP2lP7iodnfh0xJOH/mjtUhM5nN3mrl+g6njdSfQ0XSWTWFPktkB5K
# NwDXME2bka4xBxDJrI43XFVtu3SpKRbNcv5ujZs0jF4gA3GQykM9UahyusmaxwtQ # 4MfaSNY53ZBBJ7e2rx6TCcDCi8AHi8NvszFFQR6V2rtLxdwcbiRYcny0pW0oOnC7
# DILavBxCuVye9HX86Z1Bdac1wCWY6hvbtoT4ffm+/Uondgh8VzEWd36ImGOpAXQM # tFIbswA+bIfeOfakPxRWCSjYIalNZF07F0LrsTZwD1ZbZhJzbL97KSr/IgZWeZlH
# +UOR8t4JG5777yIBTv51W3qnEx7e4bazezEXIgC7DdXFSq18ypjJuCkLMrb2bWgp # D55hGgtKUtXW/4lv55fO/Uv6HctZgD2HCwrt5pvRa4kWPM8h3D5ZMUpijqVzfiCJ
# /CxGT2ff4itbsv8Z++oJI2oOewhlLiA5tVMkbB0lVGQbkeEP4nqUKwAvOt7g1i7f # G6gxArYx5fE89+/BiKGf8F7DJQxDTdcy1lLOFR3RhcYrEg1g3acQBDCc/tGnEeSS
# uSxGdoNbaesSNefScqii1OTXHsLSjNciRQQJZMxJI44F15X7LOVdaaIbvgoLfIWy # D7HDLUnzHq3VQ47Wm8MjLhL2e5myE+Jiy9TaQB67GHKtUAc4JlO8Ct6H2XkWk003
# T9s+OgUxmAvDdZKpRcHO//tW6JqFCoi+gdW04mUSvME+MRx6RFZe1cjZNPpJEnwh # 3gyNGsfK3PZsmG7syNgFz7lI+mCycqReACkV62IvFz+mVXCjCBGS80R2traxFSso
# kmKGhndTmaBjg1nJH0okrfmR87jjIhulemrdJsUZtcdzCBxT1gW5AaHFOMUuHBkR # TerPqC4VwGD9wgwrdXF1bzzOEv7zaV3k+XnrJqRG4ZY53no04ggttROT/HFBvthh
# P9h0VG3f12fejgEotaDyTmPYBazFlwSrXtrr1uFmS0SDw681z5xGjK92279MlUpo # wcUcFA17tYgMVosai1DNxJR+E+ioE+JoRJlCcfoGBcUi0D5G7NArraA/Hx97gMC3
# 8RBOupy5LSPC59Oi1j7SSeW+xyBfS+0aiyKEAYfk1qO0f3Eql9kTQBZjaizDdKmq # SMKHS4pToqOvXa3cg+GfQNXTjouKOQY86Bb1fx/Chjx84MRY1gzcTaZ5w+/CFpmH
# ubbhLFZ/dE5qax8nkz2Rxw== # TTLfnQjpTLMeIpYWm0g7Eg==
# SIG # End signature block # SIG # End signature block