Private/Invoke-R1RestMethod.ps1

function Invoke-R1RestMethod {
    <#
    .SYNOPSIS
    Wrapper for Invoke-WebRequest to call the RadiantOne API

    .DESCRIPTION
    Sends requests to the RadiantOne API, catches exceptions and outputs success.
    Acts as a wrapper for the Invoke-WebRequest cmdlet so that status codes can be queried and
    acted on, and so that every command in the module sends its request the same way.

    The authentication token held in the module scope session is sent as a bearer token, unless
    an Authorization header is supplied by the caller.
    Unless otherwise specified, requests are sent with ContentType=application/json.

    .PARAMETER Method
    The method for the REST Method.
    Only accepts GET, POST, PUT, PATCH or DELETE

    .PARAMETER URI
    The address of the API or service to send the request to.

    .PARAMETER Body
    The body of the request to send to the API

    .PARAMETER Headers
    The header of the request to send to the API.

    .PARAMETER SessionVariable
    If passed, will be sent to Invoke-WebRequest which in turn will create a websession variable
    using the string value as the name. This variable will only exist in the current scope, so
    will be set as the value of the module scope session WebSession property.
    Cannot be specified with WebSession

    .PARAMETER WebSession
    Accepts a WebRequestSession object containing session details
    Cannot be specified with SessionVariable

    .PARAMETER Credential
    See Invoke-WebRequest

    .PARAMETER TimeoutSec
    See Invoke-WebRequest
    Specify a timeout value in seconds

    .PARAMETER Certificate
    See Invoke-WebRequest
    The client certificate used for a secure web request.

    .PARAMETER CertificateThumbprint
    See Invoke-WebRequest
    The thumbprint of the certificate to use for client certificate authentication.

    .PARAMETER ContentType
    Specifies the content type of the web request.

    .PARAMETER OutFile
    Saves the response body to the specified file, for the endpoints which return a file download.

    .PARAMETER SkipCertificateCheck
    Bypass certificate validation for the request. Applies to deployments presenting a self-signed
    certificate.

    .PARAMETER SslProtocol
    Force the request to use a specific TLS protocol, e.g. 'Tls12', or 'Tls12,Tls13' to allow either.
    PowerShell Core only; ignored under Windows PowerShell, whose Invoke-WebRequest has no such
    parameter.

    Left unset the connection negotiates the strongest protocol both ends support, so specify this
    only for an endpoint which requires a specific protocol.

    .EXAMPLE
    Invoke-R1RestMethod -Uri $URI -Method GET

    Send request to the RadiantOne API
    #>

    [CmdletBinding(DefaultParameterSetName = 'WebSession')]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')]
        [String]$Method,

        [Parameter(Mandatory = $true)]
        [String]$URI,

        [Parameter(Mandatory = $false)]
        [Object]$Body,

        [Parameter(Mandatory = $false)]
        [hashtable]$Headers,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'SessionVariable'
        )]
        [String]$SessionVariable,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'WebSession'
        )]
        [Microsoft.PowerShell.Commands.WebRequestSession]$WebSession,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [int]$TimeoutSec,

        [Parameter(Mandatory = $false)]
        [System.Security.Cryptography.X509Certificates.X509Certificate]$Certificate,

        [Parameter(Mandatory = $false)]
        [string]$CertificateThumbprint,

        [Parameter(Mandatory = $false)]
        [string]$ContentType,

        [Parameter(Mandatory = $false)]
        [string]$OutFile,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCertificateCheck,

        [Parameter(Mandatory = $false)]
        [string]$SslProtocol
    )

    Begin {

        #Set defaults for all function calls
        $ProgressPreference = 'SilentlyContinue'
        $PSBoundParameters.Add('UseBasicParsing', $true)

        #Send the session token as a bearer token unless the caller supplied its own Authorization header
        if (-not ($PSBoundParameters.ContainsKey('Headers'))) {

            $PSBoundParameters.Add('Headers', @{ })

        }

        if ((-not ($PSBoundParameters['Headers'].ContainsKey('Authorization'))) -and
            (-not ([string]::IsNullOrEmpty($Script:psRadiantOneSession.Token)))) {

            $PSBoundParameters['Headers'].Add('Authorization', "Bearer $($Script:psRadiantOneSession.Token)")

        }

        if ($null -ne $Script:psRadiantOneSession.WebSession) {

            #use the WebSession if it exists in the module scope, and alternate session is not specified.
            if (-not ($PSBoundParameters.ContainsKey('WebSession'))) {

                $PSBoundParameters.Add('WebSession', $Script:psRadiantOneSession.WebSession)

            }

        }

        #Unless otherwise specified, expected content type is json
        if (-not ($PSBoundParameters.ContainsKey('ContentType'))) {

            $PSBoundParameters.Add('ContentType', 'application/json')

        }

        #Bypass strict RFC header parsing in PS Core
        if ($IsCoreCLR) {

            $PSBoundParameters.Add('SkipHeaderValidation', $true)

            #No protocol is pinned here. .NET negotiates the strongest protocol both ends support,
            #and WebSslProtocol is a flags enum, so pinning 'Tls12' would set TLS 1.2 as the only
            #permitted protocol and exclude TLS 1.3. A caller needing a specific protocol passes
            #SslProtocol, which reaches Invoke-WebRequest unaltered.

        } else {

            #SslProtocol and SkipCertificateCheck are PowerShell Core parameters
            $null = $PSBoundParameters.Remove('SslProtocol')
            $null = $PSBoundParameters.Remove('SkipCertificateCheck')

            if ($SkipCertificateCheck) {

                [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }

            }

            #A SecurityProtocol of SystemDefault (0) lets Schannel negotiate the strongest protocol
            #both ends support, which is the correct modern value and is left untouched. Only a
            #process pinned to explicit legacy protocols needs TLS 1.2 adding, and it is combined
            #with the protocols already permitted rather than replacing them.
            $SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol

            if (([int]$SecurityProtocol -ne 0) -and
                ([Net.SecurityProtocolType].GetEnumNames() -contains 'Tls12') -and
                (-not ($SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12)))) {

                [Net.ServicePointManager]::SecurityProtocol = $SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

            }

        }

    }

    Process {

        #Show sanitised request body if in debug mode
        If ([System.Management.Automation.ActionPreference]::SilentlyContinue -ne $DebugPreference) {

            If (($PSBoundParameters.ContainsKey('Body')) -and ($null -ne $PSBoundParameters['Body'])) {

                switch (($Body).GetType().Name) {

                    'String' { Write-Debug "[Body] $(Hide-SecretValue -InputValue $Body)" }

                    'Byte[]' { Write-Debug "[Body] $(Hide-SecretValue -InputValue $([System.Text.Encoding]::UTF8.GetString($Body)))" }

                }

            }

        }

        #Send a String body as raw UTF8 bytes so ParameterBinding/module logging of the
        #Invoke-WebRequest call records a non-revealing type name (System.Byte[]) instead of the
        #literal request content
        if (($PSBoundParameters.ContainsKey('Body')) -and ($null -ne $PSBoundParameters['Body']) -and (($PSBoundParameters['Body']).GetType().Name -eq 'String')) {

            $PSBoundParameters['Body'] = [System.Text.Encoding]::UTF8.GetBytes($PSBoundParameters['Body'])

        }

        try {

            #make web request, splat PSBoundParameters
            $APIResponse = Invoke-WebRequest @PSBoundParameters -ErrorAction Stop

        } catch [System.UriFormatException] {

            #Catch URI Format errors. Likely module scope url is not set; Connect-R1Session should be run.
            $PSCmdlet.ThrowTerminatingError(

                [System.Management.Automation.ErrorRecord]::new(

                    "$PSItem Run Connect-R1Session",
                    'psRadiantOne.NoSession',
                    [System.Management.Automation.ErrorCategory]::ConnectionError,
                    $PSItem

                )

            )

        } catch {
            #catch other errors

            If ($null -ne $($PSItem)) {

                $Script:psRadiantOneSession.LastError = $PSItem
                $Script:psRadiantOneSession.LastErrorTime = Get-Date

                $ErrorID = $PSItem | Select-Object -ExpandProperty FullyQualifiedErrorId

                try {

                    $ErrorDetails = $PSItem.ErrorDetails | ConvertFrom-Json -ErrorAction Stop
                    $ValidJson = $true

                } catch {

                    $ValidJson = $false
                    $ErrorMessage = $null

                } finally {

                    if ($ValidJson) {

                        #The RadiantOne ClientError object reports status, code and message.
                        #The authentication endpoints instead report a reason.
                        switch ($ErrorDetails) {

                            { $null -ne $PSItem.message } {
                                $ErrorMessage = $ErrorDetails | Select-Object -ExpandProperty message
                            }
                            { $null -ne $PSItem.reason } {
                                $ErrorMessage = $ErrorDetails | Select-Object -ExpandProperty reason
                            }
                            { $null -ne $PSItem.code } {
                                $ErrorID = $ErrorID, $ErrorDetails.code -join ','
                            }

                        }

                    } else {

                        $ErrorMessage = $PSItem.ErrorDetails

                    }

                    if ([string]::IsNullOrWhiteSpace($ErrorMessage)) {

                        #A response carrying no body leaves nothing to report, and an empty message
                        #hides which request failed and how. Fall back to the http status.
                        $Response = $PSItem.Exception.Response

                        if ($null -ne $Response) {

                            #ReasonPhrase under PowerShell Core, StatusDescription under Windows PowerShell
                            $StatusText = if ($null -ne $Response.PSObject.Properties['ReasonPhrase']) {
                                $Response.ReasonPhrase
                            } else {
                                $Response.StatusDescription
                            }

                            $ErrorMessage = "The API returned HTTP $([int]$Response.StatusCode) $StatusText with no error details. Request was $Method $URI"

                        } else {

                            $ErrorMessage = $PSItem.Exception.Message

                        }

                    }

                    #throw the error
                    $PSCmdlet.ThrowTerminatingError(

                        [System.Management.Automation.ErrorRecord]::new(

                            [System.Exception]::new($ErrorMessage),
                            $ErrorID,
                            [System.Management.Automation.ErrorCategory]::NotSpecified,
                            $PSItem

                        )

                    )

                }

            }

        } finally {

            #Add Command Data to module scope session variable
            $Script:psRadiantOneSession.LastCommand = Get-ParentFunction | Select-Object -ExpandProperty CommandData
            $Script:psRadiantOneSession.LastCommandResults = $APIResponse
            $Script:psRadiantOneSession.LastCommandTime = Get-Date

            #If Session Variable passed as argument
            If ($PSCmdlet.ParameterSetName -eq 'SessionVariable') {

                #Make the WebSession available in the module scope
                $Script:psRadiantOneSession.WebSession = $(Get-Variable $(Get-Variable SessionVariable).Value).Value

            }

            #If Command Succeeded
            if ($?) {

                #Status code indicates success
                If ($APIResponse.StatusCode -match '^20\d$') {

                    #Pass APIResponse to Get-R1Response
                    $APIResponse | Get-R1Response

                }

            }

        }

    }

    End { }

}