NuGet/NuGetFeedClass.ps1
#requires -Version 5.0 [NuGetFeed[]] $NuGetFeedCache = @() # PROOF OF CONCEPT PREVIEW: This class holds the connection to a NuGet feed class NuGetFeed { [string] $url [string] $token [string[]] $patterns [string[]] $fingerprints [string] $searchQueryServiceUrl [string] $packagePublishUrl [string] $packageBaseAddressUrl [hashtable] $orgType = @{} NuGetFeed([string] $nuGetServerUrl, [string] $nuGetToken, [string[]] $patterns, [string[]] $fingerprints) { $this.url = $nuGetServerUrl $this.token = $nuGetToken $this.patterns = $patterns $this.fingerprints = $fingerprints # When trusting nuget.org, you should only trust packages signed by an author or packages matching a specific pattern (like using a registered prefix or a full name) if ($nuGetServerUrl -like 'https://api.nuget.org/*' -and $patterns.Contains('*') -and (!$fingerprints -or $fingerprints.Contains('*'))) { throw "Trusting all packages on nuget.org is not supported" } try { $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue" $capabilities = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $this.url $global:ProgressPreference = $prev $this.searchQueryServiceUrl = $capabilities.resources | Where-Object { $_.'@type' -eq 'SearchQueryService' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1 if (!$this.searchQueryServiceUrl) { # Azure DevOps doesn't support SearchQueryService, but SearchQueryService/3.0.0-beta $this.searchQueryServiceUrl = $capabilities.resources | Where-Object { $_.'@type' -eq 'SearchQueryService/3.0.0-beta' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1 } $this.packagePublishUrl = $capabilities.resources | Where-Object { $_."@type" -eq 'PackagePublish/2.0.0' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1 $this.packageBaseAddressUrl = $capabilities.resources | Where-Object { $_."@type" -eq 'PackageBaseAddress/3.0.0' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1 if (!$this.searchQueryServiceUrl -or !$this.packagePublishUrl -or !$this.packageBaseAddressUrl) { Write-Host "Capabilities of NuGet server $($this.url) are not supported" $capabilities.resources | ForEach-Object { Write-Host "- $($_.'@type')"; Write-Host "-> $($_.'@id')" } } Write-Verbose "Capabilities of NuGet server $($this.url) are:" Write-Verbose "- SearchQueryService=$($this.searchQueryServiceUrl)" Write-Verbose "- PackagePublish=$($this.packagePublishUrl)" Write-Verbose "- PackageBaseAddress=$($this.packageBaseAddressUrl)" } catch { throw (GetExtendedErrorMessage $_) } } static [NuGetFeed] Create([string] $nuGetServerUrl, [string] $nuGetToken, [string[]] $patterns, [string[]] $fingerprints) { $nuGetFeed = $script:NuGetFeedCache | Where-Object { $_.url -eq $nuGetServerUrl -and $_.token -eq $nuGetToken -and (-not (Compare-Object $_.patterns $patterns)) -and (-not (Compare-Object $_.fingerprints $fingerprints)) } if (!$nuGetFeed) { $nuGetFeed = [NuGetFeed]::new($nuGetServerUrl, $nuGetToken, $patterns, $fingerprints) $script:NuGetFeedCache += $nuGetFeed } return $nuGetFeed } [void] Dump([string] $message) { Write-Host $message } [hashtable] GetHeaders() { $headers = @{ "Content-Type" = "application/json; charset=utf-8" } # nuget.org only support anonymous access if ($this.token -and $this.url -notlike 'https://api.nuget.org/*') { $headers += @{ "Authorization" = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("user:$($this.token)")))" } } return $headers } [bool] IsTrusted([string] $packageId) { return ($packageId -and ($this.patterns | Where-Object { $packageId -like $_ })) } [hashtable[]] Search([string] $packageName) { if ($this.searchQueryServiceUrl -match '^https://nuget.pkg.github.com/(.*)/query$') { # GitHub support for SearchQueryService is unstable and is not usable # use GitHub API instead # GitHub API unfortunately doesn't support filtering, so we need to filter ourselves $organization = $matches[1] $headers = @{ "Accept" = "application/vnd.github+json" "X-GitHub-Api-Version" = "2022-11-28" } if ($this.token) { $headers += @{ "Authorization" = "Bearer $($this.token)" } } if (-not $this.orgType.ContainsKey($organization)) { $orgMetadata = Invoke-RestMethod -Method GET -Headers $headers -Uri "https://api.github.com/users/$organization" if ($orgMetadata.type -eq 'Organization') { $this.orgType[$organization] = 'orgs' } else { $this.orgType[$organization] = 'users' } } $queryUrl = "https://api.github.com/$($this.orgType[$organization])/$organization/packages?package_type=nuget&per_page=100&page=" $page = 1 Write-Host -ForegroundColor Yellow "Search package using $queryUrl$page" $matching = @() while ($true) { $result = Invoke-RestMethod -Method GET -Headers $headers -Uri "$queryUrl$page" if ($result.Count -eq 0) { break } $matching += @($result | Where-Object { $_.name -like "*$packageName*" -and $this.IsTrusted($_.name) } | Sort-Object { $_.name.replace('.symbols','') } | ForEach-Object { @{ "id" = $_.name; "versions" = @() } } ) $page++ } } else { $queryUrl = "$($this.searchQueryServiceUrl)?q=$packageName&take=50" try { Write-Host -ForegroundColor Yellow "Search package using $queryUrl" $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue" $searchResult = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl $global:ProgressPreference = $prev } catch { throw (GetExtendedErrorMessage $_) } # Check that the found pattern matches the package name and the trusted patterns $matching = @($searchResult.data | Where-Object { $_.id -like "*$($packageName)*" -and $this.IsTrusted($_.id) } | Sort-Object { $_.id.replace('.symbols','') } | ForEach-Object { @{ "id" = $_.id; "versions" = @($_.versions.version) } } ) } $exact = $matching | Where-Object { $_.id -eq $packageName -or $_.id -eq "$packageName.symbols" } if ($exact) { Write-Host "Exact match found for $packageName" $matching = $exact } else { Write-Host "$($matching.count) matching packages found" } return $matching | ForEach-Object { Write-Host "- $($_.id)"; $_ } } [string[]] GetVersions([hashtable] $package, [bool] $descending, [bool] $allowPrerelease) { if (!$this.IsTrusted($package.id)) { throw "Package $($package.id) is not trusted on $($this.url)" } if ($package.versions.count -ne 0) { $versionsArr = $package.versions } else { $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($package.Id.ToLowerInvariant())/index.json" try { Write-Host -ForegroundColor Yellow "Get versions using $queryUrl" $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue" $versions = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl $global:ProgressPreference = $prev } catch { throw (GetExtendedErrorMessage $_) } $versionsArr = @($versions.versions) } Write-Host "$($versionsArr.count) versions found" $versionsArr = @($versionsArr | Where-Object { $allowPrerelease -or !$_.Contains('-') } | Sort-Object { ($_ -replace '-.+$') -as [System.Version] }, { "$($_)z" } -Descending:$descending | ForEach-Object { "$_" }) Write-Host "First version is $($versionsArr[0])" Write-Host "Last version is $($versionsArr[$versionsArr.Count-1])" return $versionsArr } # Normalize name or publisher name to be used in nuget id static [string] Normalize([string] $name) { return $name -replace '[^a-zA-Z0-9_\-]','' } static [string] NormalizeVersionStr([string] $versionStr) { $idx = $versionStr.IndexOf('-') $version = [System.version]($versionStr.Split('-')[0]) if ($version.Build -eq -1) { $version = [System.Version]::new($version.Major, $version.Minor, 0, 0) } if ($version.Revision -eq -1) { $version = [System.Version]::new($version.Major, $version.Minor, $version.Build, 0) } if ($idx -gt 0) { return "$version$($versionStr.Substring($idx))" } else { return "$version" } } static [Int32] CompareVersions([string] $version1, [string] $version2) { $version1 = [NuGetFeed]::NormalizeVersionStr($version1) $version2 = [NuGetFeed]::NormalizeVersionStr($version2) $ver1 = $version1 -replace '-.+$' -as [System.Version] $ver2 = $version2 -replace '-.+$' -as [System.Version] if ($ver1 -eq $ver2) { # add a 'z' to the version to make sure that 5.1.0 is greater than 5.1.0-beta # Tags are sorted alphabetically (alpha, beta, rc, etc.), even though this shouldn't matter # New prerelease versions will always have a new version number return [string]::Compare("$($version1)z", "$($version2)z") } elseif ($ver1 -gt $ver2) { return 1 } else { return -1 } } # Test if version is included in NuGet version range # https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges static [bool] IsVersionIncludedInRange([string] $versionStr, [string] $nuGetVersionRange) { $versionStr = [NuGetFeed]::NormalizeVersionStr($versionStr) $version = $versionStr -replace '-.+$' -as [System.Version] if ($nuGetVersionRange -match '^\s*([\[(]?)([\d\.]*)(,?)([\d\.]*)([\])]?)\s*$') { $inclFrom = $matches[1] -ne '(' $range = $matches[3] -eq ',' $inclTo = $matches[5] -eq ']' if ($matches[1] -eq '' -and $matches[5] -eq '') { $range = $true } if ($matches[2]) { $fromver = [System.Version]([NuGetFeed]::NormalizeVersionStr($matches[2])) } else { $fromver = [System.Version]::new(0,0,0,0) if ($inclFrom) { Write-Host "Invalid NuGet version range $nuGetVersionRange" return $false } } if ($matches[4]) { $tover = [System.Version]([NuGetFeed]::NormalizeVersionStr($matches[4])) } elseif ($range) { $tover = [System.Version]::new([int32]::MaxValue,[int32]::MaxValue,[int32]::MaxValue,[int32]::MaxValue) if ($inclTo) { Write-Host "Invalid NuGet version range $nuGetVersionRange" return $false } } else { $tover = $fromver } if (!$range -and (!$inclFrom -or !$inclTo)) { Write-Host "Invalid NuGet version range $nuGetVersionRange" return $false } if ($inclFrom) { if ($inclTo) { return $version -ge $fromver -and $version -le $tover } else { return $version -ge $fromver -and $version -lt $tover } } else { if ($inclTo) { return $version -gt $fromver -and $version -le $tover } else { return $version -gt $fromver -and $version -lt $tover } } } return $false } [string] FindPackageVersion([hashtable] $package, [string] $nuGetVersionRange, [string[]] $excludeVersions, [string] $select, [bool] $allowPrerelease) { $versions = $this.GetVersions($package, ($select -ne 'Earliest'), $allowPrerelease) if ($excludeVersions) { Write-Host "Exclude versions: $($excludeVersions -join ', ')" } foreach($version in $versions ) { if ($excludeVersions -contains $version) { continue } if (($select -eq 'Exact' -and [NuGetFeed]::NormalizeVersionStr($nuGetVersionRange) -eq [NuGetFeed]::NormalizeVersionStr($version)) -or ($select -ne 'Exact' -and [NuGetFeed]::IsVersionIncludedInRange($version, $nuGetVersionRange))) { if ($nuGetVersionRange -eq '0.0.0.0') { Write-Host "$select version is $version" } else { Write-Host "$select version matching '$nuGetVersionRange' is $version" } return $version } } return '' } [xml] DownloadNuSpec([string] $packageId, [string] $version) { if (!$this.IsTrusted($packageId)) { throw "Package $packageId is not trusted on $($this.url)" } $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant())/$($packageId.ToLowerInvariant()).nuspec" try { Write-Host "Download nuspec using $queryUrl" $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue" $tmpFile = Join-Path ([System.IO.Path]::GetTempPath()) "$([GUID]::NewGuid().ToString()).nuspec" Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl -OutFile $tmpFile $nuspec = Get-Content -Path $tmpfile -Encoding UTF8 -Raw Remove-Item -Path $tmpFile -Force $global:ProgressPreference = $prev } catch { throw (GetExtendedErrorMessage $_) } return [xml]$nuspec } [string] DownloadPackage([string] $packageId, [string] $version) { if (!$this.IsTrusted($packageId)) { throw "Package $packageId is not trusted on $($this.url)" } $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant())/$($packageId.ToLowerInvariant()).$($version.ToLowerInvariant()).nupkg" $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([GUID]::NewGuid().ToString()) try { Write-Host -ForegroundColor Green "Download package using $queryUrl" $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue" $filename = "$tmpFolder.zip" Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl -OutFile $filename if ($this.fingerprints) { $arguments = @("nuget", "verify", $filename) if ($this.fingerprints.Count -eq 1 -and $this.fingerprints[0] -eq '*') { Write-Host "Verifying package using any certificate" } else { Write-Host "Verifying package using $($this.fingerprints -join ', ')" $arguments += @("--certificate-fingerprint $($this.fingerprints -join ' --certificate-fingerprint ')") } cmddo -command 'dotnet' -arguments $arguments -silent -messageIfCmdNotFound "dotnet not found. Please install it from https://dotnet.microsoft.com/download" } Expand-Archive -Path $filename -DestinationPath $tmpFolder -Force $global:ProgressPreference = $prev Remove-Item $filename -Force Write-Host "Package successfully downloaded" } catch { throw (GetExtendedErrorMessage $_) } return $tmpFolder } [void] PushPackage([string] $package) { if (!($this.token)) { throw "NuGet token is required to push packages" } Write-Host "Preparing NuGet Package for submission" $headers = $this.GetHeaders() $headers += @{ "X-NuGet-ApiKey" = $this.token "X-NuGet-Client-Version" = "6.3.0" } $FileContent = [System.IO.File]::ReadAllBytes($package) $boundary = [System.Guid]::NewGuid().ToString(); $LF = "`r`n"; $body = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF") $body += [System.Text.Encoding]::UTF8.GetBytes("Content-Type: application/octet-stream$($LF)Content-Disposition: form-data; name=package; filename=""$([System.IO.Path]::GetFileName($package))""$($LF)$($LF)") $body += $fileContent $body += [System.Text.Encoding]::UTF8.GetBytes("$LF--$boundary--$LF") $tmpFile = Join-Path ([System.IO.Path]::GetTempPath()) ([GUID]::NewGuid().ToString()) [System.IO.File]::WriteAllBytes($tmpFile, $body) Write-Host "Submitting NuGet package" try { Invoke-RestMethod -UseBasicParsing -Uri $this.packagePublishUrl -ContentType "multipart/form-data; boundary=$boundary" -Method Put -Headers $headers -inFile $tmpFile | Out-Host Write-Host -ForegroundColor Green "NuGet package successfully submitted" } catch [System.Net.WebException] { if ($_.Exception.Status -eq "ProtocolError" -and $_.Exception.Response -is [System.Net.HttpWebResponse]) { $response = [System.Net.HttpWebResponse]($_.Exception.Response) if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Conflict) { Write-Host -ForegroundColor Yellow "NuGet package already exists" } else { throw (GetExtendedErrorMessage $_) } } else { throw (GetExtendedErrorMessage $_) } } catch { throw (GetExtendedErrorMessage $_) } finally { Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue } } } # SIG # Begin signature block # MIImZgYJKoZIhvcNAQcCoIImVzCCJlMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC9oLeqbAajU/ED # sjfCsR38CEidZB0tMih7MUbH2OgVv6CCH34wggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0GCSqG # SIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0 # ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290IFI0 # NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYTAkdC # MRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVi # bGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAw # ggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEeAEZG # bEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGVoYW3 # haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk8XL/ # tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzhP06S # hdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41aHU5 # pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ac7I # H60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/2yGg # W+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9gRvd+ # QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhAQnAg # NpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8BAf8E # BAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcDAzAb # BgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyGOmh0 # dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nUm9v # dFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8vY3J0 # LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYucDdj # MCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0B # AQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW4HL9 # hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIhrCym # laS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYpmlG+ # vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7DcML4i # TAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yELg09 # Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRzNyE6 # bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw4rg3 # sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6E52z # 1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6xWls # /GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZtg0y9 # ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggZZMIIE # waADAgECAhANIM3qwHRbWKHw+Zq6JhzlMA0GCSqGSIb3DQEBDAUAMFQxCzAJBgNV # BAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3Rp # Z28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjExMDIyMDAwMDAwWhcN # MjQxMDIxMjM1OTU5WjBdMQswCQYDVQQGEwJESzEUMBIGA1UECAwLSG92ZWRzdGFk # ZW4xGzAZBgNVBAoMEkZyZWRkeSBLcmlzdGlhbnNlbjEbMBkGA1UEAwwSRnJlZGR5 # IEtyaXN0aWFuc2VuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgYC5 # tlg+VRktRRkahxxaV8+DAd6vHoDpcO6w7yT24lnSoMuA6nR7kgy90Y/sHIwKE9Ww # t/px/GAY8eBePWjJrFpG8fBtJbXadRTVd/470Hs/q9t+kh6A/0ELj7wYsKSNOyuF # Poy4rtClOv9ZmrRpoDVnh8Epwg2DpklX2BNzykzBQxIbkpp+xVo2mhPNWDIesntc # 4/BnSebLGw1Vkxmu2acKkIjYrne/7lsuyL9ue0vk8TGk9JBPNPbGKJvHu9szP9oG # oH36fU1sEZ+AacXrp+onsyPf/hkkpAMHAhzQHl+5Ikvcus/cDm06twm7VywmZcas # 2rFAV5MyE6WMEaYAolwAHiPz9WAs2GDhFtZZg1tzbRjJIIgPpR+doTIcpcDBcHnN # dSdgWKrTkr2f339oT5bnJfo7oVzc/2HGWvb8Fom6LQAqSC11vWmznHYsCm72g+fo # TKqW8lLDfLF0+aFvToLosrtW9l6Z+l+RQ8MtJ9EHOm2Ny8cFLzZCDZYw32BydwcL # V5rKdy4Ica9on5xZvyMOLiFwuL4v2V4pjEgKJaGSS/IVSMEGjrM9DHT6YS4/oq9q # 20rQUmMZZQmGmEyyKQ8t11si8VHtScN5m0Li8peoWfCU9mRFxSESwTWow8d462+o # 9/SzmDxCACdFwzvfKx4JqDMm55cL+beunIvc0NsCAwEAAaOCAZwwggGYMB8GA1Ud # IwQYMBaAFA8qyyCHKLjsb0iuK1SmKaoXpM0MMB0GA1UdDgQWBBTZD6uy9ZWIIqQh # 3srYu1FlUhdM0TAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUE # DDAKBggrBgEFBQcDAzARBglghkgBhvhCAQEEBAMCBBAwSgYDVR0gBEMwQTA1Bgwr # BgEEAbIxAQIBAwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9D # UFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGln # by5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUF # BwEBBG0wazBEBggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0 # aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6 # Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQASEbZACurQeQN8 # WDTR+YyNpoQ29YAbbdBRhhzHkT/1ao7LE0QIOgGR4GwKRzufCAwu8pCBiMOUTDHT # ezkh0rQrG6khxBX2nSTBL5i4LwKMR08HgZBsbECciABy15yexYWoB/D0H8WuGe63 # PhGWueR4IFPbIz+jEVxfW0Nyyr7bXTecpKd1iprm+TOmzc2E6ab95dkcXdJVx6Zy # s++QrrOfQ+a57qEXkS/wnjjbN9hukL0zg+g8L4DHLKTodzfiQOampvV8QzbnB7Y8 # YjNcxR9s/nptnlQH3jorNFhktiBXvD62jc8pAIg6wyH6NxSMjtTsn7QhkIp2kusw # IQwD8hN/fZ/m6gkXZhRJWFr2WRZOz+edZ62Jf25C/NYWscwfBwn2hzRZf1HgyxkX # Al88dvvUA3kw1T6uo8aAB9IcL6Owiy7q4T+RLRF7oqx0vcw0193Yhq/gPOaUFlqz # ExP6TQ5TR9XWVPQk+a1B1ATKMLi1JShO6KWTmNkFkgkgpkW69BEwggauMIIElqAD # AgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYT # AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy # dC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAz # MjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQK # Ew5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBS # U0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDM # g/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOx # s+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09ns # ad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtA # rF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149z # k6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6 # OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qh # HGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1 # KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX # 6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0 # sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQID # AQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2F # L3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08w # DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEB # BGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsG # AQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgG # BmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+Y # qUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjY # C+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0 # FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6 # WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGj # VoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzp # SwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwd # eDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o # 08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n # +2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y # 3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIO # K+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGvDCCBKSgAwIBAgIQC65mvFq6f5WH # xvnpBOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMO # RGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNB # NDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1 # MTEyNTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAw # HgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEB # BQADggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0K # MCBDEr4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2 # O8oo76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh # 43rOH3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEY # TX9ReNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X # 6kgXj3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD # 8UTVDSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4 # c16Jidj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+ # CO/CacBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqIm # d93NRxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva # 7b1XCB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5 # AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNV # HSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgB # hv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYE # FJ9XLAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9j # cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU # aW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzAB # hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9j # YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy # NTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvj # OIQSR9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZd # rlWBb0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6Hzele # dbDCzFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9U # FAL1UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWU # m3WpByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30 # VAGEsshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vA # lk/8a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2 # WgNFYagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC # 5H7QEY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx # 2EgEdeoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJs # yz/JceENc2Sg8h3KeFUCS7tpFk7CrDqkMYIGPjCCBjoCAQEwaDBUMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgUjM2AhANIM3qwHRbWKHw+Zq6JhzlMA0G # CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI # hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC # ARUwLwYJKoZIhvcNAQkEMSIEILM2+7anR0nj/JsXI+C6ZAOrWwiWjDELHQSHAYH+ # I5fwMA0GCSqGSIb3DQEBAQUABIICADiDp2vVZxV+u/K1Cl0Ui143P25pojtjhWXH # OJRvc8bKYOZJ9yyLcnwIEAeWEYmo1BJQdBFgu5CZ15LDPCccQ0EXRb0Hn/tWBi0+ # vek/yCM81BPt1XnUt6g+DBAZ7t1duuMJ0jU3KH5l3hP/pPRv+uCjUSwIj9Ep5AkC # LyjM8a2xJo9TZg3KA5+qwho8NdtQAFTCI91LdZILKhltRUu4nW14rwXJ74p4trR6 # 7RdWE66nOXiM5h9g4uCd1KesYGp2GY7Oh9GXWxu+S1l9MAeOvYLEKDk9BezjwRFO # v8IJPhTR3j9AXA7K+jC+BG8KUbOkCXXm647cUcsSCv4ZZIiZWF4BnEPDrJPIk/HB # QgOf1qwVaQ0Gp4EVLIc5REmPwwkD/PHcepW8DjKu/xqvhtKTVunyAAQKtkYGFuRk # 3ninwgrsBx9BgaPKrQOTb5lvjX1LTix5xb+6WASAsb+qnZuoFTvl3fMrEvRafJt1 # vp6V/N1JbA49N97FS6kposn948XiyU+mzSkNpU3e9pvSgnwoUsDwcbTyQR2yQsew # 6SyW4Euy/4h8LMUYBdPgRX9X9HIzAxAnVIbVGpDL2SxED0eoaHAtih+XA+SWBgow # 1TNw/u4c5vLmezRvM+sbGTXF6eLryLBpMFKC2J4BZtkgiKt0yRzgolHlm42YhMVR # GhBD7ormoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0wggMJAgEBMHcwYzELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2Vy # dCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQC65m # vFq6f5WHxvnpBOMzBDANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkq # hkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI0MTAwOTExMzE0OFowLwYJKoZIhvcN # AQkEMSIEIICn5FEfl8LSQEv0/UL/uzRcRf6z4c2N+VKJprW7j9KtMA0GCSqGSIb3 # DQEBAQUABIICAGjdcPrlscvXo/1NBY5szrfYyqDkGiQBxASJk191dcdZ62G0iBe4 # Qna1TyKvZq63nxfJZ7agLxlcS+WPTUM3S+53UnhWmXOMezMRd9PCywEtxhdwXrlv # 2k7frDNdVI1F+SqsX/xYFrc+Due0V8yZ34KVn+NCSbDoSBhvP29jLE6DpCV5cD76 # +5tImv4UZddTipXNMn1pGUE81/kKOP3wwrxCqK+vSICCJBKhBG9QHWHvrpVYtgOd # aOq+U01JY6BZ/aY1BNJHLyCPDvOCPyHyPhOcdb7iZmUYbXJotBzNSoAXmp9fiFra # cmnn90V96EvKOF+06y040rSTwTbSSOR8eCgaz2RGTfZN1mDHc+e6VycJhl7Sc7PZ # FQ0ncFOeI+YPTWuVGJ8dzfIRYrHjFLFtnGHtICBnpACMV7G7aRyBg5NUa/fU1oqF # L/xwS9XCO//ZWr0i7rEB0QuCtTBFjDgpqwEi/GPkD3ChyfNitCN0CtIYWcyNJiKX # Rv/pW0e58L+wPl5DRpjnaTzupD6RyWz8cdWGEG5lO9gE2lPVGdxYnL+vd/zm1ux9 # 7YH7rUWa6cfgx4hJkKy5nXe8z0WRoarDQbR+9p0Z8CjvVSzOkoOnqQrrgPf6mIiy # 8X4UPZcvfotp9qYwBqdjr8dn755QOmt+OmJIoJdvINmmoRcqsr1vSS1V # SIG # End signature block |