Private/connect-functions.ps1

# Functions to connect and manage connections

function Get-CSConnection {
    <#
    .SYNOPSIS
        Returns the current CloudStack connection information.

    .DESCRIPTION
        Retrieves the stored connection parameters. Used internally by other functions.
    #>

    if ($null -eq $script:CSConnection) {
        throw "Not connected to CloudStack server. Please run Connect-CSServer first."
    }
    return $script:CSConnection
}

function New-CSSignature {
    param(
        [string]$QueryString,
        [string]$SecretKey
    )

    $hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1
    $hmacsha1.Key = [Text.Encoding]::UTF8.GetBytes($SecretKey)

    $signatureBytes = $hmacsha1.ComputeHash([Text.Encoding]::UTF8.GetBytes($QueryString.ToLower()))
    $signature = [Convert]::ToBase64String($signatureBytes)

    return $signature
}

function Invoke-CSApiRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Command,

        [Parameter(Mandatory=$false)]
        [hashtable]$Parameters = @{}
    )

    $connection = Get-CSConnection

    # Start with base parameters
    $apiParams = @{
        command = $Command
        apiKey = $connection.ApiKey
        response = 'json'
    }

    # Copy the caller's parameters, skipping only $null. An explicit empty string
    # is intentional and must be sent (e.g. Set-CSConfiguration -Value '' to clear
    # a setting), so it is not dropped here.
    foreach ($key in $Parameters.Keys) {
        if ($null -ne $Parameters[$key]) {
            $apiParams[$key] = $Parameters[$key]
        }
    }

    # Sort parameters alphabetically (case-insensitive) and build query string for signing
    $sortedParams = $apiParams.GetEnumerator() | Sort-Object {$_.Name.ToLower()}
    $canonicalQueryStringParts = @()
    $urlQueryStringParts = @()

    foreach ($param in $sortedParams) {
        $key = $param.Name.ToLower()
        $rawValue = $param.Value
        if ($rawValue -is [System.IFormattable]) {
            # Format numbers with the invariant culture so a decimal such as 2.5
            # is never sent (or signed) as '2,5' on comma-decimal locales.
            $value = $rawValue.ToString($null, [System.Globalization.CultureInfo]::InvariantCulture)
        }
        else {
            $value = $rawValue.ToString()
        }

        # CloudStack canonical signing encodes values, not field names. This
        # matters for indexed names such as tags[0].key. Encode both pieces
        # only when constructing the transmitted URL.
        $encodedValue = [System.Uri]::EscapeDataString($value)

        $canonicalQueryStringParts += "$key=$encodedValue"
        $encodedKey = [System.Uri]::EscapeDataString($key)
        $urlQueryStringParts += "$encodedKey=$encodedValue"
    }

    $canonicalQueryString = $canonicalQueryStringParts -join '&'
    $urlQueryString = $urlQueryStringParts -join '&'

    Write-Verbose "Query string for signing: $canonicalQueryString"

    # Generate signature
    $signature = New-CSSignature -QueryString $canonicalQueryString -SecretKey $connection.SecretKey
    $encodedSignature = [System.Uri]::EscapeDataString($signature)

    # Build final URL
    $url = "$($connection.BaseUrl)?$urlQueryString&signature=$encodedSignature"

    Write-Verbose "API Request: $Command"
    Write-Verbose "Full URL: $url"

    try {
        $response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 30 -ErrorAction Stop
        Write-Verbose "API Response received successfully"

        if ($response.PSObject.Properties.Name -match 'errorresponse') {
            $errorMsg = "CloudStack API Error: $($response.errorresponse.errortext)"
            Write-Error $errorMsg
            throw $errorMsg
        }

        return $response
    }
    catch [System.Net.WebException] {
        $statusCode = [int]$_.Exception.Response.StatusCode
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Error "HTTP $statusCode - $statusDescription"
        Write-Error "URL: $url"

        if ($_.Exception.Response) {
            $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
            $responseBody = $reader.ReadToEnd()
            Write-Error "Response: $responseBody"
        }
        throw
    }
    catch {
        Write-Error "CloudStack API request failed: $_"
        Write-Error "Command: $Command"
        Write-Error "URL: $url"
        throw
    }
}