RackspaceCloudOffice.psm1

#requires -version 3

Add-Type -AssemblyName System.Security

function Invoke-RsCloudOfficeRequest {
    <#
    .SYNOPSIS
    REST client for the Rackspace Cloud Office API [1]

    [1]: http://api-wiki.apps.rackspace.com/api-wiki/index.php/Main_Page
    #>


    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,

        [ValidateSet('Default', 'Get', 'Head', 'Post', 'Put', 'Delete', 'Trace', 'Options', 'Merge', 'Patch')]
        [string]$Method = 'Get',

        [ValidateSet('application/json', 'application/x-www-form-urlencoded')]
        [string]$ContentType = 'application/x-www-form-urlencoded',

        [string]$UnpaginateProperty,
        [string]$UserKey,
        [string]$SecretKey,
        [string]$BaseUrl,
        [string]$ConfgFile = "$env:LOCALAPPDATA\RsCloudOfficeApi.config",

        [switch]$WrapBodyInArray,

        [Parameter(ValueFromPipeline=$true)]
        $Body
    )

    begin {
        $UserKey = Get-UserKey $ConfgFile $UserKey
        if (-not $UserKey) {
            throw '-UserKey is required'
        }

        $SecretKey = Get-SecretKey $ConfgFile $SecretKey
        if (-not $SecretKey) {
            throw '-SecretKey is required'
        }

        $BaseUrl = Get-BaseUrl $ConfgFile $BaseUrl
    }

    process {
        if ($UnpaginateProperty) {
            Unpaginate-Request $UnpaginateProperty {
                param([string[]]$queryStringArgs)
                $pagePath = Join-PathWithQueryString $Path $queryStringArgs
                Invoke-SingleRequest Get "${BaseUrl}${pagePath}" $ContentType $UserKey $SecretKey
            }
        }
        else {
            $Body | Invoke-SingleRequest $Method "${BaseUrl}${Path}" $ContentType $UserKey $SecretKey
        }
    }
}

function Set-RsCloudOfficeConfig {
    param(
        [string]$UserKey,
        [string]$SecretKey,
        [string]$BaseUrl,
        [string]$ConfgFile = "$env:LOCALAPPDATA\RsCloudOfficeApi.config"
    )

    $UserKey = Get-UserKey $ConfgFile $UserKey
    if (-not $UserKey) {
        throw '-UserKey is required'
    }

    $SecretKey = Get-SecretKey $ConfgFile $SecretKey
    if (-not $SecretKey) {
        throw '-SecretKey is required'
    }

    $BaseUrl = Get-BaseUrl $ConfgFile $BaseUrl

    Out-Config $ConfgFile @{
        userKey=$UserKey
        secretKey=$SecretKey
        baseUrl=$BaseUrl
    }
}

Export-ModuleMember -Function @('Invoke-RsCloudOfficeRequest', 'Set-RsCloudOfficeConfig')

function Get-UserKey($ConfigFile, $UserKey) {
    Select-FirstValue $UserKey { Get-ConfigFileNode $ConfigFile /config/userKey }
}

function Get-SecretKey($ConfigFile, $SecretKey) {
    Select-FirstValue $SecretKey { Get-ConfigFileNode $ConfigFile /config/secretKey }
}

function Get-BaseUrl($ConfigFile, $BaseUrl) {
    Select-FirstValue $BaseUrl `
        { Get-ConfigFileNode $ConfigFile /config/baseUrl } `
        'https://api.emailsrvr.com'
}

