Public/Invoke-ControlAPIMaster.ps1

function Invoke-ControlAPIMaster {
    <#
    .SYNOPSIS
        Internal function used to make API calls
    .DESCRIPTION
        Internal function used to make API calls
    .PARAMETER Arguments
        Required parameters for the API call
        A URI without a leading "/" will default to the Automate Extension path.
        A URI without a protocol/server will default to the Control Server established by Connect-ControlAPI
    .OUTPUTS
        The returned results from the API call
    .NOTES
        Version: 1.0
        Author: Darren White
        Creation Date: 2020-08-01
        Purpose/Change: Initial script development

        Version: 1.1.0
        Author: Darren White
        Creation Date: 2020-12-01
        Purpose/Change: Include values in $Script:CWCHeaders variable in request

    .EXAMPLE
        $APIRequest = @{
            'URI' = "ReportService.ashx/GenerateReportForAutomate"
            'Body' = ConvertTo-Json @("Session","",@('SessionID','SessionType','Name','CreatedTime'),"NOT IsEnded", "", 10000)
        }
        $AllSessions = Invoke-ControlAPIMaster -Arguments $APIRequest
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        $Arguments,
        [int]$MaxRetry = 5
    )

    Begin {
    }

    Process {
        # Check that we have cached connection info
        If(!$Script:CWCIsConnected) {
            $ErrorMessage = @()
            $ErrorMessage += "Not connected to a Control server."
            $ErrorMessage +=  $_.ScriptStackTrace
            $ErrorMessage += ''
            $ErrorMessage += "----> Run 'Connect-ControlAPI' to initialize the connection before issuing other ControlAPI commands."
            Write-Error ($ErrorMessage | Out-String)
            Return
        }

        # Add default set of arguments
        $Arguments.Item('UseBasicParsing')=$Null
        If (!$Arguments.Headers) {$Arguments.Headers=@{}}
        Foreach($Key in $script:CWCHeaders.Keys){
            If($Arguments.Headers.Keys -notcontains $Key){
                $Arguments.Headers += @{$Key = $script:CWCHeaders.$Key}
            }
        }
        If ($Script:ControlAPIKey) {
            $Arguments.Headers.Item('CWAIKToken') = (Get-CWAIKToken)
        }
        Else {
            $Arguments.Item('Credential')=$Script:ControlAPICredentials
        }

        # Check URI format
        if($Arguments.URI -notlike '*`?*' -and $Arguments.URI -like '*`&*') {
            $Arguments.URI = $Arguments.URI -replace '(.*?)&(.*)', '$1?$2'
        }        
        if($Arguments.URI -notmatch '^(https?://|/)') {
            $Arguments.URI = ($Script:CWCExtensionURI + $Arguments.URI)
        }
        if($Arguments.URI -notmatch '^https?://') {
            $Arguments.URI = ($Script:ControlServer + $Arguments.URI)
        }

        If(!$Arguments.ContainsKey('Method')) {
            $Arguments.Add('Method','POST')
        }
        If(!$Arguments.ContainsKey('ContentType')) {
            $Arguments.Add('ContentType','application/json; charset=utf-8')
        }

        # Issue request
        Write-Debug "Calling Control Server Extension with the following arguments:`n$(($Arguments|Out-String -Stream) -join "`n")"
        Try {
            $ProgressPreference = 'SilentlyContinue'
            $Result = Invoke-WebRequest @Arguments
        } 
        Catch {
            # Start error message
            $ErrorMessage = @()
            If($_.Exception.Response){
                # Read exception response
                $ErrorStream = $_.Exception.Response.GetResponseStream()
                $Reader = New-Object System.IO.StreamReader($ErrorStream)
                $global:ErrBody = $Reader.ReadToEnd() | ConvertFrom-Json

                If($errBody.code){
                    $ErrorMessage += "An exception has been thrown."
                    $ErrorMessage +=  $_.ScriptStackTrace
                    $ErrorMessage += ''    
                    $ErrorMessage += "--> $($ErrBody.code)"
                    If($errBody.code -eq 'Unauthorized'){
                        $ErrorMessage += "-----> $($ErrBody.message)"
                        $ErrorMessage += "-----> Use 'Connect-ControlAPI' to set new authentication."
                    } Else {
                        $ErrorMessage += "-----> $($ErrBody.message)"
                        $ErrorMessage += "-----> ^ Error has not been documented please report. ^"
                    }
                }
            }

            If ($_.ErrorDetails) {
                $ErrorMessage += "An error has been thrown."
                $ErrorMessage +=  $_.ScriptStackTrace
                $ErrorMessage += ''
                $global:errDetails = $_.ErrorDetails | ConvertFrom-Json
                $ErrorMessage += "--> $($errDetails.code)"
                $ErrorMessage += "--> $($errDetails.message)"
                If($errDetails.errors.message){
                    $ErrorMessage += "-----> $($errDetails.errors.message)"
                }
            }
            If (!$ErrorMessage) {$ErrorMessage+='An unknown error was returned'; $ErrorMessage+=$Result|Out-String -Stream}
            Write-Error ($ErrorMessage | Out-String)
            Return
        }

        # Not sure this will be hit with current iwr error handling
        # May need to move to catch block need to find test
        # TODO Find test for retry
        # Retry the request
        $Retry = 0
        while ($Retry -lt $MaxRetry -and $Result.StatusCode -eq 500) {
            $Retry++
            $Wait = $([math]::pow( 2, $Retry))
            Write-Warning "Issue with request, status: $($Result.StatusCode) $($Result.StatusDescription)"
            Write-Warning "$($Retry)/$($MaxRetry) retries, waiting $($Wait)ms."
            Start-Sleep -Milliseconds $Wait
            $ProgressPreference = 'SilentlyContinue'
            $Result = Invoke-WebRequest @Arguments
        }
        If ($Retry -ge $MaxRetry -and $Result.StatusCode -eq 500) {
            $Script:CWCIsConnected=$False
            Write-Error "Max retries hit. Status: $($Result.StatusCode) $($Result.StatusDescription)"
            Return
        }
    }

    End {
        If ($Result) {
            Try {
                Get-Variable -Name CWCServerTime -Scope 1 -ErrorAction Stop
                Set-Variable -Name CWCServerTime -Scope 1 -Value (Get-Date $($Result.Headers.Date))
            } Catch {}
            $SCData = $(If ($Result.Content) {$Result.Content | ConvertFrom-Json})
            If ($SCData -and $SCData.FieldNames -and $SCData.Items -and $SCData.Items.Count -gt 0) {
                $FNames = $SCData.FieldNames
                $SCData.Items | ForEach-Object {
                    $x = $_
                    $SCEventRecord = @{}
                    For ($i = 0; $i -lt $FNames.Length; $i++) {
                        $Null = $SCEventRecord.Add($FNames[$i],$x[$i])
                    }
                    [pscustomobject]$SCEventRecord
                }
            } ElseIf ($SCData) {
                $SCData
            } Else {
                $Result.Content
            }
        }
        Return
    }
}