ScreenConnectUtils.psm1


# module variables
$ScriptPath = Split-Path (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition -Parent
$ModuleName = (Get-Item (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition).BaseName


# turn on informational messages
$InformationPreference = 'Continue'

# load localized language
Import-LocalizedData -BindingVariable 'Messages' -FileName 'Messages' -BaseDirectory (Join-Path $ScriptPath 'lang')

# load the config
if ( Test-Path "$ScriptPath\DefaultConfig.psd1" ) {

    # configuration parameters
    # we have to add it in the loader script so that it's available to the dot sourced files
    $ConfigSplat = @{
        Name        = $ModuleName
        CompanyName = 'Brooksworks'
        DefaultPath = "$ScriptPath\DefaultConfig.psd1"
    }

    # create config variable
    # we have to add it in the loader script so that it's available to the dot sourced files
    $Config = Import-Configuration @ConfigSplat

}

# import cached data
if ( Test-Path "$ScriptPath\data\*.json" ) {

    $Data = @{}
    Get-ChildItem -Path "$ScriptPath\data" -Filter '*.json' |
        ForEach-Object { $Data.($_.BaseName) = Get-Content $_.FullName | ConvertFrom-Json }

}
<#
.SYNOPSIS
 Extracts attachments from a JNLP file.
 
.PARAMETER Path
 Path to the JNLP file to search for attachments.
 
.PARAMETER Destination
 Path to destination directory.
 
#>

function Expand-JnlpAttachments {

    param(

        [Parameter(Mandatory)]
        [ValidatePattern('\.jnlp$')]
        $Path

    )

    $Path = [System.IO.FileInfo][string]( Resolve-Path $Path )

    Get-Content $Path -Raw |
        ForEach-Object { ([xml]$_).jnlp.'application-desc'.argument } |
        Where-Object { $_ -match 'JNLP_ATTACHMENTS' } |
        ForEach-Object { $_.Split('=',2)[1].Split(';') } |
        Select-Object @{N='Path';E={ Join-Path $Path.Directory.FullName $_.Split(',',2)[0] }}, @{N='Base64Data';E={ $_.Split(':',2)[1] }} |
        ForEach-Object {
            $DecodedData = [System.Convert]::FromBase64String( $_.Base64Data )
            $MemoryStream = New-Object System.IO.MemoryStream ( , $DecodedData )
            $DeflateStream = New-Object System.IO.Compression.DeflateStream ( $MemoryStream, [System.IO.Compression.CompressionMode]::Decompress )
            $ByteList = New-Object collections.generic.list[byte]
            while ( ( $Byte = $DeflateStream.ReadByte() ) -ne -1 ) { $ByteList.Add( $Byte) }
            Set-Content -Encoding Byte -Value $ByteList.ToArray() -Path $_.Path
        }

}

<#
.SYNOPSIS
 Creates an Immediate Scheduled Task
 
.PARAMETER ComputerName
 The computer(s) for the task to be executed.
  
.PARAMETER TaskName
 The name of the task.
  
 Note: Can not be a duplicate name
 
.PARAMETER Description
 The description for the task.
 
.PARAMETER Command
 The main command to execute.
 
.PARAMETER ArgumentList
 The list of parameters to pass to the executable.
  
 Note: A single parameter will not be modified, however
 if an array of parameters is passed any parameter
 containing a space will be wrapped in double quotes.
 
.PARAMETER WorkingDirectory
 The working directory for the task if applicable.
  
 Note: This variable is interpereted on the local machine.
  
.PARAMETER Credential
 The credential to use when creating the task.
  
.PARAMETER Wait
 Wait for the task to complete before continuing.
 
#>

function New-ImmediateScheduledTask {

    [CmdletBinding()]
    param(
    
        [string[]]
        $ComputerName,

        [Parameter(Mandatory)]
        [string]
        $TaskName,

        [string]
        $Description,

        [Parameter(Mandatory)]
        [string]
        $Command,

        [string[]]
        $ArgumentList,

        [string]
        $WorkingDirectory,

        [pscredential]
        $Credential,

        [switch]
        $Wait

    )

    # immediate task template
    $TaskXmlTemplate = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2019-09-11T15:07:41.203317</Date>
    <Author>METHODE\sgraybrook</Author>
    <URI>\Test Task</URI>
  </RegistrationInfo>
  <Triggers>
    <RegistrationTrigger>
      <EndBoundary>2020-09-11T15:10:31</EndBoundary>
      <Enabled>true</Enabled>
    </RegistrationTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <DeleteExpiredTaskAfter>PT1M</DeleteExpiredTaskAfter>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command></Command>
    </Exec>
  </Actions>
</Task>
'@


    # parse the arguments
    # if there is only one argument we pass as is
    # otherwise we wrap arguments with spaces in double quotes and join together
    $Arguments = if ( $ArgumentList.Count -eq 1 ) {
        
        $ArgumentList[0].Trim()

    } elseif ( $ArgumentList.Count -gt 1 ) {

        ( $Arguments | % Trim | %{ if ( $_.IndexOf(' ') -ne -1 ) { '"{0}"' -f $_ } else { $_ } } ) -join ' '

    }
    
    # set the parameters
    $ParamSplat = @{}
    if ( $Credential ) { $ParamSplat.Credential = $Credential }
    if ( $ComputerName ) { $ParamSplat.ComputerName = $ComputerName }

    # register the task
    Invoke-Command @ParamSplat -ScriptBlock {

        param([xml]$TaskXml, [string]$TaskName, [string]$Description, [string]$Command, [string]$Arguments, [string]$WorkingDirectory, [bool]$Wait)

        # set the task name
        $TaskXml.Task.RegistrationInfo.URI = '\' + $TaskName

        # set the description or remove it
        if ( -not [string]::IsNullOrEmpty( $Description ) ) {

            $DescriptionNode = $TaskXml.CreateElement('Description', $TaskXml.DocumentElement.NamespaceURI)
            $DescriptionNode.InnerText = $Description
            [void]$TaskXml.Task.RegistrationInfo.AppendChild( $DescriptionNode )

        }
    
        # set the author to the executing user
        $TaskXml.Task.RegistrationInfo.Author = $env:USERDOMAIN, $env:USERNAME -join '\'
    
        # registration time is now
        $TaskXml.Task.RegistrationInfo.Date = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffffff')
    
        # set trigger to expire in one minute
        $TaskXml.Task.Triggers.RegistrationTrigger.EndBoundary = (Get-Date).AddMinutes(1).ToString('yyyy-MM-ddTHH:mm:ss')
    
        # task executable
        $TaskXml.Task.Actions.Exec.Command = $Command
    
        # task arguments
        if ( -not [string]::IsNullOrEmpty( $Arguments ) ) {

            $ArgumentsNode = $TaskXml.CreateElement('Arguments', $TaskXml.DocumentElement.NamespaceURI)
            $ArgumentsNode.InnerText = $Arguments
            [void]$TaskXml.Task.Actions.Exec.AppendChild($ArgumentsNode)

        }

        # set the working directory
        if ( -not [string]::IsNullOrEmpty( $WorkingDirectory ) ) {
            
            $WorkingDirectoryNode = $TaskXml.CreateElement('WorkingDirectory', $TaskXml.DocumentElement.NamespaceURI)
            $WorkingDirectoryNode.InnerText = $WorkingDirectory
            [void]$TaskXml.Task.Actions.Exec.AppendChild( $WorkingDirectoryNode )

        }

        # create the temporary task xml file
        $TempXmlPath = Join-Path $env:TEMP 'ImmediateTaskDefinition.xml'
        Set-Content -Path $TempXmlPath -Value $TaskXml.InnerXml -Encoding Unicode -Force -Confirm:$false

        # schedule the task
        if ( (Start-Process -FilePath 'schtasks.exe' -ArgumentList "/Create /XML ""$TempXmlPath"" /tn ""$TaskName""" -Wait -PassThru).ExitCode -eq 0 ) {

            Write-Host ( 'Task ''{0}'' was scheduled successfully on computer {1}' -f $TaskName, $env:COMPUTERNAME )

            # should we wait?
            if ( $Wait ) {

                Start-Sleep -Seconds 5

                while ( (Start-Process -FilePath 'schtasks.exe' -ArgumentList "/Query /tn ""$TaskName""" -Wait -PassThru).ExitCode -eq 0 ) {

                
                    Write-Host ( 'Waiting for task ''{0}'' to finish on {1}' -f $TaskName, $env:COMPUTERNAME )

                    Start-Sleep -Seconds 5
            
                }

            }

        } else {

            Write-Error ( 'Failed to create task ''{0}'' on computer {1}' -f $TaskName, $env:COMPUTERNAME )

        }

    } -ArgumentList $TaskXmlTemplate, $TaskName, $Description, $Command, $Arguments, $WorkingDirectory, $Wait.IsPresent

}

<#
.SYNOPSIS
 Utility function to check if host is online.
#>

function Test-HostConnection {

    [CmdletBinding()]
    param(

        [string]
        $ComputerName

    )

    if ( $PSBoundParameters.Keys -notcontains 'ErrorAction' ) {

        $ErrorActionPreference = 'Stop'

    }

    Write-Verbose ( $Messages.CheckingHostConnectionVerboseMessage -f $ComputerName )

    # verify computer is responding

    if ( -not( Test-Connection -ComputerName $ComputerName -Count 1 -Quiet ) ) {

        Write-Error ( $Messages.HostConnectionFailedError -f $ComputerName )

        return $false

    }
    
    # check that port 445 is open

    $Socket= New-Object Net.Sockets.TcpClient
    $IAsyncResult= [IAsyncResult] $Socket.BeginConnect( $ComputerName, 445, $null, $null )
    $IAsyncResult.AsyncWaitHandle.WaitOne( 500, $true ) > $null
    $PortOpen = $Socket.Connected
    $Socket.close()

    if ( -not $PortOpen ) {

        Write-Error ( $Messages.HostPortConnectionFailedError -f $ComputerName )

        return $false

    }
    
    
    return $true

}

<#
.DESCRIPTION
 Connect to a ScreenConnect remote support session from PowerShell. Note that
 you must have the Guest Session starter extension enabled.
 
 See: https://docs.connectwise.com/ConnectWise_Control_Documentation/Supported_extensions/Productivity/Guest_Session_Starter
  
.PARAMETER ScreenConnectUri
 URI for ScreenConnect instance
  
.PARAMETER SessionName
 What should your session be named
  
.PARAMETER ScreenConnectPath
 Path to ScreenConnect files
#>

function Connect-SupportSession {

    param(

        [Parameter(Mandatory=$true)]
        [ValidatePattern('(?# must include http/https )^https?://.+')]
        [ValidateNotNullOrEmpty()]
        [string]
        $ScreenConnectUri,

        [ValidateNotNullOrEmpty()]
        [string]
        $SessionName = "PowerShell Session - $env:COMPUTERNAME",

        [ValidateNotNullOrEmpty()]
        [string]
        $ScreenConnectPath = ( Join-Path $env:TEMP 'ScreenConnectClient' )

    )

    $ErrorActionPreference = 'Stop'

    if ( $PSVersionTable.PSVersion.Major -lt 3 ) {

        throw 'Minimum supported version of PowerShell is 3.0'

    }

    $ScreenConnectPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ScreenConnectPath)

    if ( -not( Test-Path -Path $ScreenConnectPath -PathType Container ) ) {

        New-Item -Path $ScreenConnectPath -ItemType Directory -Force > $null

    }

    $ConnectionParams = @{
        y = 'Guest'
        h = $null
        p = $null
        s = $null
        k = $null
        i = $SessionName
    }

    $InvokeWebRequestSplat = @{
        Uri             = '{0}/Script.ashx' -f $ScreenConnectUri.Trim('/')
        UseBasicParsing = $true
    }
    $ScreenConnectJS = Invoke-WebRequest @InvokeWebRequestSplat

    if ( $ScreenConnectJS.RawContent -match '"h":"(?<h>[^"]+)","p":(?<p>\d+),"k":"(?<k>[^"]+)"' ) {

        $ConnectionParams.h = $Matches.h
        $ConnectionParams.p = $Matches.p
        $ConnectionParams.k = [uri]::EscapeDataString($Matches.k)

    } else {

        Write-Error 'Could not parse connection params!'

    }

    $InvokeRestMethodSplat = @{
        Method      = 'Post'
        Uri         = '{0}/App_Extensions/2d4e908b-8471-431d-b4e0-2390f43bfe67/Service.ashx/CreateGuestSupportSession' -f $ScreenConnectUri.Trim('/')
        Body        = (ConvertTo-Json @($SessionName) -Compress)
        ContentType = 'application/json'
    }
    $ConnectionParams.s = Invoke-RestMethod @InvokeRestMethodSplat

    $ScreenConnectArguments = ( $ConnectionParams.Keys | %{ '{0}={1}' -f $_, $ConnectionParams.$_ } ) -join '&' -replace '^', '"?' -replace '$', '"'

    $ScreenConnectExe = Join-Path $ScreenConnectPath 'ScreenConnect.WindowsClient.exe'

    if ( -not (Test-Path -Path $ScreenConnectExe ) ) {

        $URIs = @(
            '{0}/Bin/ConnectWiseControl.ClientBootstrap.jnlp{1}' -f $ScreenConnectUri.Trim('/'), $ScreenConnectArguments.Trim('"')
            '{0}/Bin/ScreenConnect.Client.exe.jar' -f $ScreenConnectUri.Trim('/')
        )

        $URIs |
            ForEach-Object {@{ Uri = $_ ; OutFile = Join-Path $ScreenConnectPath ( Split-Path -Path ( $_ -replace '\?.*' ) -Leaf ) }} |
            ForEach-Object { Invoke-WebRequest @_ }

        Add-Type -Assembly System.IO.Compression.Filesystem

        [System.IO.Compression.ZipFile]::ExtractToDirectory( "$ScreenConnectPath\ScreenConnect.Client.exe.jar", "$ScreenConnectPath" )

        Expand-JnlpAttachments -Path "$ScreenConnectPath\ConnectWiseControl.ClientBootstrap.jnlp"
    
    }

    if ( Test-Path -Path $ScreenConnectExe ) {

        Start-Process -FilePath $ScreenConnectExe -ArgumentList $ScreenConnectArguments

    } else {

        Write-Error 'Could not locate ScreenConnect.WindowsClient.exe'

    }

}

<#
.SYNOPSIS
    Attempts to install ScreenConnect Host Client on a remote machine.
.PARAMETER Computer
    Computer(s) to attempt to install.
.PARAMETER Credential
    PSCredential object to use for authentication.
.PARAMETER Username
    The plaintext username to use for authentication. Defaults to 'Administrator'.
.PARAMETER Password
    The plaintext password to use for authentication.
.OUTPUTS
    No output.
#>

function Install-HostClient {

    [CmdletBinding(SupportsShouldProcess)]
    param(

        [parameter(Mandatory=$true, Position=1, ValueFromPipeline=$True)]
        [string[]]
        $Computer,

        [Parameter(Mandatory=$true, Position=2)]
        [ValidatePattern('(?# must be an EXE or MSI )\.(exe|msi)$')]
        [string]
        $Installer,

        [pscredential]
        $Credential = [pscredential]::Empty

    )

    begin {

        if ( $PSBoundParameters.Keys -notcontains 'InformationAction' ) {

            $InformationPreference = 'Continue'

        }

        if ( $PSBoundParameters.Keys -notcontains 'ErrorAction' ) {
            
            $ErrorActionPreference = 'Stop'

        }

        Get-Command -Name PsExec.exe > $null

        $CredentialSplat = @{}
        if ( $Credential -ne [pscredential]::Empty ) { $CredentialSplat.Credential = $Credential }

        $InstallerPath =  Resolve-Path $Installer |
            Get-Item

        $ScheduledTaskSplat = switch ( $InstallerPath.Extension ) {

            '.exe' {@{
                TaskName         = 'Deploy ScreenConnect Client (EXE)'
                Command          = Join-Path 'C:\_ScreenConnectDeployment' $InstallerPath.Name
            }}

            '.msi' {@{
                TaskName         = 'Deploy ScreenConnect Client (MSI)'
                Command          = 'C:\Windows\System32\msiexec.exe'
                ArgumentList     = '/i {0} /qn' -f ( Join-Path 'C:\_ScreenConnectDeployment' $InstallerPath.Name )
            }}
        }

    }

    process {

        foreach ( $ComputerItem in $Computer ) {

            if ( -not( Test-HostConnection $ComputerItem ) ) { continue }

            Write-Verbose ( $Messages.MappingTemporaryDriveVerboseMessage -f "\\$ComputerItem\C$" )

            if ( -not( New-PSDrive -Name 'RemoteComputer' -PSProvider FileSystem -Root "\\$ComputerItem\C$" @CredentialSplat ) ) {
            
                Write-Error $Messages.CouldNotMapRemoteDriveError

                continue
            
            }

            if ( -not( Test-Path -Path 'RemoteComputer:\_ScreenConnectDeployment' ) ) {

                Write-Verbose ( $Messages.CreatingDeploymentDirectoryVerboseMessage -f 'C:\_ScreenConnectDeployment' )

                New-Item 'RemoteComputer:\_ScreenConnectDeployment' -ItemType Directory > $null

            }

            Write-Verbose $Messages.PushingInstallerFileVerboseMessage

            Copy-Item -Path $InstallerPath -Destination 'RemoteComputer:\_ScreenConnectDeployment\' -Force

            Write-Information ( $Messages.InvokingScreenConnectInstallerMessage -f $ComputerItem )

            New-ImmediateScheduledTask @ScheduledTaskSplat -ComputerName $ComputerItem @CredentialSplat -Wait

            Write-Verbose ( $Messages.RemovingDeploymentDirectoryVerboseMessage -f 'C:\_ScreenConnectDeployment' )

            Remove-Item 'RemoteComputer:\_ScreenConnectDeployment' -Recurse -Confirm:$false -ErrorAction Continue

            Write-Verbose $Messages.UnMappingTemporaryDriveVerboseMessage

            Remove-PSDrive -Name 'RemoteComputer'

            Write-Information $Messages.InstallationFinishedMessage

        }

    }

}


# cleanup
$ExecutionContext.SessionState.Module.OnRemove = {}

# SIG # Begin signature block
# MIIesgYJKoZIhvcNAQcCoIIeozCCHp8CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUgPlWIgT746+JDoyJGL2PtTaJ
# s1Wgghm9MIIEhDCCA2ygAwIBAgIQQhrylAmEGR9SCkvGJCanSzANBgkqhkiG9w0B
# AQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNV
# BAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRU
# cnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEw
# NDgzOFowgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2Fs
# dCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8G
# A1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNF
# UkZpcnN0LU9iamVjdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6q
# gT+jo2F4qjEAVZURnicPHxzfOpuCaDDASmEd8S8O+r5596Uj71VRloTN2+O5bj4x
# 2AogZ8f02b+U60cEPgLOKqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQ
# w5ujm9M89RKZd7G3CeBo5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbH
# d2pBnqcP1/vulBe3/IW+pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh
# 2JU022R5KP+6LhHC5ehbkkj7RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzT
# bafc8H9vg2XiaquHhnUCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rE
# JlTvA73gJMtUGjAdBgNVHQ4EFgQU2u1kdBScFDyr3ZmpvVsoTYs8ydgwDgYDVR0P
# AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQG
# A1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVz
# dEV4dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGG
# GWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEFBQADggEBAE1C
# L6bBiusHgJBYRoz4GTlmKjxaLG3P1NmHVY15CxKIe0CP1cf4S41VFmOtt1fcOyu9
# 08FPHgOHS0Sb4+JARSbzJkkraoTxVHrUQtr802q7Zn7Knurpu9wHx8OSToM8gUmf
# ktUyCepJLqERcZo20sVOaLbLDhslFq9s3l122B9ysZMmhhfbGN6vRenf+5ivFBjt
# pF72iZRF8FUESt3/J90GSkD2tLzx5A+ZArv9XQ4uKMG+O18aP5cQhLwWPtijnGMd
# ZstcX9o+8w8KCTUi29vAPwD55g1dZ9H9oB4DK9lA977Mh2ZUgKajuPUZYtXSJrGY
# Ju6ay0SnRVqBlRUa9VEwggTmMIIDzqADAgECAhBiXE2QjNVC+6supXM/8VQZMA0G
# CSqGSIb3DQEBBQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNV
# BAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdv
# cmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTEdMBsGA1UEAxMU
# VVROLVVTRVJGaXJzdC1PYmplY3QwHhcNMTEwNDI3MDAwMDAwWhcNMjAwNTMwMTA0
# ODM4WjB6MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEg
# MB4GA1UEAxMXQ09NT0RPIFRpbWUgU3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQCqgvGEqVvYcbXSXSvt9BMgDPmb6dGPdF5u7uspSNjI
# vizrCmFgzL2SjXzddLsKnmhOqnUkcyeuN/MagqVtuMgJRkx+oYPp4gNgpCEQJ0Ca
# WeFtrz6CryFpWW1jzM6x9haaeYOXOh0Mr8l90U7Yw0ahpZiqYM5V1BIR8zsLbMaI
# upUu76BGRTl8rOnjrehXl1/++8IJjf6OmqU/WUb8xy1dhIfwb1gmw/BC/FXeZb5n
# OGOzEbGhJe2pm75I30x3wKoZC7b9So8seVWx/llaWm1VixxD9rFVcimJTUA/vn9J
# AV08m1wI+8ridRUFk50IYv+6Dduq+LW/EDLKcuoIJs0ZAgMBAAGjggFKMIIBRjAf
# BgNVHSMEGDAWgBTa7WR0FJwUPKvdmam9WyhNizzJ2DAdBgNVHQ4EFgQUZCKGtkqJ
# yQQP0ARYkiuzbj0eJ2wwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
# AQAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwEQYDVR0gBAowCDAGBgRVHSAAMEIGA1Ud
# HwQ7MDkwN6A1oDOGMWh0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZp
# cnN0LU9iamVjdC5jcmwwdAYIKwYBBQUHAQEEaDBmMD0GCCsGAQUFBzAChjFodHRw
# Oi8vY3J0LnVzZXJ0cnVzdC5jb20vVVROQWRkVHJ1c3RPYmplY3RfQ0EuY3J0MCUG
# CCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEB
# BQUAA4IBAQARyT3hBeg7ZazJdDEDt9qDOMaSuv3N+Ntjm30ekKSYyNlYaDS18Ash
# U55ZRv1jhd/+R6pw5D9eCJUoXxTx/SKucOS38bC2Vp+xZ7hog16oYNuYOfbcSV4T
# p5BnS+Nu5+vwQ8fQL33/llqnA9abVKAj06XCoI75T9GyBiH+IV0njKCv2bBS7vzI
# 7bec8ckmONalMu1Il5RePeA9NbSwyVivx1j/YnQWkmRB2sqo64sDvcFOrh+RMrjh
# JDt77RRoCYaWKMk7yWwowiVp9UphreAn+FOndRWwUTGw8UH/PlomHmB+4uNqOZrE
# 6u4/5rITP1UDBE0LkHLU6/u8h5BRsjgZMIIE/jCCA+agAwIBAgIQK3PbdGMRTFpb
# MkryMFdySTANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJHQjEbMBkGA1UECBMS
# R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD
# T01PRE8gQ0EgTGltaXRlZDEgMB4GA1UEAxMXQ09NT0RPIFRpbWUgU3RhbXBpbmcg
# Q0EwHhcNMTkwNTAyMDAwMDAwWhcNMjAwNTMwMTA0ODM4WjCBgzELMAkGA1UEBhMC
# R0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9y
# ZDEYMBYGA1UECgwPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDDCJTZWN0aWdvIFNI
# QS0xIFRpbWUgU3RhbXBpbmcgU2lnbmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
# MIIBCgKCAQEAv1I2gjrcdDcNeNV/FlAZZu26GpnRYziaDGayQNungFC/aS42Lwpn
# P0ChSopjNZvQGcx0qhcZkSu1VSAZ+8AaOm3KOZuC8rqVoRrYNMe4iXtwiHBRZmns
# d/7GlHJ6zyWB7TSCmt8IFTcxtG2uHL8Y1Q3P/rXhxPuxR3Hp+u5jkezx7M5ZBBF8
# rgtgU+oq874vAg/QTF0xEy8eaQ+Fm0WWwo0Si2euH69pqwaWgQDfkXyVHOaeGWTf
# dshgRC9J449/YGpFORNEIaW6+5H6QUDtTQK0S3/f4uA9uKrzGthBg49/M+1BBuJ9
# nj9ThI0o2t12xr33jh44zcDLYCQD3npMqwIDAQABo4IBdDCCAXAwHwYDVR0jBBgw
# FoAUZCKGtkqJyQQP0ARYkiuzbj0eJ2wwHQYDVR0OBBYEFK7u2WC6XvUsARL9jo2y
# VXI1Rm/xMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM
# MAoGCCsGAQUFBwMIMEAGA1UdIAQ5MDcwNQYMKwYBBAGyMQECAQMIMCUwIwYIKwYB
# BQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMEIGA1UdHwQ7MDkwN6A1oDOG
# MWh0dHA6Ly9jcmwuc2VjdGlnby5jb20vQ09NT0RPVGltZVN0YW1waW5nQ0FfMi5j
# cmwwcgYIKwYBBQUHAQEEZjBkMD0GCCsGAQUFBzAChjFodHRwOi8vY3J0LnNlY3Rp
# Z28uY29tL0NPTU9ET1RpbWVTdGFtcGluZ0NBXzIuY3J0MCMGCCsGAQUFBzABhhdo
# dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAen+pStKw
# pBwdDZ0tXMauWt2PRR3wnlyQ9l6scP7T2c3kGaQKQ3VgaoOkw5mEIDG61v5MzxP4
# EPdUCX7q3NIuedcHTFS3tcmdsvDyHiQU0JzHyGeqC2K3tPEG5OfkIUsZMpk0uRlh
# dwozkGdswIhKkvWhQwHzrqJvyZW9ljj3g/etfCgf8zjfjiHIcWhTLcuuquIwF4Mi
# KRi14YyJ6274fji7kE+5Xwc0EmuX1eY7kb4AFyFu4m38UnnvgSW6zxPQ+90rzYG2
# V4lO8N3zC0o0yoX/CLmWX+sRE+DhxQOtVxzhXZIGvhvIPD+lIJ9p0GnBxcLJPufF
# cvfqG5bilK+GLjCCBUwwggQ0oAMCAQICEQCV7K1bRdp1yZPPBYrFbG8VMA0GCSqG
# SIb3DQEBCwUAMHwxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNo
# ZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRl
# ZDEkMCIGA1UEAxMbU2VjdGlnbyBSU0EgQ29kZSBTaWduaW5nIENBMB4XDTE5MTAx
# NTAwMDAwMFoXDTIwMTAwNzIzNTk1OVowgZQxCzAJBgNVBAYTAlVTMQ4wDAYDVQQR
# DAU2MDEyMDERMA8GA1UECAwISWxsaW5vaXMxDjAMBgNVBAcMBUVsZ2luMRowGAYD
# VQQJDBExMjg3IEJsYWNraGF3ayBEcjEaMBgGA1UECgwRU2hhbm5vbiBHcmF5YnJv
# b2sxGjAYBgNVBAMMEVNoYW5ub24gR3JheWJyb29rMIIBIjANBgkqhkiG9w0BAQEF
# AAOCAQ8AMIIBCgKCAQEA1A3wiJRalXGleCYOLaKdlD5iZrswpu4ChSnCx8XvkWeL
# R/XBQSvebJXpF99sdVwwUeouEk1i5EA2AIU88DoEw0+1XxC6DAUwYAVXmo3M+dkv
# OwNXHrWwSRqNwmhABHVejGOInKsi1jYa3DPI2dFBL19Trg0ez0oXkMVwbKGDpwt9
# U7WbbjveLcAPnpvR65dk3Jhb9bmCMirCnALjaOOnFzlCUiagx9nDszzw7fYRAlf6
# EJNnicwwBujOmA59q9urwAuEA7/VXTAMpE2wmhVsM4xqscbzAPs7PSVgkOTrZR6a
# 51r1HSCzrULISVZKxF0mD4/6qOElqM/X/nd7q7dmSQIDAQABo4IBrjCCAaowHwYD
# VR0jBBgwFoAUDuE6qFM6MdWKvsG7rWcaA4WtNA4wHQYDVR0OBBYEFJHiTLW7XSJv
# Xn/hpQzh7bxSIUZ2MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBABgNVHSAEOTA3MDUG
# DCsGAQQBsjEBAgEDAjAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29t
# L0NQUzBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnNlY3RpZ28uY29tL1Nl
# Y3RpZ29SU0FDb2RlU2lnbmluZ0NBLmNybDBzBggrBgEFBQcBAQRnMGUwPgYIKwYB
# BQUHMAKGMmh0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1JTQUNvZGVTaWdu
# aW5nQ0EuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAm
# BgNVHREEHzAdgRtzaGFubm9uLmdyYXlicm9va0BnbWFpbC5jb20wDQYJKoZIhvcN
# AQELBQADggEBACNm23H5GuT8THomfaxBDdgN/4g4FgsClLsxhAyRyWxqnE4udxre
# x1Dq3FQtdXoeXFPaaFYVH/zvmEFuh+oz65Ejomo2WPSOVKiF6NbLpxScHW2c1+yO
# NHDqn/TGtx0+RrfUgOFgao/AzuRqxei90CotgUe73cpmG0JPdmV1+hnMAhojoO4g
# bhfdb69y8fCaDzLoTmybz1JOfcinR12TLntNV+Def2CXaNoOV2VNKpauAiIh2BkK
# 7LoabyBtMNQbMNCY33dyNq9V7tvVxdYOlPRoANB3SfATPtKQCrix7T85qrFoRHBC
# SxTfYFHsyGQVno6lmMfQstJ6q+TQJz1gFcUwggX1MIID3aADAgECAhAdokgwb5sm
# GNCC4JZ9M9NqMA0GCSqGSIb3DQEBDAUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRo
# ZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xODExMDIwMDAwMDBaFw0zMDEyMzEyMzU5
# NTlaMHwxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIx
# EDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEkMCIG
# A1UEAxMbU2VjdGlnbyBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEAhiKNMoV6GJ9J8JYvYwgeLdx8nxTP4ya2JWYpQIZU
# RnQxYsUQ7bKHJ6aZy5UwwFb1pHXGqQ5QYqVRkRBq4Etirv3w+Bisp//uLjMg+gwZ
# iahse60Aw2Gh3GllbR9uJ5bXl1GGpvQn5Xxqi5UeW2DVftcWkpwAL2j3l+1qcr44
# O2Pej79uTEFdEiAIWeg5zY/S1s8GtFcFtk6hPldrH5i8xGLWGwuNx2YbSp+dgcRy
# QLXiX+8LRf+jzhemLVWwt7C8VGqdvI1WU8bwunlQSSz3A7n+L2U18iLqLAevRtn5
# RhzcjHxxKPP+p8YU3VWRbooRDd8GJJV9D6ehfDrahjVh0wIDAQABo4IBZDCCAWAw
# HwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFA7hOqhT
# OjHVir7Bu61nGgOFrTQOMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/
# AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMDBggrBgEFBQcDCDARBgNVHSAECjAIMAYG
# BFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29t
# L1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUF
# BwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VT
# RVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2Nz
# cC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQBNY1DtRzRKYaTb3moq
# jJvxAAAeHWJ7Otcywvaz4GOz+2EAiJobbRAHBE++uOqJeCLrD0bs80ZeQEaJEvQL
# d1qcKkE6/Nb06+f3FZUzw6GDKLfeL+SU94Uzgy1KQEi/msJPSrGPJPSzgTfTt2Sw
# piNqWWhSQl//BOvhdGV5CPWpk95rcUCZlrp48bnI4sMIFrGrY1rIFYBtdF5KdX6l
# uMNstc/fSnmHXMdATWM19jDTz7UKDgsEf6BLrrujpdCEAJM+U100pQA1aWy+nyAl
# EA0Z+1CQYb45j3qOTfafDh7+B1ESZoMmGUiVzkrJwX/zOgWb+W/fiH/AI57SHkN6
# RTHBnE2p8FmyWRnoao0pBAJ3fEtLzXC+OrJVWng+vLtvAxAldxU0ivk2zEOS5LpP
# 8WKTKCVXKftRGcehJUBqhFfGsp2xvBwK2nxnfn0u6ShMGH7EezFBcZpLKewLPVdQ
# 0srd/Z4FUeVEeN0B3rF1mA1UJP3wTuPi+IO9crrLPTru8F4XkmhtyGH5pvEqCgul
# ufSe7pgyBYWe6/mDKdPGLH29OncuizdCoGqC7TtKqpQQpOEN+BfFtlp5MxiS47V1
# +KHpjgolHuQe8Z9ahyP/n6RRnvs5gBHN27XEp6iAb+VT1ODjosLSWxr6MiYtaldw
# HDykWC6j81tLB9wyWfOHpxptWDGCBF8wggRbAgEBMIGRMHwxCzAJBgNVBAYTAkdC
# MRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQx
# GDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEkMCIGA1UEAxMbU2VjdGlnbyBSU0Eg
# Q29kZSBTaWduaW5nIENBAhEAleytW0XadcmTzwWKxWxvFTAJBgUrDgMCGgUAoHgw
# GAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGC
# NwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQx
# FgQUP5JCU3+GoUrJogwj/TaUTlcUQTgwDQYJKoZIhvcNAQEBBQAEggEAflB3bj2k
# 2OlU9z+M8AflJ0D3mo0jsghGPZbJq9wHMqy5Rbu3Kqa2eB+6j2BcvQ3iAsGN4Cbu
# T27+sJXZtDmm3M1bsuXRlKsokS2cxWC1vjW7YjR/Eg5JIgLIlYTczl9ytK5Eka1u
# /QFcjHTFhl1nPVLTu74miI8+0QDzLFGceQxemaepIJdy17SrE9mL2IcCTU8pcPBG
# TVC1jtfCIO6W/GNk8UEHXLoK8QPemH17JQs5oJriyzrPN0xLgyI+jvYT/pQTm1xI
# uCJkgnXI4CbOlPSxNi+N2VDnbUq+hTWdgowSVr4Q/yFdwwcFqt0zKrO47bWppq5f
# wWRY7yecR/twjaGCAigwggIkBgkqhkiG9w0BCQYxggIVMIICEQIBATCBjjB6MQsw
# CQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQH
# EwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEgMB4GA1UEAxMX
# Q09NT0RPIFRpbWUgU3RhbXBpbmcgQ0ECECtz23RjEUxaWzJK8jBXckkwCQYFKw4D
# AhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
# DTIwMDEyMzE5MTkzMlowIwYJKoZIhvcNAQkEMRYEFPOUZrK8wNhIjeoSzUTFU06F
# 8566MA0GCSqGSIb3DQEBAQUABIIBAFRLHencXOYoy8wWVBAb5P/XfY2wn9/lzso3
# IqdAeJn0CjoQTLJ3jedR/lZ+iT3/KiBQr7SvCKj8Cw0zI57kqUDH2ei4uTnomc+4
# C+4QvEi5KPI3og3i/Cb+pCG4jNgzgj0R0sFBq8rRw1U5eptrYqr4D/UgxNdvHPFQ
# RlXL8tzp3LOWuYTTPrih7qC95jkuKd0onaMLEa8fs0sx4FafjbkmeO3Y12lDvUr4
# t2rhohMGjkJua74KeSuzRWjaxW7NWEScg1jBsvidnZbebkl9aTvmD57j+O3opqCY
# IFe0PcecfKHyxe2TKkGY/uq1+DeuSMNqhvWuryLNrHkRekuwINI=
# SIG # End signature block