function Invoke-SingleRequest {
    param(
        [parameter(Mandatory=$true)] [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,
        [parameter(Mandatory=$true)] [string]$Uri,
        [parameter(Mandatory=$true)] [string]$ContentType,
        [parameter(Mandatory=$true)] [string]$UserKey,
        [parameter(Mandatory=$true)] [string]$SecretKey,
        [Parameter(ValueFromPipeline=$true)] $Body
    )

    $userAgent = 'https://github.com/rackerlabs/Invoke-RsCloudOfficeRequest'

    if ($WrapBodyInArray -and $Body -ne $null) {
        $Body = @(, $Body)
    }

    $encodedBody = switch ($ContentType) {
        'application/json'                  { ConvertTo-Json -Depth 32 $Body }
        'application/x-www-form-urlencoded' { ConvertTo-FormUrlEncoded $Body }
    }

    try {
        Invoke-WebRequest -Method $Method -Uri $Uri `
            -ContentType $ContentType `
            -Body $encodedBody `
            -UserAgent $userAgent `
            -Headers @{
                'Accept' = 'application/json';
                'X-Api-Signature' = (Compute-ApiSignature $userAgent $UserKey $SecretKey);
            } |
            Convert-Response
    }
    catch [System.Net.WebException] {
        if (-not $_.Exception.Response) {
            throw $_
        }

        $code = $_.Exception.Response.StatusCode -as [int]
        $message = $_.Exception.Response.Headers['x-error-message']

        Write-Error "$code $message"
    }
}

filter Convert-Response {
    Write-Verbose "$($_.StatusCode) response"
    try {
        ConvertFrom-Json $_.Content
    }
    catch {
        $_.Content
    }
}

function ConvertTo-FormUrlEncoded([hashtable] $data) {
    if ($data) {
        $pairs = $data | foreach GetEnumerator | foreach {
            $k = [System.Net.WebUtility]::UrlEncode($_.Name)
            $v = [System.Net.WebUtility]::UrlEncode($_.Value)
            "$k=$v"
        }
        $pairs -join '&'
    }
}

function Compute-ApiSignature($userAgent, $userKey, $secretKey) {
    $timestamp = Get-Date -Format yyyyMMddHHmmss
    $hash = Compute-Sha1 ($userKey + $userAgent + $timestamp + $secretKey)
    "${userKey}:${timestamp}:${hash}"
}

function Compute-Sha1($data) {
    $hasher = New-Object System.Security.Cryptography.SHA1Managed
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($data)
    [System.Convert]::ToBase64String($hasher.ComputeHash($bytes))
}

function Unpaginate-Request {
    param(
        [string]$UnpaginateProperty,
        [scriptblock]$Request,
        [int]$Size = 50
    )

    $page = & $Request "size=$Size"
    $items = @($page | select -ExpandProperty $UnpaginateProperty -ErrorAction SilentlyContinue)
    $offset = $items.Length
    $items

    $usesMarkerPaging = $page.Order -ne $null
    if ($usesMarkerPaging) {
        while ($offset -lt $page.Total) {
            $marker = Select-FirstValue $items[-1].Value $items[-1].CommonName $items[-1]
            $page = & $Request @("marker=$marker", "size=$Size")

            $items = @($page | select -ExpandProperty $UnpaginateProperty)
            $offset += $items.Length
            $items
        }
    }
    else { # offset paging
        while ($offset -lt $page.Total) {
            $page = & $Request @("offset=$Offset","size=$Size")

            $page | select -ExpandProperty $UnpaginateProperty -ErrorAction SilentlyContinue

            $offset = $page.Offset + $Size
        }
    }
}

function Join-PathWithQueryString {
    param(
        [string]$BasePath,
        [string[]]$QueryStringArgs
    )

    $joined = $QueryStringArgs -join '&'

    if ($BasePath -match '\?') {
        "${BasePath}&${joined}"
    }
    else {
        "${BasePath}?${joined}"
    }
}

function Get-ConfigFileNode($ConfigFile, $NodePath) {
    [xml]$cfg = Get-Content $ConfgFile -ErrorAction SilentlyContinue
    if ($cfg) {
        $cfg.SelectNodes($NodePath) | select -First 1 -ExpandProperty '#text' -ErrorAction SilentlyContinue
    }
}

function Out-Config {
    param(
        [parameter(Mandatory=$true)] [string]$Path,
        [parameter(Mandatory=$true)] [hashtable]$Config
    )

    $nodes = $Config.Keys | foreach {
        $val = $Config[$_]
        $val = [Security.SecurityElement]::Escape($val)
        "<$_>$val</$_>"
    }
    $nodes = $nodes -join "`n"

    @"
<?xml version="1.0" encoding="utf-8"?>
<config>
$nodes
</config>
"@
 | Out-File $Path -Encoding ascii
}

function Select-FirstValue {
    $args |
        foreach { if ($_ -is [scriptblock]) { & $_ } else { $_ } } |
        where { $_ } |
        select -First 1
}