OpenCloudConfig.psm1

<#
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
#>



function Write-Log {
  <#
  .SYNOPSIS
    Write a log message to the windows event log and powershell verbose stream

  .DESCRIPTION
    This function takes a log message and writes it to the windows event log as well as the powershell verbose stream.
    If the specified logName or source are missing from the windows event log, they are created.

  .PARAMETER message
    The message parameter is the log message to be recorded to the event log

  .PARAMETER severity
    The logging severity is the severity rating for the message being recorded.
    There are four ratings:
    - debug: verbose messages about state observations for debugging purposes
    - info: normal messages about state changes
    - warn: messages about unexpected occurences or observations that are not fatal to the running of the application
    - error: messages about failure of a critical logic path in the application

  .PARAMETER source
    The optional source parameter maps directly to the required event log source.
    This should be set to the name of the application being logged.

  .PARAMETER logName
    The optional logName parameter maps directly to the required event log logName.
    Most logs should go to the 'Application' pool

  .EXAMPLE
    These examples show how to call the Write-Log function with named parameters.
    PS C:\> Write-Log -message 'the sun is shining, the weather is sweet.' -severity 'debug' -source 'AmazingDaysApp'
    PS C:\> Write-Log -message 'it has started to rain. an umbrella has been provided.' -severity 'info' -source 'AmazingDaysApp'
    PS C:\> Write-Log -message 'thunder and lightning, very, very frightening.' -severity 'warn' -source 'AmazingDaysApp'
    PS C:\> Write-Log -message 'you are snowed in. the door is jammed shut.' -severity 'error' -source 'AmazingDaysApp'

  .NOTES

  #>

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

    [ValidateSet('debug', 'error', 'info', 'warn')]
    [string] $severity = 'info',

    [string] $source = 'OpenCloudConfig',
    [string] $logName = 'Application'
  )
  if ((-not ([System.Diagnostics.EventLog]::Exists($logName))) -or (-not ([System.Diagnostics.EventLog]::SourceExists($source)))) {
    try {
      New-EventLog -LogName $logName -Source $source
    } catch {
      Write-Error -Exception $_.Exception -message ('failed to create event log source: {0}/{1}' -f $logName, $source)
    }
  }
  switch ($severity[0].ToString().ToLower()) {
    # debug
    'd' {
      $entryType = 'SuccessAudit'
      $eventId = 2
      break
    }
    # warn
    'w' {
      $entryType = 'Warning'
      $eventId = 3
      break
    }
    # error
    'e' {
      $entryType = 'Error'
      $eventId = 4
      break
    }
    # info
    default {
      $entryType = 'Information'
      $eventId = 1
      break
    }
  }
  try {
    Write-EventLog -LogName $logName -Source $source -EntryType $entryType -EventId $eventId -Message $message
  } catch {
    Write-Error -Exception $_.Exception -message ('failed to write to event log source: {0}/{1}. the log message was: {2}' -f $logName, $source, $message)
    Write-Verbose -Message ('failed to write to event log source: {0}/{1}. the log message was: {2}' -f $logName, $source, $message)
  }
  Write-Verbose -Message $message
}

function Get-TooltoolResource {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string] $localPath,

    [Parameter(Mandatory = $true)]
    [string] $sha512,

    [Parameter(Mandatory = $true)]
    [string] $tokenPath,

    [Parameter(Mandatory = $true)]
    [string] $tooltoolHost,

    [string] $eventLogName = 'Application',

    [string] $eventLogSource = 'OpenCloudConfig'
  )
  begin {
    Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime())
  }
  process {
    # read and validate tooltool bearer token from $tokenPath
    try {
      if (-not (Test-Path -Path $tokenPath -ErrorAction SilentlyContinue)) {
        throw [System.IO.FileNotFoundException] ('token file not found at {0}' -f $tokenPath)
      }
      $bearerToken = (Get-Content -Path $tokenPath -Raw)
      # todo: validate token with a regex
      if ($bearerToken) {
        Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: tooltool bearer token obtained at {1}' -f $($MyInvocation.MyCommand.Name), $tokenPath)
      } else {
        Write-Log -logName $eventLogName -source $eventLogSource -Severity 'error' -message ('{0} :: invalid or null token found at {1}' -f $($MyInvocation.MyCommand.Name), $tokenPath)
      }
    } catch {
      Write-Log -logName $eventLogName -source $eventLogSource -Severity 'error' -message ('{0} :: failed to read valid tooltool bearer token at {1}. {2}' -f $($MyInvocation.MyCommand.Name), $tokenPath, $_.Exception.Message)
    }
    if ($bearerToken) {
      # download remote resource
      $url = ('https://{0}/sha512/{1}' -f $tooltoolHost, $sha512)
      $headers = @{
        'Authorization' = ('Bearer {0}' -f $bearerToken)
      }
      return ((Get-RemoteResource -url $url -headers $headers -localPath $localPath -eventLogName $eventLogName -eventLogSource $eventLogSource) -and ((Get-FileHash -Path $localPath -Algorithm 'SHA512').Hash -eq $sha512))
    }
    return $false
  }
  end {
    Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime())
  }
}

