Private/Invoke-GuacRest.ps1
|
function Get-GuacTransportOptions { <# .SYNOPSIS Builds a splatting hashtable of transport options from a GuacSession. .DESCRIPTION Extracts the TLS, proxy, and timeout settings carried by a [N2C_GuacAdmin_GuacSession] (or a server base URL for anonymous calls) into a hashtable suitable for splatting to Invoke-GuacRest. Public cmdlets use this so that every request for a session reuses the exact channel configuration the session was created with. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $false)] [AllowNull()] [object] $Session, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Server = [string]::Empty, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Token = [string]::Empty, [Parameter(Mandatory = $false)] [int] $TimeoutSec = 0 ) $options = [ordered]@{} if ($null -ne $Session) { if ([string]::IsNullOrWhiteSpace($Server)) { $Server = [string]$Session.Server } if ([string]::IsNullOrWhiteSpace($Token)) { $Token = [string]$Session.Token } $options['Server'] = $Server $options['CertificateThumbprint'] = [string]$Session.CertificateThumbprint $options['Certificate'] = $Session.Certificate $options['Proxy'] = [string]$Session.Proxy $options['ProxyCredential'] = $Session.ProxyCredential $options['NoProxy'] = [string]$Session.NoProxy if ($TimeoutSec -le 0) { $options['TimeoutSec'] = [int]$Session.TimeoutSec } } else { $options['Server'] = $Server $options['CertificateThumbprint'] = [string]::Empty $options['Certificate'] = $null $options['Proxy'] = [string]::Empty $options['ProxyCredential'] = $null $options['NoProxy'] = [string]::Empty } if ($TimeoutSec -gt 0) { $options['TimeoutSec'] = $TimeoutSec } $options['Token'] = $Token return $options } function Invoke-GuacRest { <# .SYNOPSIS Single private REST transport for all Guacamole API calls. .DESCRIPTION Executes an HTTP request against a Guacamole instance and converts every failure into a terminating [N2C_GuacAdmin_GuacRestException] (raised as a terminating ErrorRecord, so callers can catch it and read StatusCode/Type/Reason/Endpoint/RawBody). No caller can observe a silent failure: non-2xx responses and transport-level errors (connection refused, DNS failure, timeout) always terminate. The authentication token is transmitted via the "Guacamole-Token" header rather than the "token" query parameter. Both are accepted by the server with the header taking priority (see guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java, getAuthenticationToken), and using the header keeps the token out of URIs that proxies, load balancers, and client-side logging may record. On success the response body is returned already decoded: JSON responses are parsed to PowerShell objects on both Windows PowerShell 5.1 and PowerShell 7.x; non-JSON bodies are returned as strings; HEAD and empty-body 204 responses return $true. Cross-version notes: - The request is always sent through a WebRequestSession so that custom headers and the client certificate work identically on 5.1 (where Invoke-WebRequest has no -Headers parameter) and 7.x. - -UseBasicParsing is added only on 5.1 (it does not exist in 7.x). - -CertificateThumbprint is supported on both versions; an X509Certificate2 is installed on the WebRequestSession. - Parameter availability is detected once at module load time ($script:GuacIwrCapabilities). Reference: guacamole/src/main/java/org/apache/guacamole/rest/** #> [CmdletBinding()] [OutputType([object])] param ( [Parameter(Mandatory = $true)] [string] $Server, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Token = [string]::Empty, [Parameter(Mandatory = $true)] [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'HEAD')] [string] $Method, [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [AllowNull()] [object] $Body, [Parameter(Mandatory = $false)] [ValidateSet('application/json', 'application/x-www-form-urlencoded')] [string] $ContentType = 'application/json', [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $CertificateThumbprint = [string]::Empty, [Parameter(Mandatory = $false)] [AllowNull()] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Proxy = [string]::Empty, [Parameter(Mandatory = $false)] [AllowNull()] [System.Management.Automation.PSCredential] $ProxyCredential, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $NoProxy = [string]::Empty, [Parameter(Mandatory = $false)] [int] $TimeoutSec = 30 ) if ($TimeoutSec -le 0) { $TimeoutSec = 30 } $url = ConvertTo-GuacRestUrl -Server $Server -Path $Path $maskedToken = ConvertTo-GuacMaskedSecret -Value $Token Write-Verbose ("N2C.GuacAdmin: {0} {1} (token={2})" -f ($Method, $url, $maskedToken)) # ---- Build the WebRequestSession (headers + client certificate) ---- # A WebRequestSession is used (instead of the -Headers parameter, which # does not exist on Windows PowerShell 5.1) so that custom headers work # identically on both versions. $webSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession $webSession.UserAgent = 'N2C.GuacAdmin' if (-not [string]::IsNullOrEmpty($Token)) { $webSession.Headers.Add('Guacamole-Token', $Token) } if ($null -ne $Certificate) { $webSession.ClientCertificate = $Certificate } # ---- Assemble Invoke-WebRequest arguments ---- $iwrArgs = [ordered]@{} $iwrArgs['Uri'] = $url $iwrArgs['Method'] = $Method $iwrArgs['WebSession'] = $webSession $iwrArgs['TimeoutSec'] = $TimeoutSec $iwrArgs['ErrorAction'] = 'Stop' if ($null -ne $Body) { $bodyString = [string]$Body $iwrArgs['Body'] = $bodyString $iwrArgs['ContentType'] = $ContentType } if (-not [string]::IsNullOrEmpty($CertificateThumbprint)) { $iwrArgs['CertificateThumbprint'] = $CertificateThumbprint } if (-not [string]::IsNullOrEmpty($Proxy)) { $iwrArgs['Proxy'] = $Proxy if ($null -ne $ProxyCredential) { $iwrArgs['ProxyCredential'] = $ProxyCredential } } if (-not [string]::IsNullOrEmpty($NoProxy) -and $script:GuacIwrCapabilities.Contains('NoProxy')) { $iwrArgs['NoProxy'] = $NoProxy } if ($script:GuacIwrCapabilities.Contains('UseBasicParsing')) { $iwrArgs['UseBasicParsing'] = $true } # ---- Execute ---- try { $response = Invoke-WebRequest @iwrArgs } catch [System.Management.Automation.ActionPreferenceStopException] { throw } catch [System.Exception] { $statusCode = Get-GuacResponseStatus -ErrorRecord $_ $bodyText = $null if ($statusCode -ne 0) { $bodyText = Get-GuacResponseBody -ErrorRecord $_ } $apiError = ConvertFrom-GuacErrorBody -Body $bodyText $reason = $null if ($null -ne $apiError.Message) { $reason = [string]$apiError.Message } elseif ($null -ne $apiError.Type) { $reason = [string]$apiError.Type } else { $reason = $_.Exception.Message } # Mask the token in the endpoint (it may appear in the URL path, for # example DELETE /api/tokens/{token}). $safeUrl = $url if (-not [string]::IsNullOrEmpty($Token) -and $safeUrl.Contains($Token)) { $safeUrl = $safeUrl.Replace($Token, $maskedToken) } if ($statusCode -eq 0) { $message = 'Guacamole REST request failed: {0} {1} ({2})' -f ($Method, $safeUrl, $reason) } else { $message = 'Guacamole REST request failed: {0} {1} -> HTTP {2}: {3}' -f ($Method, $safeUrl, $statusCode, $reason) } # Construct via the module-scope type reference (captured at load time # in N2C.GuacAdmin.psm1) rather than New-Object by name: runtime # resolution of the class name inside module scope is unreliable in # some hosts (observed: TypeNotFound under Pester 5.7.1). $exception = $script:GuacRestExceptionType::new($message, $_.Exception) $exception.StatusCode = $statusCode $exception.Type = [string]$apiError.Type $exception.Endpoint = $safeUrl $exception.Reason = $reason $exception.RawBody = $bodyText $category = Get-GuacErrorCategory -StatusCode $statusCode $record = New-Object System.Management.Automation.ErrorRecord( $exception, 'GuacRestRequestFailed', $category, $url) $PSCmdlet.ThrowTerminatingError($record) } # ---- Decode the success response ---- if ($Method -eq 'HEAD') { return $true } $content = $response.Content $responseHeaders = $null try { $responseHeaders = $response.Headers } catch [System.Exception] { $responseHeaders = $null } $responseContentType = Get-GuacResponseHeaderValue -Headers $responseHeaders -Name 'Content-Type' if ($null -eq $content) { return $true } if ($content -is [string]) { if ([string]::IsNullOrEmpty($content)) { return $true } if ($responseContentType -and $responseContentType -match 'json') { return ($content | ConvertFrom-Json) } return $content } # PowerShell 7.x Invoke-WebRequest already decodes JSON content into # objects; return whatever it produced. return $content } |