Unicorn.psm1

$ErrorActionPreference = 'Stop'
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path
$MicroCHAP = $ScriptPath + '\MicroCHAP.dll'
Add-Type -Path $MicroCHAP

Function Sync-Unicorn {
    Param(
        [Parameter(Mandatory=$True)]
        [string]$ControlPanelUrl,

        [Parameter(Mandatory=$True)]
        [string]$SharedSecret,

        [string[]]$Configurations,

        [string]$Verb = 'Sync',

        [switch]$SkipTransparentConfigs,

        [switch]$DebugSecurity
    )

    # PARSE THE URL TO REQUEST
    $parsedConfigurations = '' # blank/default = all
    
    if($Configurations) {
        $parsedConfigurations = ($Configurations) -join "^"
    }

    $skipValue = 0
    if($SkipTransparentConfigs) {
        $skipValue = 1
    }

    $url = "{0}?verb={1}&configuration={2}&skipTransparentConfigs={3}" -f $ControlPanelUrl, $Verb, $parsedConfigurations, $skipValue 

    if($DebugSecurity) {
        Write-Host "Sync-Unicorn: Preparing authorization for $url"
    }

    # GET AN AUTH CHALLENGE
    $challenge = Get-Challenge -ControlPanelUrl $ControlPanelUrl

    if($DebugSecurity) {
        Write-Host "Sync-Unicorn: Received challenge from remote server: $challenge"
    }

    # CREATE A SIGNATURE WITH THE SHARED SECRET AND CHALLENGE
    $signatureService = New-Object MicroCHAP.SignatureService -ArgumentList $SharedSecret

    $signature = $signatureService.CreateSignature($challenge, $url, $null)

    if($DebugSecurity) {
        Write-Host "Sync-Unicorn: MAC '$($signature.SignatureSource)'"
        Write-Host "Sync-Unicorn: HMAC '$($signature.SignatureHash)'"
        Write-Host "Sync-Unicorn: If you get authorization failures compare the values above to the Sitecore logs."
    }

    Write-Host "Sync-Unicorn: Executing $Verb..."

    # USING THE SIGNATURE, EXECUTE UNICORN
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $result = Invoke-StreamingWebRequest -Uri $url -Mac $signature.SignatureHash -Nonce $challenge

    if($result.TrimEnd().EndsWith('****ERROR OCCURRED****')) {
        throw "Unicorn $Verb to $url returned an error. See the preceding log for details."
    }

    # Uncomment this if you want the console results to be returned by the function
    # $result
}

Function Get-Challenge {
    Param(
        [Parameter(Mandatory=$True)]
        [string]$ControlPanelUrl
    )

    $url = "$($ControlPanelUrl)?verb=Challenge"

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $result = Invoke-WebRequest -Uri $url -TimeoutSec 360 -UseBasicParsing

    $result.Content
}

Function Invoke-StreamingWebRequest($Uri, $MAC, $Nonce) {
    $responseText = new-object -TypeName "System.Text.StringBuilder"

    $request = [System.Net.WebRequest]::Create($Uri)
    $request.Headers["X-MC-MAC"] = $MAC
    $request.Headers["X-MC-Nonce"] = $Nonce
    $request.Timeout = 10800000

    $response = $request.GetResponse()
    $responseStream = $response.GetResponseStream()
    $responseStreamReader = new-object System.IO.StreamReader $responseStream
    
    while(-not $responseStreamReader.EndOfStream) {
        $line = $responseStreamReader.ReadLine()

        if($line.StartsWith('Error:')) {
            Write-Host $line.Substring(7) -ForegroundColor Red
        }
        elseif($line.StartsWith('Warning:')) {
            Write-Host $line.Substring(9) -ForegroundColor Yellow
        }
        elseif($line.StartsWith('Debug:')) {
            Write-Host $line.Substring(7) -ForegroundColor Gray
        }
        elseif($line.StartsWith('Info:')) {
            Write-Host $line.Substring(6) -ForegroundColor White
        }
        else {
            Write-Host $line -ForegroundColor White
        }

        [void]$responseText.AppendLine($line)
    }

    return $responseText.ToString()
}

Export-ModuleMember -Function Sync-Unicorn