Functions/Public/Invoke-AkamaiRequest.ps1
function Invoke-AkamaiRequest { [Alias('iar')] [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $Path, [Parameter()] [ValidateSet("GET", "HEAD", "PUT", "POST", "DELETE", "PATCH")] [string] $Method = "GET", [Parameter()] [hashtable] $QueryParameters, [Parameter()] [hashtable] $AdditionalHeaders, [Parameter()] $Body, [Parameter()] [string] $InputFile, [Parameter()] [string] $OutputFile, [Parameter()] [string] $MaxBody = 131072, [Parameter()] [switch] $SkipHttpErrorCheck, [Parameter()] [int] $Retry, [Parameter()] [string] $EdgeRCFile, [Parameter()] [string] $Section, [Parameter()] [string] $AccountSwitchKey ) ## Define shared variables $RedirectStatuses = 301, 302, 307, 308 $RetryStatuses = 500, 502, 503, 504 $RetryMethods = 'GET', 'HEAD' # Make sure options are available if ($null -eq $Global:AkamaiOptions) { Get-AkamaiOptions | Out-Null } # Get auth creds from various potential sources $Credentials = Get-AkamaiCredentials -EdgeRCFile $EdgeRCFile -Section $Section -AccountSwitchKey $AccountSwitchKey if ($Debug) { ## Check creds if in Debug mode Confirm-Auth -Auth $Credentials } # Path with QueryString compatibility if ($Path.Contains('?')) { $PathElements = $Path.Split('?') $Path = $PathElements[0] $QueryFromPath = $PathElements[1] } # Build QueryNameValueCollection $QueryNVCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) # Sanitize QueryFromPath (for compatibility) if ($QueryFromPath) { $QueryString = [System.Web.HttpUtility]::ParseQueryString($QueryFromPath) foreach ($key in $QueryString.Keys) { if (@($null, '') -notcontains $key -and @($null, '') -notcontains $QueryString[$key]) { $QueryNVCollection.Add($key, $QueryString[$key]) } } } # Merge QueryParameters Hashtable (if same keys, hashtable values win over values from path) foreach ($key in $QueryParameters.Keys) { if (@($null, '') -notcontains $key -and @($null, '') -notcontains $QueryParameters[$key]) { # Lower case boolean params $SanitizedValue = $QueryParameters.$key if ($SanitizedValue -imatch 'true|false') { $SanitizedValue = $SanitizedValue.ToString().ToLower() } if ($key -notin $QueryNVCollection.Keys) { $QueryNVCollection.Add($key, $SanitizedValue) } else { $QueryNVCollection[$key] = $SanitizedValue } } } # Add account switch key from $Credentials, if present if ($Credentials.account_key) { $QueryNVCollection.Add('accountSwitchKey', $Credentials.account_key) } # Build Request URL [System.UriBuilder]$Request = New-Object -TypeName 'System.UriBuilder' $Request.Scheme = 'https' $Request.Host = $Credentials.host $Request.Path = $Path $Request.Query = $QueryNVCollection.ToString() # ReqURL Verification Write-Debug "Request URL = $($Request.Uri.AbsoluteUri)" If (($null -eq $Request.Uri.AbsoluteUri) -or ($Request.Host -notmatch "akamaiapis.net")) { throw "Error: Invalid Request URI" } # Convert Body to string if not already, but only if Content-Type hasn't been overridden $ConvertBody = $false if ($null -ne $Body -and $Body -isnot 'String') { $ConvertBody = $true } if ($null -ne $AdditionalHeaders -and $null -ne $AdditionalHeaders['content-type'] -and -not $AdditionalHeaders['content-type'].contains('json')) { Write-Debug 'Forgoing body conversion due to custom content-type.' $ConvertBody = $false } if ($ConvertBody) { try { Write-Debug "Converting Body of type $($Body.GetType().Name) to JSON." $Body = ConvertTo-Json -InputObject $Body -Depth 100 } catch { Write-Error $_ throw 'Body could not be converted to a JSON string' } } ## Create IDictionary to hold request headers $Headers = @{} ## Generate Auth header and add to dictionary $AuthHeaderParams = @{ Credentials = $Credentials Method = $Method ExpandedPath = ($Request.Path + $Request.Query) Body = $Body InputFile = $InputFile ErrorAction = 'SilentlyContinue' } $Headers['Authorization'] = Get-AkamaiAuthHeader @AuthHeaderParams ## Calculate custom UA $UserAgent = Get-AkamaiUserAgent $Headers.Add('User-Agent', $UserAgent) # Add headers $Headers.Add('Accept', 'application/json') $Headers.Add('Content-Type', 'application/json; charset=utf-8') # Add additional headers if ($AdditionalHeaders) { $AdditionalHeaders.Keys | ForEach-Object { $Headers[$_] = $AdditionalHeaders[$_] } } # Add PAPI prefix removal header if required if ($Global:AkamaiOptions.DisablePapiPrefixes) { $Headers['PAPI-Use-Prefixes'] = 'false' } # Set ContentType param from Content-Type header. This is sent along with bodies to fix string encoding issues in IRM $ContentType = $Headers['Content-Type'] # turn off the "Expect: 100 Continue" header as it's not supported on the Akamai side. [System.Net.ServicePointManager]::Expect100Continue = $false # Set TLS version to 1.2 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 $RequestParams = @{ Method = $Method Uri = $Request.Uri Headers = $Headers ContentType = $ContentType MaximumRedirection = 0 ErrorAction = 'Stop' OutVariable = 'Response' } # Add -AllowInsecureRedirect if Pwsh 7.4 or higher if ($PSVersionTable.PSVersion -ge '7.4.0') { $RequestParams['AllowInsecureRedirect'] = $true } # Add -SkipHttpErrorCheck if Pwsh 7+ if ($PSVersionTable.PSVersion -ge '7.0.0') { $RequestParams['SkipHttpErrorCheck'] = $SkipHttpErrorCheck } # Add -UseBasicParsing if Pwsh < 6 if ($PSVersionTable.PSVersion -lt '6.0.0') { $RequestParams['UseBasicParsing'] = $true } # Support proxy as environment variable if ($null -ne $ENV:https_proxy) { $RequestParams.Proxy = $ENV:https_proxy } # Include credentials if ($null -ne $ENV:proxy_use_default_credentials) { $params.ProxyUseDefaultCredentials = $true } # Add body or inputfile, exclusively if ($Body) { $RequestParams.Body = $Body } elseif ($InputFile) { $RequestParams.InFile = $InputFile } # Add outputfile if ($OutputFile) { $RequestParams.OutFile = $OutputFile } ## Backup and set ProgressPreference $OldProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' # Reset retry params $RetryRequest = $false $RetryWaitTime = $null if ($null -eq $PSBoundParameters.Retry) { $Retry = 0 } ##---- Make request try { $Response = Invoke-WebRequest @RequestParams } catch { # Expand error to access exception data during evaluation $ExpandedError = Expand-AkamaiError -ErrorRecord $_ -Options $Global:AkamaiOptions if ([int] $ExpandedError.Exception.Response.StatusCode -eq 302) { # Construct fake response object for redirect chasing $Response = [PSCustomObject] @{ StatusCode = 302 Headers = [PSCustomObject] @{ Location = $ExpandedError.Exception.Response.Headers.Location } } } elseif ($ExpandedError.ErrorDetails.Message -and $ExpandedError.ErrorDetails.Message.Contains('The maximum redirection count has been exceeded')) { # Do nothing here, this is expected in pwsh 5.1 } else { # 429 handling # Extract "retry after X seconds", if it exists. Otherwise, retry using the standard exponential backoff logic if ($Global:AkamaiOptions.enableRateLimitRetries -and [int] $ExpandedError.Exception.Response.StatusCode -eq 429) { if ($ExpandedError.Exception.Data.detail -match '(?<WaitTime>[\d]+) seconds') { $RetryWaitTime = $Matches.WaitTime } $RetryRequest = $true } # General error handling elseif ($Global:AkamaiOptions.enableErrorRetries -and [int] $ExpandedError.Exception.Response.StatusCode -in $RetryStatuses -and $Method.ToUpper() -in $RetryMethods) { $RetryRequest = $true } # No recourse other than throwing the error. Ah well. else { $PSCmdlet.ThrowTerminatingError($ExpandedError) } } # Wait for the calculated period, then repeat the request if ($RetryRequest) { if ($PSBoundParameters.Retry -ge $Global:AkamaiOptions.maxErrorRetries) { Write-Warning "Request will not be retried as retries have reached the maximum limit ($($Global:AkamaiOptions.maxErrorRetries))." $PSCmdlet.ThrowTerminatingError($ExpandedError) } else { Write-Warning "Received the following error: $([int] $ExpandedError.Exception.Response.StatusCode) $($ExpandedError.Exception.Response.StatusCode)." # Ensure retry is set in PSBoundParameters for the next request $PSBoundParameters.Retry = ++$Retry if ($RetryWaitTime) { Write-Warning "Waiting for $RetryWaitTime seconds before retrying. Wait time determine from API response." } else { $RetryWaitTime = $Global:AkamaiOptions.initialErrorWait * ([Math]::Pow(2, $PSBoundParameters.Retry - 1)) Write-Warning "Waiting for $RetryWaitTime seconds before retrying." } Start-Sleep -Seconds $RetryWaitTime Write-Debug "Retrying request. Attempt = $($PSBoundParameters.Retry)." # Retry request $Response = Invoke-AkamaiRequest @PSBoundParameters } } } ## Reset ProgressPreference $ProgressPreference = $OldProgressPreference ## Chase redirects, with signature regeneration if ($Response.StatusCode -in $RedirectStatuses) { if ($null -eq $Response.Headers.Location) { throw "Response with status $($Response.statusCode) missing Location header" } $Location = $Response.Headers.Location | Select-Object -First 1 $RedirectUrl = [System.UriBuilder]::new($Location) $RedirectPath = $RedirectUrl.Path + $RedirectUrl.Query Write-Debug "Redirecting to $RedirectPath." $RedirectParams = @{ 'Method' = $Method 'Path' = $RedirectPath 'AdditionalHeaders' = $AdditionalHeaders 'EdgeRCFile' = $EdgeRCFile 'Section' = $Section 'AccountSwitchKey' = $AccountSwitchKey 'Debug' = ($PSBoundParameters.Debug -eq $true) } try { $Response = Invoke-AkamaiRequest @RedirectParams } catch { # Expand errors for more useful response, then throw $PSCmdlet.ThrowTerminatingError((Expand-AkamaiError -ErrorRecord $_ -Options $Global:AkamaiOptions)) } return $Response } ## Parse response $ParsedResponseBody = $null if ($Response.Content) { # Extract content-type if ($null -ne $Response.Headers.'Content-Type') { $ResponseContentType = $Response.Headers.'Content-Type' | Select-Object -First 1 } else { # Assume empty string, purely to avoid issues evaluating it later. Should maybe default to text/plain? $ResponseContentType = '' } if ($ResponseContentType -is 'Array') { $ResponseContentType = $ResponseContentType[0] } Write-Debug "Response content type = $ResponseContentType" # Handle response content $ParsedResponseBody = $Response.content # Handle IWR byte[] response content if ($ParsedResponseBody -is 'Byte[]') { Write-Debug "Converting response from byte[]." $ParsedResponseBody = [System.Text.Encoding]::UTF8.GetString($ParsedResponseBody) } # Convert json bodies to PSCustomObject. This can happen in addition to the byte array conversion if ($ResponseContentType.Contains('json')) { try { Write-Debug "Converting response body from JSON." $ParsedResponseBody = $ParsedResponseBody | ConvertFrom-Json } catch { Write-Debug "JSON conversion failed. Falling back to raw response." } } # Convert XML bodies to PSCustomObject elseif ($ResponseContentType -eq 'application/xml') { try { Write-Debug "Converting response body from XML." $ParsedResponseBody = [xml] $ParsedResponseBody } catch { Write-Debug "XML conversion failed. Falling back to raw response." } } # Convert CSV bodies to PSCustomObject elseif ($ResponseContentType -eq 'text/csv') { try { Write-Debug "Converting response body from XML." $ParsedResponseBody = $ParsedResponseBody | ConvertFrom-Csv } catch { Write-Debug "XML conversion failed. Falling back to raw response." } } } $ParsedResponse = [PSCustomObject] @{ Status = $Response.StatusCode Headers = $Response.Headers Body = $ParsedResponseBody } # Report on 429 limits if ($Global:AkamaiOptions.EnableRateLimitWarnings) { if ($null -ne $ParsedResponse.Headers -and $null -ne $ParsedResponse.Headers['X-RateLimit-Remaining']) { if ($ParsedResponse.Headers['X-RateLimit-Limit']) { $RateLimitRemaining = [int] ($ParsedResponse.Headers['X-RateLimit-Remaining'] | Select-Object -First 1) } if ($ParsedResponse.Headers['X-RateLimit-Limit']) { $RateLimitTotal = [int] ($ParsedResponse.Headers['X-RateLimit-Limit'] | Select-Object -First 1) } if ($RateLimitRemaining -and $RateLimitTotal) { $RateLimitPercentUsed = (1 - ($RateLimitRemaining / $RateLimitTotal)) * 100 $RateLimitPercentDisplay = "{0:F2}" -f $RateLimitPercentUsed if ($RateLimitPercentUsed -gt $Global:AkamaiOptions.RateLimitWarningPercentage) { Write-Warning "Akamai Rate Limit used = $RateLimitPercentDisplay%. Remaining requests = $RateLimitRemaining." } } } } return $ParsedResponse } # SIG # Begin signature block # MIIp2AYJKoZIhvcNAQcCoIIpyTCCKcUCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDsMjb5AVaH1Yik # iscgKdR/kRMbOlUI/HFlATg1kdj+x6CCDo4wggawMIIEmKADAgECAhAIrUCyYNKc # TJ9ezam9k67ZMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0z # NjA0MjgyMzU5NTlaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg # UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQDVtC9C0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0 # JAfhS0/TeEP0F9ce2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJr # Q5qZ8sU7H/Lvy0daE6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhF # LqGfLOEYwhrMxe6TSXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+F # LEikVoQ11vkunKoAFdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh # 3K3kGKDYwSNHR7OhD26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJ # wZPt4bRc4G/rJvmM1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQay # g9Rc9hUZTO1i4F4z8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbI # YViY9XwCFjyDKK05huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchAp # QfDVxW0mdmgRQRNYmtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRro # OBl8ZhzNeDhFMJlP/2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IB # WTCCAVUwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+ # YXsIiGX0TkIwHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0P # AQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAk # BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAC # hjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v # dEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAED # MAgGBmeBDAEEATANBgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql # +Eg08yy25nRm95RysQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFF # UP2cvbaF4HZ+N3HLIvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1h # mYFW9snjdufE5BtfQ/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3Ryw # YFzzDaju4ImhvTnhOE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5Ubdld # AhQfQDN8A+KVssIhdXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw # 8MzK7/0pNVwfiThV9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnP # LqR0kq3bPKSchh/jwVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatE # QOON8BUozu3xGFYHKi8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bn # KD+sEq6lLyJsQfmCXBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQji # WQ1tygVQK+pKHJ6l/aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbq # yK+p/pQd52MbOoZWeE4wggfWMIIFvqADAgECAhAJS8amgSAG6MIweRq3uiU3MA0G # CSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg # UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjUwNzAxMDAwMDAwWhcNMjYwMzA3 # MjM1OTU5WjCB3jETMBEGCysGAQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgEC # EwhEZWxhd2FyZTEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xEDAOBgNV # BAUTBzI5MzM2MzcxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNldHRz # MRIwEAYDVQQHEwlDYW1icmlkZ2UxIDAeBgNVBAoTF0FrYW1haSBUZWNobm9sb2dp # ZXMgSW5jMSAwHgYDVQQDExdBa2FtYWkgVGVjaG5vbG9naWVzIEluYzCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMf1sSwHg7wzLGx81ZgJvrYMiCwYtKGv # KNsDCkQsgYkqnuJYfiiRQ6kuk9RRUyqPLa9lLXa6xu6nBI8M6OzG9gxKNg7563G9 # 2P+lC1Gs6V8/uv5ZjzKwbKx3i40b4lHn+VeMNyooPZkzAjG9H8gNrCyCrk8FL7pL # 1eOOPU1Hi3UWX9QHfBbI8HABRgyUPz7sNLDq0rRgcGIwvyIh5pqAoyBJE/HDXMCX # ktktW+OMIIXXpSm9pcCVLT90etajQxCtH6LBV+CFDK/cxFeuDIyWdCR8DMC/oCdz # ZoHbT3taOfN+lyMHUPWC8t7MkPg4OMqNG6nsF9yHDOAvL5h7PZe+npK/X4MYYsr1 # 7XFiak5H6hwiycQ8cVdPp+d0IeaQEnSqv5rI7IHc+V46sYmcmm/z5kvH9XWWD2VN # Aj2Sdald4A9aipa143yG7yvyhmm/TCPco1QboXUuaZh6vpfH6u7mDFiQmOc15hw5 # Bc63ktDaGqHqujU2Fy9CGReZznEqrLv7jXNRuNyRnfc5oF2h8x8s2g0NaB3e3JVD # l4utM2cYNe//akXajWRWWiEG21d2881vtRqyw/eE5fFDHJEdD633EBaLlkrn3K3x # RoIhgsaEFUum9N43y1Cv3QVROED9jfTi5xTZnzSC36/uThhlJE+CFkEJeWGIrANx # x3T+6/gEnQFZAgMBAAGjggICMIIB/jAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5h # ewiIZfROQjAdBgNVHQ4EFgQUU+SbrrGchS0n0MB/su/rrjrMnMQwPQYDVR0gBDYw # NDAyBgVngQwBAzApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNv # bS9DUFMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMIG1BgNV # HR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMFOg # UaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRD # b2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCBlAYIKwYBBQUHAQEE # gYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggr # BgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0T # BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAVV2MjlQ7ejh5cR4thBQn3dxFI8z6OSFP # ZURget+GnxVXGUlX2Qe4aNDHTV6GXqb7+JFTuT68WnAe6aWifWr+WmpoNJ7uQxJH # T5t8OW+nK+eXcZLxF3weAQbxwaImGcEC/LKUwYnGttEM6ZsaqIg7H96w4/egNN/V # i3CrIn4ASbganOwnHRW02DIw0lE6KLWcxDdLx07nLJxrjyAfxQqV2mrnPC9kAIJe # U98TlkTaAzro8OWjiPxdQQ0AVd17hGLsKsjA8nz8HlCB9RmtvQ1LeRtVpHM/A6jX # lP1Cl1mi6HRfAbck1ymomvNwbhLqCHsdoNJUFjZuYRaGHdtLAt+NJkXr5E9kOwoJ # Isiq1n+D60HX4jOZ8crR0RsG2cbz/UD917kjWWRBoY5r4OLnVwCpOpjcFDlun2Bq # usQg+UNWp2Tr6K2MhDePbHTQ2NEbnM89LKLtYHjDoh/zIRGlFqBNwEdCXG6HiThj # fiAm40DhwxwJN7KsN/BJbPlXsNUL0I9YWtxnluFkkwxzWuee5hkweO71qCFJaJHx # fd2/AakHLAgHa7EoOBYlqnjgp+MIIu9Stoi8EhdjeZOCE2JmH/dOstP6TLF2HlZy # pvVaxOMZ0L1syN5z7pebN9dJ1qEsSdPQgxqRijFvbXILCSPrkDl9GO69AEh2HiMe # i8g8MkCqzXMxghqgMIIanAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2Rl # IFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAlLxqaBIAbowjB5Gre6 # JTcwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0B # CQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAv # BgkqhkiG9w0BCQQxIgQgQ9ckem24CgWXMOYjjE+eaGLiG+qpfAqfFgGBQedb3NEw # DQYJKoZIhvcNAQEBBQAEggIAbBFzTV9ig5MwsjU/J1m06YP8bjgVay+7J9wJgE9J # N7s9ZqYcpPQqVFn0lteJWAEZvSTcNCe2qELNm9vCouIP5yKR3KZs5JGqRzIsKRkW # 5pikPBmNxme/qx5kpIFuSz2bXrA/5Cd6TdX5VtqIzIMX0Oek95Rt8Z4SVv5eVdIQ # KHupxO1A9vJS+5GXWAzqWOuo5/Mahq0MeAEkg1E85ZrDbrk1l9tV8QzBfK4560vA # 6puh8XGs5t1hpPFDgR4yiG5sPCa8ePlMJXb7Fo7v8dJtMdvxtXbwdrSe3WZalO2a # 1ohBIlWRSlEMHJ4K5ra/RwuF1FlypCVQFwFAcQutJX5Iw6+hFEDkekh9Re9Qebpt # lIxLc3IuZvNWa31jlm1wov2fYak7iCtm6m4TJFm9BzN9adLglkLr83PbgVc42Nut # 9RMMolFvcMaS6VecvRB24xYfcfuvCua26xYeYdhpO3OrLZkLVehOlN/RotbFMedE # 5DTu94m44EwS2sBW/jGoCbaaCoSU7vcBbUWAcBF64iUae1BBj53wJfvC+J17hcIX # NoOOyv8OQRpkwbJOpgs7n18nvmAVrodFDcBXXdlQQXLqWy/XDSKZmLejk7r4Vffz # zbiyQgBiGHF/I9HgwUMbKaFq/DggzKJVyVqJ9ammcTREt1VBNBYHbxMRZXq/m2gq # Gl2hghd2MIIXcgYKKwYBBAGCNwMDATGCF2IwghdeBgkqhkiG9w0BBwKgghdPMIIX # SwIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJEAEEoGgEZjBkAgEBBglg # hkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQg72dORUF+gD7QlWflBiQx5KjTQfXQ # WScLw4Gvi+xjpHICEB1++hkjrGyqG6uI/NWW6dwYDzIwMjUwODA1MTg0MTE5WqCC # EzowggbtMIIE1aADAgECAhAKgO8YS43xBYLRxHanlXRoMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwHhcNMjUwNjA0MDAwMDAwWhcNMzYwOTAzMjM1OTU5WjBjMQsw # CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRp # Z2lDZXJ0IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBSZXNwb25kZXIgMjAyNSAx # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0EasLRLGntDqrmBWsytX # um9R/4ZwCgHfyjfMGUIwYzKomd8U1nH7C8Dr0cVMF3BsfAFI54um8+dnxk36+jx0 # Tb+k+87H9WPxNyFPJIDZHhAqlUPt281mHrBbZHqRK71Em3/hCGC5KyyneqiZ7syv # FXJ9A72wzHpkBaMUNg7MOLxI6E9RaUueHTQKWXymOtRwJXcrcTTPPT2V1D/+cFll # ESviH8YjoPFvZSjKs3SKO1QNUdFd2adw44wDcKgH+JRJE5Qg0NP3yiSyi5MxgU6c # ehGHr7zou1znOM8odbkqoK+lJ25LCHBSai25CFyD23DZgPfDrJJJK77epTwMP6eK # A0kWa3osAe8fcpK40uhktzUd/Yk0xUvhDU6lvJukx7jphx40DQt82yepyekl4i0r # 8OEps/FNO4ahfvAk12hE5FVs9HVVWcO5J4dVmVzix4A77p3awLbr89A90/nWGjXM # Gn7FQhmSlIUDy9Z2hSgctaepZTd0ILIUbWuhKuAeNIeWrzHKYueMJtItnj2Q+aTy # LLKLM0MheP/9w6CtjuuVHJOVoIJ/DtpJRE7Ce7vMRHoRon4CWIvuiNN1Lk9Y+xZ6 # 6lazs2kKFSTnnkrT3pXWETTJkhd76CIDBbTRofOsNyEhzZtCGmnQigpFHti58CSm # vEyJcAlDVcKacJ+A9/z7eacCAwEAAaOCAZUwggGRMAwGA1UdEwEB/wQCMAAwHQYD # VR0OBBYEFOQ7/PIx7f391/ORcWMZUEPPYYzoMB8GA1UdIwQYMBaAFO9vU0rp5AZ8 # esrikFb2L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF # BQcDCDCBlQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw # LmRpZ2ljZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDovL2NhY2VydHMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5nUlNBNDA5NlNIQTI1 # NjIwMjVDQTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly9jcmwzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy # NTYyMDI1Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBAGUqrfEcJwS5rmBB7NEIRJ5jQHIh+OT2Ik/bNYul # CrVvhREafBYF0RkP2AGr181o2YWPoSHz9iZEN/FPsLSTwVQWo2H62yGBvg7ouCOD # wrx6ULj6hYKqdT8wv2UV+Kbz/3ImZlJ7YXwBD9R0oU62PtgxOao872bOySCILdBg # hQ/ZLcdC8cbUUO75ZSpbh1oipOhcUT8lD8QAGB9lctZTTOJM3pHfKBAEcxQFoHlt # 2s9sXoxFizTeHihsQyfFg5fxUFEp7W42fNBVN4ueLaceRf9Cq9ec1v5iQMWTFQa0 # xNqItH3CPFTG7aEQJmmrJTV3Qhtfparz+BW60OiMEgV5GWoBy4RVPRwqxv7Mk0Sy # 4QHs7v9y69NBqycz0BZwhB9WOfOu/CIJnzkQTwtSSpGGhLdjnQ4eBpjtP+XB3pQC # tv4E5UCSDag6+iX8MmB10nfldPF9SVD7weCC3yXZi/uuhqdwkgVxuiMFzGVFwYbQ # siGnoa9F5AaAyBjFBtXVLcKtapnMG3VH3EmAp/jsJ3FVF3+d1SVDTmjFjLbNFZUW # MXuZyvgLfgyPehwJVxwC+UpX2MSey2ueIu9THFVkT+um1vshETaWyQo8gmBto/m3 # acaP9QsuLj3FNwFlTxq25+T4QwX9xa6ILs84ZPvmpovq90K8eWyG2N01c4IhSOxq # t81nMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJhjANBgkqhkiG9w0BAQsF # ADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL # ExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJv # b3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1OTU5WjBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtHgx0wqYQXK+PEbAHKx1 # 26NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkbsDpz4aH+qbxeLho8I6jY # 3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+AbdJgMQB3h2DZ0Mal5kY # p77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAktVJMQbzIBHYJBYgzWIjk8 # eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3Gmq6bNMI1I7Gb5IBZK4i # vbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv82wnJRfN20VRS3hpLgIR4 # hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638ZxqU14lDnki7CcoKCz6eu # m5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7VvwDDjAmSFTUms+wV/FbW # Bqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2NL+cnT6Toy+rN+nM8M7Ln # LqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTKIsDq1BtmXUqEG1NqzJKS # 4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUozZtqgPrHRVHhGNKlYzyjl # roPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIGA1UdEwEB/wQIMAYBAf8C # AQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtOMB8GA1UdIwQYMBaAFOzX # 44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggr # BgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw # LmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDag # NIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RH # NC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3 # DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N0IfFiBowf0/Dm1wGc/Do # 7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7Jd2E4/iEIUBO41P5F448r # SYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1jEMK/DMm/axFSgoR8n6c3 # nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdogP8HRtrYfctSLANEBfHU1 # 6r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMvNYY2o1f4MXRJDMdTSlOL # h0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBldNE2K9i697cvaiIo2p61E # d2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjTY6hc3hsXMrS+U0yy+GWq # AXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/BdHLiRu7hAWE6bTEm4XYR # kA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyIPbAvjSabnf7+Pu0VrFgo # iovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8dC3JE3rYkrhLULy7Dc90 # G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2jNrbM1pD2T7m3XDCCBY0w # ggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENB # MB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMx # FTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv # bTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orY # WcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8ae # FaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckg # HWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwr # t0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y # 1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjX # WkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIb # Zpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0c # lcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLim # dwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIW # IgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZ # qbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX # 44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3z # bcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBF # BgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl # cnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG # 9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviH # GmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59Pes # MHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3 # A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rb # II01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+ # 2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3wwggN4AgEBMH0waTELMAkG # A1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdp # Q2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1 # IENBMQIQCoDvGEuN8QWC0cR2p5V0aDANBglghkgBZQMEAgEFAKCB0TAaBgkqhkiG # 9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MDgwNTE4NDEx # OVowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU3WIwrIYKLTBr2jixaHlSMAf7QX4w # LwYJKoZIhvcNAQkEMSIEICXvtYrMNF0Nn/cWxyuT5Ddw7C4/luo3O6zIq2nWzLVU # MDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIEqgP6Is11yExVyTj4KOZ2ucrsqzP+Nt # JpqjNPFGEQozMA0GCSqGSIb3DQEBAQUABIICAColv1mD3CrJfTspJw4muOkVp65a # 9g9we+ZBeNnDEWK6wIPyLamwIpQL6Rc7LZjNbv+A+Hbt1wMpUQydXJ5NlJ2S0fJA # 5zyObXNM5X98SyBr5NNAgx/1WQ7U4n7uAxV+Dwfm3hjYAfCztLAgfn50bmcxFcAP # P+EE2IEjUor3wr9ezgp2qwX6NUMWm7WrvWoVtpwCGw8VnHV9Au1veYRsvq9Cw9UT # 4MUbyQV8BLHxZ9uQVFqy8RT4gH1Ki3lUSw0rIgADaesirFCzxwMezymj8LnWb/uG # 19bgWmEZXAMLVyUvmL+mOTdIQTXd/4BK5kDAsVaLSCrPQE0EDH2VTAEm55t5bZz2 # P4XvVvQyzNYyA3vA81MHz8Yr/d/S8wmzCSpNbae9cYAukbj88pN6bfwj1hLXRvKp # qHegLS+bcUkA2LryZUO/S9lAMBqXpAhfQVDRkVBEOxzE3X+f0L/0dKyEi//o20Yw # P3rJH7a18m7TucRKCawgHEJI1T5f41GVpjXP1iPQW5hwkIIShivPw1D4ZtIgfBx1 # nRRK2Sm8ImDLQz74oNJu5lbLB+PJaWZTwPAl69KimD2JcZ90U2rjYg20ZInm4RGt # usohsCP6JlAiEDr3cBYjC0hYyov6gF3/+2amppb4c3abE+oyx7v+Xr5EDjm9GN0N # czkIv24J41HV15fV # SIG # End signature block |