function Get-RemoteResource {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string] $localPath,
    [Parameter(Mandatory = $true)]
    [string] $url,
    [hashtable] $headers,
    [string] $eventLogName = 'Application',
    [string] $eventLogSource = 'OpenCloudConfig'
  )
  begin {
    Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime())
  }
  process {
    try {
      if ([Net.ServicePointManager]::SecurityProtocol -ne ([Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12)) {
        [Net.ServicePointManager]::SecurityProtocol = ([Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12)
        [Net.ServicePointManager]::Expect100Continue = $true
        Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: added TLS v1.2 to security protocol support list for current powershell session' -f $($MyInvocation.MyCommand.Name))
      } else {
        Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: detected TLS v1.2 in security protocol support list' -f $($MyInvocation.MyCommand.Name))
      }
    } catch {
      Write-Log -logName $eventLogName -source $eventLogSource -severity 'error' -message ('{0} :: failed to add TLS v1.2 to security protocol support list for current powershell session. {1}' -f $($MyInvocation.MyCommand.Name), $_.Exception.Message)
    }
    try {
      if (Test-Path -Path $localPath -ErrorAction SilentlyContinue) {
        try {
          Remove-Item $localPath -force
          Write-Log -logName $eventLogName -source $eventLogSource -severity 'warn' -message ('{0} :: deleted {1} before download from {2}' -f $($MyInvocation.MyCommand.Name), $localPath, $url)
        } catch {
          Write-Log -logName $eventLogName -source $eventLogSource -Severity 'error' -message ('{0} :: failed to delete {1} before download from {2}. {3}' -f $($MyInvocation.MyCommand.Name), $localPath, $url, $_.Exception.Message)
        }
      }
      $webClient = New-Object -TypeName 'System.Net.WebClient'
      if (($headers) -and ($headers.ContainsKey('Authorization')) -and ($headers['Authorization'])) {
        $webClient.Headers.Add('Authorization', $headers['Authorization'])
      }
      # todo: handle non-auth headers
      $webClient.DownloadFile($url, $localPath)
      Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: remote resource downloaded from {1} to {2} on first attempt' -f $($MyInvocation.MyCommand.Name), $url, $localPath)
    } catch {
      Write-Log -logName $eventLogName -source $eventLogSource -Severity 'error' -message ('{0} :: failed to download remote resource from {1} to {2} on first attempt. {3}' -f $($MyInvocation.MyCommand.Name), $url, $localPath, $_.Exception.Message)
      try {
        # handle redirects (eg: sourceforge)
        if ($headers) {
          Invoke-WebRequest -Uri $url -OutFile $localPath -headers $headers -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
        } else {
          Invoke-WebRequest -Uri $url -OutFile $localPath -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
        }
        Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: remote resource downloaded from {1} to {2} on second attempt' -f $($MyInvocation.MyCommand.Name), $url, $localPath)
      } catch {
        Write-Log -logName $eventLogName -source $eventLogSource -Severity 'error' -message ('{0} :: failed to download remote resource from {1} to {2} on second attempt. {3}' -f $($MyInvocation.MyCommand.Name), $url, $localPath, $_.Exception.Message)
        return $false
      }
    }
    return ((Test-Path -Path $localPath -ErrorAction SilentlyContinue) -and ((Get-Item -Path $localPath -ErrorAction 'SilentlyContinue').Length -gt 0))
  }
  end {
    Write-Log -logName $eventLogName -source $eventLogSource -severity 'debug' -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime())
  }
}