Public/Invoke-IBWAPI.ps1

function Invoke-IBWAPI
{
    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName='Uri')]
    param(
        [Parameter(ParameterSetName='Uri',Mandatory,Position=0)]
        [Uri]$Uri,
        [Parameter(ParameterSetName='HostVersion',Mandatory,Position=0)]
        [Alias('host')]
        [string]$WAPIHost,
        [Parameter(ParameterSetName='HostVersion',Mandatory,Position=1)]
        [Alias('version')]
        [string]$WAPIVersion,
        [Parameter(ParameterSetName='HostVersion',Mandatory,Position=2)]
        [string]$Query,
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method=([Microsoft.PowerShell.Commands.WebRequestMethod]::Get),
        [PSCredential]$Credential,
        [Object]$Body,
        [string]$ContentType = 'application/json; charset=utf-8',
        [string]$OutFile,
        [string]$SessionVariable,
        [Microsoft.PowerShell.Commands.WebRequestSession]$WebSession,
        [switch]$SkipCertificateCheck
    )

    ###########################################################################
    # This function is largely just a wrapper around Invoke-RestMethod that is able
    # to trap errors and present them to the caller in a more useful fashion.
    # For instance, HTTP 400 errors are normally returned as an Exception without
    # any context. But Infoblox returns details about *why* the request was bad in
    # the response body. So we swallow the original exception and throw a new one
    # with the specific error details.
    #
    # We also allow for disabling certificate validation on a per-call basis.
    # However, due to how the underlying .NET framework caches cert validation
    # results, hosts that were ignored may continue to be ignored for
    # a period of time after the initial call even if validation is turned
    # back on. This issue only affects the Desktop edition.
    ###########################################################################

    # Built the URI if they passed individual Host+Version
    if ('HostVersion' -eq $PSCmdlet.ParameterSetName) {
        $apiBase = $script:APIBaseTemplate -f $WAPIHost,$WAPIVersion
        $Uri = '{0}{1}' -f $apiBase,$Query
    }

    # Build a hashtable of parameters that we will later send to Invoke-RestMethod via splatting
    $opts = @{
        Uri         = $Uri          # mandatory param
        Method      = $Method       # has default value, should always exist
        ContentType = $ContentType  # has default value, should always exist
        Verbose     = $false
        ErrorAction = 'Stop'
    }
    if ($PSBoundParameters.Credential) { $opts.Credential = $PSBoundParameters.Credential }
    if ($PSBoundParameters.OutFile)    { $opts.OutFile    = $PSBoundParameters.OutFile }
    if ($PSBoundParameters.WebSession) { $opts.WebSession = $PSBoundParameters.WebSession }

    if ($Body) {
        # If the ContentType was explicitly specified, we're going to assume the caller
        # wants the body passed to Invoke-RestMethod as-is. Otherwise, we're going to try
        # and make sure we send properly UTF-8 encoded JSON so non-ASCII characters don't
        # get messed up.
        if ($PSBoundParameters.ContentType) {
            Write-Debug "Using Body as-is"
            $opts.Body = $Body
            $bodyDebug = $Body
        }
        elseif ($Body -is [string]) {
            # A string value may or may not be valid JSON, but we're goint to UTF-8 encode
            # encode it anyway because the ContentType still claims it is.
            Write-Debug "UTF-8 encoding string Body"
            $opts.Body = [Text.Encoding]::UTF8.GetBytes($Body)
            $bodyDebug = $Body
        }
        else {
            # All that's left is some sort of object that should be JSON convertable
            Write-Debug "Converting Body to JSON and UTF-8 encoding"
            $opts.Body = [Text.Encoding]::UTF8.GetBytes(
                ($Body | ConvertTo-Json -Compress -Depth 5)
            )
            $bodyDebug = $Body | ConvertTo-Json -Depth 5
        }
    }

    # add Core edition parameters if necessary
    if ($SkipCertificateCheck -and $script:SkipCertSupported) {
        $opts.SkipCertificateCheck = $true
    }

    if ('SkipHeaderValidation' -in (Get-Command Invoke-RestMethod).Parameters.Keys) {
        # PS Core doesn't like the way our multipart Content-Type header looks for some
        # reason. So we need to disable its built-in validation.
        $opts.SkipHeaderValidation = $true
    }

    # deal with session stuff
    if ($opts.WebSession) {
        Write-Debug "using explicit session"
    } elseif ($savedSession = Get-IBSession $opts.Uri $opts.Credential) {
        $opts.WebSession = $savedSession
    } else {
        Write-Debug "creating new session"
        # prepare to save the session for later
        $opts.SessionVariable = 'innerSession'
    }

    try
    {
        if ($SkipCertificateCheck -and !$script:SkipCertSupported) {
            [CertValidation]::Ignore();
            Write-Verbose "Disabled cert validation"
        }

        try {
            $methodUpper = $opts.Method.ToString().ToUpper()

            if ($PSCmdlet.ShouldProcess($opts.Uri, $methodUpper)) {

                # send the request
                Write-Verbose "$methodUpper $($opts.Uri)"
                if ($bodyDebug) { Write-Verbose "Body:`n$bodyDebug" }
                $response = Invoke-RestMethod @opts

                # attempt to detect a master candidate's meta refresh tag
                if ($response -is [Xml.XmlDocument] -and $response.OuterXml -match 'CONTENT="0; URL=https://(?<gm>[\w.]+)"') {
                    $gridmaster = $matches.gm
                    Write-Warning "WAPIHost $($opts.Uri.Authority) is requesting a redirect to $gridmaster. Retrying request against that host."

                    # retry the request using the parsed grid master
                    $opts.Uri = [uri]$opts.Uri.ToString().Replace($opts.Uri.Authority, $gridmaster)
                    Write-Verbose "$methodUpper $($opts.Uri)"
                    if ($bodyDebug) { Write-Verbose "Body:`n$bodyDebug" }
                    Invoke-RestMethod @opts
                } else {
                    Write-Output $response
                }

                # make sure to send our session variable up to the caller scope if defined
                if ($savedSession) {
                    if ($SessionVariable) {
                        Set-Variable -Name $SessionVariable -Value $savedSession -Scope 2
                    }
                } else {
                    if ((-not $opts.WebSession) -and $SessionVariable) {
                        Set-Variable -Name $SessionVariable -Value $innerSession -Scope 2
                    }

                    # save the session variable internally to re-use later
                    Set-IBSession $opts.Uri $opts.Credential $innerSession
                }

            }
        }
        finally {
            if ($SkipCertificateCheck -and !$script:SkipCertSupported) {
                [CertValidation]::Restore();
                Write-Verbose "Enabled cert validation"
            }
        }
    }
    catch
    {
        $ex = $_.Exception
        $response = $ex.Response

        if ($response.StatusCode -in 400,404) {

            # Since we can't catch explicit exception types between PowerShell editions
            # without errors for non-existent types, we need to string match the type names
            # and re-throw anything we don't care about.
            $exType = $ex.GetType().FullName
            if ('System.Net.WebException' -eq $exType) {

                # This is the exception that gets thrown in PowerShell Desktop edition

                # grab the raw response body
                $sr = New-Object IO.StreamReader($response.GetResponseStream())
                $sr.BaseStream.Position = 0
                $sr.DiscardBufferedData()
                $body = $sr.ReadToEnd()
                Write-Debug "Error Body:`n$body"

            } elseif ('Microsoft.PowerShell.Commands.HttpResponseException' -eq $exType) {

                # This is the exception that gets thrown in PowerShell Core edition

                # Response object type depends on platform
                # Linux type: System.Net.Http.CurlHandler+CurlResponseMessage
                # Mac type: ???
                # Win type: System.Net.Http.HttpResponseMessage

                # Currently in PowerShell 6, there's no way to get the raw response body from an
                # HttpResponseException because they dispose the response stream.
                # https://github.com/PowerShell/PowerShell/issues/5555
                # https://get-powershellblog.blogspot.com/2017/11/powershell-core-web-cmdlets-in-depth.html
                # However, a "processed" version of the body is available via ErrorDetails.Message
                # which *should* work for us. The processing they're doing should only be removing HTML
                # tags. And since our body should be JSON, there shouldn't be any tags to remove.
                # So we'll just go with it for now until someone reports a problem.
                $body = $_.ErrorDetails.Message
                Write-Debug "Error Body:`n$body"

            } else {
                $PSCmdlet.WriteError([Management.Automation.ErrorRecord]::new(
                    $_, $null, [Management.Automation.ErrorCategory]::InvalidOperation, $null
                ))
                return
            }

            $wapiErr = ConvertFrom-Json $body -EA Ignore
            if ($wapiErr) {
                $PSCmdlet.WriteError([Management.Automation.ErrorRecord]::new(
                    $wapiErr.Error, $null, [Management.Automation.ErrorCategory]::InvalidOperation, $null
                ))
            } else {
                $PSCmdlet.WriteError([Management.Automation.ErrorRecord]::new(
                    $body, $null, [Management.Automation.ErrorCategory]::InvalidOperation, $null
                ))
            }

        } else {
            Write-Debug ($response | ConvertTo-Json)
            $PSCmdlet.WriteError($_)
        }
    }
}