RDWebClientManagement.psm1

#
# Copyright (C) Microsoft. All rights reserved.
#

$defaultInstallationPath = $env:ProgramFiles + '\RemoteDesktopWeb'
$configKeyPath = 'HKLM:\Software\Microsoft\RemoteDesktopWeb'
$installedPathValueName = 'InstalledPath'
$managementToolsVersionName = 'ManagementToolsVersion'
$suppressTelemetryValueName = 'SuppressTelemetry'
$internalDirName = 'Internal'
$clientDirName = 'Clients'
$packageTempDirName = 'Temp'
$configDataDirName = 'Config'
$prodClientVdirName = 'webclient'
$testClientVdirName = 'webclient-test'
$everybodyIdentity = New-Object System.Security.Principal.SecurityIdentifier 'S-1-1-0'
$everybodyReadRule = New-Object System.Security.AccessControl.FileSystemAccessRule $everybodyIdentity,'Read','Allow'
$nobodyReadRule = New-Object System.Security.AccessControl.FileSystemAccessRule $everybodyIdentity,'Read','Deny'
$rdWebPath = '/RDWeb'
$manifestFileName = 'manifest.json'
$clientContentDirName = 'content'

[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration')
[System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography.X509Certificates')

function UpdateIISContext
    (
    $context
    )
{
    $context.IISServerManager = Get-IISServerManager
    if (!$context.IISServerManager)
    {
        Write-Error 'The IIS Server Manager could not be loaded'
        return $FALSE
    }

    $webSites = Get-IISSite
    Foreach ($site in $webSites)
    {
        $rdWebApp = $site.Applications | Where-Object {$_.Path -eq $rdWebPath } | Select-Object -First 1
        $rdWebPagesApp = $site.Applications | Where-Object {$_.Path -eq ($rdWebPath + '/Pages') } | Select-Object -First 1
        if ($rdWebApp)
        {
            $context.RdWebApplication = $rdWebApp
            $context.RdWebSite = $site

            if ($rdWebPagesApp)
            {
                $context.RdWebPagesApplication = $rdWebPagesApp
            }

            Break
        }
    }

    if (!$context.RdWebApplication)
    {
        Write-Error 'RD Web Access does not appear to be installed on the system.'
        return $FALSE
    }
    else
    {
        $context.ProdClientVdir = $context.RdWebApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$prodClientVdirName"} | Select-Object -First 1
        $context.ProdClientConfigVdir = $context.RdWebApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$prodClientVdirName/config"} | Select-Object -First 1
        $context.TestClientVdir = $context.RdWebApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$testClientVdirName"} | Select-Object -First 1
        $context.TestClientConfigVdir = $context.RdWebApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$testClientVdirName/config"} | Select-Object -First 1
    }

    if ($context.RdWebPagesApplication)
    {
        $context.LegacyProdClientVdir = $context.RdWebPagesApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$prodClientVdirName"} | Select-Object -First 1
        $context.LegacyProdClientConfigVdir = $context.RdWebPagesApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$prodClientVdirName/config"} | Select-Object -First 1
        $context.LegacyTestClientVdir = $context.RdWebPagesApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$testClientVdirName"} | Select-Object -First 1
        $context.LegacyTestClientConfigVdir = $context.RdWebPagesApplication.VirtualDirectories | Where-Object {$_.Path -eq "/$testClientVdirName/config"} | Select-Object -First 1
    }

    return $TRUE
}

function EnsureDirectoryExists
    (
        [Parameter(mandatory=$true)]
        $Path,
        $AccessRule
    )
{
    $targetDir = Get-Item $Path -ErrorAction SilentlyContinue
    if (!$targetDir)
    {
        $targetDir = New-Item -Path $Path -ItemType 'directory'
        if (!$targetDir)
        {
            return $FALSE
        }

        if ($AccessRule)
        {
            $acl = Get-Acl $Path
            $acl.SetAccessRule($AccessRule)
            Set-Acl $Path $acl
        }
    }

    return $TRUE
}

function UpdateDeploymentSettingsFile
    (
    $context
    )
{
    $deploymentSettings = @{
        'deploymentType' = 'rdWeb';
        'suppressTelemetry' = $context.DeploymentSettings.SuppressTelemetry
        };

    Set-Content -Path $context.DeploymentSettingsPath ('var DeploymentSettings = ' + (ConvertTo-Json $deploymentSettings))
}

function EnsureInitialized
{
    [OutputType([Boolean])]
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Low')]
    Param(
        $context,
        [string] $installPath
    )

    if ($env:RdWebClientManagementCatalogURL)
    {
        $context.PackageCatalogUrl = $env:RdWebClientManagementCatalogURL
    }
    else
    {
        $context.PackageCatalogUrl = 'https://go.microsoft.com/fwlink/?linkid=2004142&clcid=0x409'
    }

    if (!$installPath)
    {
        $installPath = $defaultInstallationPath
    }
    $internalDirPath = Join-Path -Path $installPath -ChildPath $internalDirName
    $context.ClientPath = Join-Path -Path $internalDirPath -ChildPath $clientDirName
    $context.PackageTempPath = Join-Path -Path $internalDirPath -ChildPath $packageTempDirName
    $context.ConfigDataPath = Join-Path -Path $internalDirPath -ChildPath $configDataDirName
    $context.BrokerCertPath = Join-Path -Path $context.ConfigDataPath -ChildPath 'brokercert.cer'
    $context.DeploymentSettingsPath = Join-Path -Path $context.ConfigDataPath -ChildPath 'deploymentSettings.js'

    $installedDir = Get-Item $installPath -ErrorAction SilentlyContinue
    if (!$installedDir)
    {
        if (!$PSCmdlet.ShouldProcess(
            "RDWebClientManagement in '$installPath'",
            'Install'))
        {
            return $false
        }
    }

    $freshInstall = $FALSE
    if (!$context.ConfigKey)
    {
        $context.ConfigKey = New-Item -Path $configKeyPath
        $context.ConfigKey.SetValue($installedPathValueName, $installPath)
        $context.ConfigKey.SetValue($suppressTelemetryValueName, $false, [Microsoft.Win32.RegistryValueKind]::DWord)
        $context.InstalledPath = $installPath
        $freshInstall = $TRUE
    }

    $lastVersionStr = $context.ConfigKey.GetValue($managementToolsVersionName)
    if (!$lastVersionStr)
    {
        $lastVersionStr = '0.8'
    }
    $lastVersion = [System.Version]$lastVersionStr

    if (!$freshInstall -and ($lastVersion -lt [System.Version]'0.9'))
    {
        Write-Error ("The web client was installed with an older version of RDWebClientManagement. " +
        "Automatic upgrade is not supported. Before using any other RDWebClientManagement commands, " +
        "you must delete your existing installation by running 'Uninstall-RDWebClient'.")
        return $FALSE
    }

    $context.ConfigKey | Set-ItemProperty -Name $managementToolsVersionName -Value $context.Version.ToString()
    
    if (!$installedDir)
    {
        Write-Warning "Initializing RDWebClientManagement in '$installPath'. To uninstall, use Uninstall-RDWebClient."
        if (!(EnsureDirectoryExists -Path $installPath))
        {
            return $FALSE
        }
    }

    if (!(EnsureDirectoryExists -Path $internalDirPath -AccessRule $nobodyReadRule))
    {
        return $FALSE
    }

    if (!(EnsureDirectoryExists -Path $context.ClientPath -AccessRule $everybodyReadRule))
    {
        return $FALSE
    }

    if (!(EnsureDirectoryExists -Path $context.PackageTempPath))
    {
        return $FALSE
    }

    if (!(EnsureDirectoryExists -Path $context.ConfigDataPath -AccessRule $everybodyReadRule))
    {
        return $FALSE
    }
    
    $context.DeploymentSettings = @{
        'SuppressTelemetry' = $false;
        }

    $disableTelemetry = $context.ConfigKey.GetValue($suppressTelemetryValueName)
    $context.DeploymentSettings.SuppressTelemetry = ($null -ne $disableTelemetry) -and ($disableTelemetry -gt 0)

    UpdateDeploymentSettingsFile $context
        
    if (!(UpdateIISContext $context))
    {
        return $FALSE
    }

    return $TRUE
}

function GetModuleContext
{
    $contextProps = @{};
    $contextProps.Version = (Get-Module RDWebClientManagement).Version
    $contextProps.ConfigKey = Get-Item $configKeyPath -ErrorAction SilentlyContinue
    [string]$contextProps.InstalledPath = $null
    [string]$contextProps.ClientPath = $null
    [string]$contextProps.PackageTempPath = $null
    [string]$contextProps.ConfigDataPath = $null
    [string]$contextProps.BrokerCertPath = $null
    [string]$contextProps.DeploymentSettingsPath = $null
    $contextProps.IISServerManager = $null
    $contextProps.RdWebApplication = $null
    $contextProps.RdWebPagesApplication = $null
    $contextProps.RdWebSite = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.ProdClientVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.ProdClientConfigVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.TestClientVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.TestClientConfigVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.LegacyProdClientVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.LegacyProdClientConfigVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.LegacyTestClientVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$contextProps.LegacyTestClientConfigVdir = $null
    $contextProps.PackageCatalogUrl = $null
    $contextProps.DeploymentSettings = @{}


    if ($contextProps.ConfigKey)
    {
        $contextProps.InstalledPath = $contextProps.ConfigKey.GetValue($installedPathValueName)
    }

    $context = New-Object -TypeName PSObject -Property $contextProps

    return $context;
}

function CheckClientSupport
{
    Param(
        [Parameter(mandatory=$true)]
        $context,
        [Parameter(mandatory=$true)]
        $package
    )

    if (!$package.minRDWebClientManagementVersion -or ([System.Version]$package.minRDWebClientManagementVersion -gt $context.Version) )
    {
        Write-Error "The requested package cannot be installed with this version of RDWebClientManagement."
        return $FALSE
    }

    return $TRUE
}

function GetRDWebClientPackagesInternal
{
    Param(
        [Parameter(mandatory=$true)]
        $context
    )

    foreach ($clientDir in Get-ChildItem $context.ClientPath -Directory)
    {
        $clientObject = $null
        $clientObject = Get-ChildItem $clientDir.FullName $manifestFileName -File | Get-Content -ErrorAction Continue | ConvertFrom-Json -ErrorAction Continue
        if ($clientObject -and $clientObject.version)
        {
            $clientObject | Add-Member 'path' $clientDir.FullName
            $clientObject | Add-Member '_baseVersion' ([System.Version]::Parse(($clientObject.version -replace '^([0-9.]*).*$','$1')))

            Write-Output $clientObject
        }
    }
}

<#
.SYNOPSIS
Entirely removes the Remote Desktop web client from the system, including any downloaded client packages and configured settings
#>

function Uninstall-RDWebClient
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]Param()

    $context = GetModuleContext

    if (!$PSCmdlet.ShouldProcess(
        'The Remote Desktop web client, including any downloaded client packages and configured settings.',
        'Uninstall'
    ))
    {
        return
    }

    if ($context.InstalledPath)
    {
        if (UpdateIISContext $context -and $context.RdWebApplication -and $context.IISServerManager)
        {
            $confChanged = $FALSE

            if ($context.TestClientVdir)
            {
                $context.RdWebApplication.VirtualDirectories.Remove($context.TestClientVdir)
                $confChanged = $TRUE
            }

            if ($context.TestClientConfigVdir)
            {
                $context.RdWebApplication.VirtualDirectories.Remove($context.TestClientConfigVdir)
                $confChanged = $TRUE
            }
                        
            if ($context.ProdClientVdir)
            {
                $context.RdWebApplication.VirtualDirectories.Remove($context.ProdClientVdir)
                $confChanged = $TRUE
            }

            if ($context.ProdClientConfigVdir)
            {
                $context.RdWebApplication.VirtualDirectories.Remove($context.ProdClientConfigVdir)
                $confChanged = $TRUE
            }

            #
            # Delete the legacy vdirs that may remain from a previous version of RDWebClientManagement
            #

            if ($context.LegacyProdClientVdir)
            {
                $context.RdWebPagesApplication.VirtualDirectories.Remove($context.LegacyProdClientVdir)
                $confChanged = $TRUE
            }

            if ($context.LegacyProdClientConfigVdir)
            {
                $context.RdWebPagesApplication.VirtualDirectories.Remove($context.LegacyProdClientConfigVdir)
                $confChanged = $TRUE
            }

            if ($context.LegacyTestClientVdir)
            {
                $context.RdWebPagesApplication.VirtualDirectories.Remove($context.LegacyTestClientVdir)
                $confChanged = $TRUE
            }

            if ($context.LegacyTestClientConfigVdir)
            {
                $context.RdWebPagesApplication.VirtualDirectories.Remove($context.LegacyTestClientConfigVdir)
                $confChanged = $TRUE
            }

            if ($confChanged)
            {
                $context.IISServerManager.CommitChanges()
            }
        }
        
        $internalPath = Join-Path $context.InstalledPath $internalDirName

        $acl = Get-Acl $internalPath
        $acl.RemoveAccessRule($nobodyReadRule) | Out-Null
        Set-Acl $internalPath $acl


        Get-Item $context.InstalledPath | Remove-Item -Recurse -Force
    }
    else
    {
        Write-Error 'The Remote Desktop web client is not installed.'
        return
    }

    Remove-Item $configKeyPath -ErrorAction SilentlyContinue
}

<#
.SYNOPSIS
Finds the Remote Desktop web client packages that are available for installation
 
.PARAMETER RequiredVersion
Specifies the exact version of the package to find. By default, Find-RDWebClientPackage only returns the newest available version.
 
.PARAMETER AllVersions
Indicates that Find-RDWebClientPackage should return all available versions of the client. By default, Find-RDWebClientPackage only returns the newest available version.
 
.EXAMPLE
Find-RDWebClientPackage
 
Description
-----------
Finds the client package with the highest version
 
.EXAMPLE
Find-RDWebClientPackage -AllVersions
 
Description
-----------
Finds all available client packages
 
.EXAMPLE
Find-RDWebClientPackage -RequiredVersion 0.7.0
 
Description
-----------
Finds the client package with version number 0.7.0, if it is available
#>

function Find-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter()]
        [string]$RequiredVersion,
        [switch]$AllVersions
    )

    $context = GetModuleContext
    if (!(EnsureInitialized $context))
    {
        return
    }

    $catalog = Invoke-RestMethod $context.PackageCatalogUrl
    if (!$catalog -or !$catalog.packages)
    {
        Write-Error 'The package catalog could not be read or was invalid.'
        return
    }
        
    $results = $catalog.packages
    if ($RequiredVersion)
    {
        $results = $results | Where-Object {$_.version -eq $RequiredVersion}
    }

    foreach ($client in $results)
    {
        if ($client.version)
        {
            $client | Add-Member '_baseVersion' ([System.Version]::Parse(($client.version -replace '^([0-9.]*).*$','$1')))
        }
    }

    $results = $results | Sort-Object packageId,_baseVersion -Descending 
        
    if (!$AllVersions)
    {
        $results = $results | Select-Object -First 1
    }

    $results 
}

<#
.SYNOPSIS
Installs a Remote Desktop web client package locally
 
.PARAMETER RequiredVersion
Specifies the exact version of the package to install. If you do not add this parameter, Install-RDWebClientPackage installs the highest available version of the client.
 
.EXAMPLE
Install-RDWebClientPackage
 
Description
-----------
Installs the client package with the highest version
 
.EXAMPLE
Install-RDWebClientPackage -RequiredVersion 0.7.0
 
Description
-----------
Installs the client package with version number 0.7.0, if it is available
#>

function Install-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter()]
        [string]$RequiredVersion
    )

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess('The Remote Desktop web client package','Install')
    if (!($initialized -and $continue))
    {
        return
    }

    $packageObject = $null
    if ($RequiredVersion)
    {
        $packageObject = Find-RDWebClientPackage -RequiredVersion $RequiredVersion
        if (!$packageObject)
        {
            Write-Error 'The requested Remote Desktop web client package is not available.'
            return
        }
    }
    else
    {
        $packageObject = Find-RDWebClientPackage
        if (!$packageObject)
        {
            Write-Error 'The Remote Desktop web client is not available for installation'
            return
        }
    }
    
    if (GetRDWebClientPackagesInternal $context | Where-Object {$_.version -eq $packageObject.version})
    {
        Write-Warning 'The requested Remote Desktop web client is already installed.'
        return
    }

    if (!(CheckClientSupport $context $packageObject))
    {
        return
    }

    $archiveFileName = Join-Path -Path $context.PackageTempPath -ChildPath ([System.IO.Path]::GetRandomFileName() + '.zip')
    $webclient = New-Object System.Net.WebClient
    $webclient.DownloadFile($packageObject.url, $archiveFileName);

    $newClientPath = Join-Path -Path $context.ClientPath -ChildPath ([System.IO.Path]::GetRandomFileName())
    Expand-Archive $archiveFileName $newClientPath | Out-Null
    Remove-Item $archiveFileName
}

<#
.SYNOPSIS
Returns a list of all Remote Desktop web client packages that have been installed locally
#>

function Get-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    [OutputType([PSCustomObject])]
    Param()

    $context = GetModuleContext
    if (!(EnsureInitialized $context))
    {
        return
    }

    $result = @()
    $clients = GetRDWebClientPackagesInternal($context)
    if ($clients)
    {
        foreach ($client in $clients)
        {
            $client | Add-Member 'publishedAs' @()
            $clientContent = Join-Path $client.path $clientContentDirName
            if ($context.TestClientVdir -and $context.TestClientVdir.PhysicalPath -eq $clientContent)
            {
                $client.publishedAs += 'Test'
            }

            if ($context.ProdClientVdir -and $context.ProdClientVdir.PhysicalPath -eq $clientContent)
            {
                $client.publishedAs += 'Production'
            }

            $result += $client
        }

        $result | Sort-Object packageId,_baseVersion -Descending
    }
    else
    {
        Write-Information 'No clients are installed.'
    }

}

<#
.SYNOPSIS
Uninstalls a Remote Desktop web client package from this system
 
.PARAMETER RequiredVersion
Specifies the exact version of the package to uninstall.
 
.EXAMPLE
Uninstall-RDWebClientPackage -RequiredVersion 0.7.0
 
Description
-----------
Uninstalls the client package with version number 0.7.0, if it is available
#>

function Uninstall-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param(
        [Parameter(mandatory=$true)]
        [string]$RequiredVersion
    )

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess("The Remote Desktop web client package, version '$RequiredVersion'",'Uninstall')
    if (!($initialized -and $continue))
    {
        return
    }

    $package = Get-RDWebClientPackage | Where-Object {$_.version -eq $RequiredVersion} | Select-Object -First 1
    if (!$package)
    {
        Write-Error 'The requested client package is not installed.'
        return
    }

    if ($package.publishedAs.Count -gt 0)
    {
        Write-Error 'The requested client package has been published. You must unpublish it with Unpublish-RDWebClientPackage before you can uninstall it.'
        return
    }

    Get-Item $package.path | Remove-Item -Recurse -Force
}

<#
.SYNOPSIS
Publishes a Remote Desktop web client package through IIS
 
.PARAMETER Type
Indicates the type of deployment to publish as. The type must be either 'Test' or 'Production'.
 
If the type is 'Test', the client will be published in the "RDWeb/webclient-test" vdir.
 
If the type is 'Production', the client will be published in the "RDWeb/webclient" vdir.
 
.PARAMETER RequiredVersion
Specifies the exact version of the package to publish.
 
.PARAMETER Latest
Specifies that the installed web client package with the highest version should be published.
 
.EXAMPLE
Publish-RDWebClientPackage -Latest -Type Test
 
Description
-----------
Publishes the installed web client with the highest version number into the "RDWeb/webclient-test" vdir
 
.EXAMPLE
Publish-RDWebClientPackage -RequiredVersion 0.7.0 -Type Production
 
Description
-----------
Publishes the installed web client with version number 0.7.0 into the "RDWeb/webclient" vdir
#>

function Publish-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param(
        [Parameter(mandatory=$true, Position=0)]
        [ValidateSet('Test', 'Production')]
        [string]$Type,
        [Parameter(mandatory=$true, ParameterSetName = 'RequiredVersion')]
        [string]$RequiredVersion,
        [Parameter(mandatory=$true, ParameterSetName = 'Latest')]
        [switch]$Latest
    )

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess('The Remote Desktop web client package','Publish alongside Remote Desktop Web Access')
    if (!($initialized -and $continue))
    {
        return
    }

    if (!(Get-Item -Path $context.BrokerCertPath -ErrorAction SilentlyContinue))
    {
        Write-Error 'A broker certificate must be installed before publishing a client. Use Import-RDWebClientBrokerCert.'
        return
    }

    $targetClient = $null
    if ($Latest)
    {
        $targetClient = Get-RDWebClientPackage | Sort-Object -Property version -Descending | Select-Object -First 1
    }
    elseif ($RequiredVersion)
    {
        $targetClient = Get-RDWebClientPackage | Where-Object {$_.version -eq $RequiredVersion} | Select-Object -First 1
    }

    $previouspackage = Get-RDWebClientPackage | Where-Object {$Type -in $_.publishedAs} | Select-Object -First 1

    if ($previouspackage)
    {
        if ($previouspackage.version -eq $targetClient.version)
        {
            Write-Warning 'The requested package has already been published for this client type.'
            return
        }

        Write-Warning "Replacing the previously published client package for client type '$Type' (version = $($previouspackage.version) )..."
    }

    if (!$targetClient)
    {
        Write-Error 'The requested client package is not installed.'
        return
    }
        
    if (!(CheckClientSupport $context $targetClient))
    {
        return
    }

    Write-Warning 'Using the Remote Desktop web client with per-device licensing is not supported.'
    
    [Microsoft.Web.Administration.VirtualDirectory]$targetVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$targetConfigVdir = $null
    $targetVdirPath = $null
    $targetConfigVdirPath = $null
    if ($Type -eq 'Test')
    {
        $targetVdir = $context.TestClientVdir
        $targetVdirPath = "/$testClientVdirName"
        $targetConfigVdir = $context.TestClientConfigVdir
        $targetConfigVdirPath = "/$testClientVdirName/config"
    }
    else
    {
        $targetVdir = $context.ProdClientVdir
        $targetVdirPath = "/$prodClientVdirName"
        $targetConfigVdir = $context.ProdClientConfigVdir
        $targetConfigVdirPath = "/$prodClientVdirName/config"
    }

    $clientContentPath = Join-Path $targetClient.path $clientContentDirName
    if ($targetVdir)
    {
        $targetVdir.PhysicalPath = $clientContentPath
    }
    else
    {
        $targetVdir = $context.RdWebApplication.VirtualDirectories.Add($targetVdirPath, $clientContentPath) | Out-Null
    }

    if ($targetConfigVdir)
    {
        $targetConfigVdir.PhysicalPath = $context.ConfigDataPath
    }
    else
    {
        $targetConfigVdir = $context.RdWebApplication.VirtualDirectories.Add($targetConfigVdirPath, $context.ConfigDataPath) | Out-Null
    }
    
    $context.IISServerManager.CommitChanges()
    UpdateIISContext($context) | Out-Null
}

<#
.SYNOPSIS
Unpublishes a previously-published Remote Desktop web client package
 
.PARAMETER Type
Indicates the type of deployment to unpublish. The type must be either 'Test' or 'Production'.
 
If the type is 'Test', the client in the "RDWeb/webclient-test" vdir will be unpublished.
 
If the type is 'Production', the client in the "RDWeb/webclient" vdir will be unpublished.
 
.EXAMPLE
Unpublish-RDWebClientPackage -Type Production
 
Description
-----------
Unpublishes the currently-published production client
#>

function Unpublish-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param(
        [Parameter(mandatory=$true)]
        [ValidateSet('Test', 'Production')]
        [string]$Type
    )

    $isTest = $Type -eq 'Test'

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    if ($isTest)
    {
        $vdirName = $testClientVdirName
    }
    else
    {
        $vdirName = $prodClientVdirName
    }
    $continue = $PSCmdlet.ShouldProcess("The Remote Desktop web client in '$vdirName'",'Unpublish')
    if (!($initialized -and $continue))
    {
        return
    }

    [Microsoft.Web.Administration.VirtualDirectory]$targetVdir = $null
    [Microsoft.Web.Administration.VirtualDirectory]$targetConfigVdir = $null
    if ($isTest)
    {
        $targetVdir = $context.TestClientVdir
        $targetConfigVdir =$context.TestClientConfigVdir
    }
    else
    {
        $targetVdir = $context.ProdClientVdir
        $targetConfigVdir =$context.ProdClientConfigVdir
    }

    if ($targetVdir)
    {
        $context.RdWebApplication.VirtualDirectories.Remove($targetVdir)

        if ($targetConfigVdir)
        {
            $context.RdWebApplication.VirtualDirectories.Remove($targetConfigVdir)
        }

        $context.IISServerManager.CommitChanges()
    }
    else
    {
        Write-Error 'The requested client type is not published.'
    }
}

<#
.SYNOPSIS
Imports a broker certificate file for use with the Remote Desktop web client
 
.PARAMETER Path
File path for the certificate file to import
 
.PARAMETER Password
The password to use in opening the certificate file.
 
.PARAMETER PromptForPassword
Indicates that Import-RDWebClientBrokerCert should interactively prompt the user for
a password to use in opening the certificate file.
 
.EXAMPLE
Import-RDWebClientBrokerCert brokercert.cer
 
Description
-----------
Imports a certificate from a .cer file
 
.EXAMPLE
Import-RDWebClientBrokerCert brokercert.pfx -PromptForPassword
 
Description
-----------
Imports a certificate from a password-protected .pfx file. An interactive prompt will be used to
request the password that is needed to open the file.
#>

function Import-RDWebClientBrokerCert
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(mandatory=$true, Position=0)]
        [string] $Path,
        [Parameter(mandatory=$false, ParameterSetName = 'NoPrompt')]
        [SecureString] $Password,
        [Parameter(mandatory=$true, ParameterSetName = 'WithPrompt')]
        [switch] $PromptForPassword
    )

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess('The certificate that identifies the Remote Desktop Connection Broker',
        'Import')
    if (!($initialized -and $continue))
    {
        return
    }
    
    $certificateCreateArgs = @($Path)

    $password = $Password
    if ($PromptForPassword)
    {
        $password = Read-Host -Prompt 'Password' -AsSecureString
    }

    if ($password)
    {
        $certificateCreateArgs += $password
    }

    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificateCreateArgs
    if ($cert)
    {
        [System.IO.File]::WriteAllBytes($context.BrokerCertPath, $cert.RawData)
    }
}

<#
.SYNOPSIS
Returns information about the broker certificate that is being used by the Remote Desktop web client
#>

function Get-RDWebClientBrokerCert
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    Param()

    $context = GetModuleContext
    if (!(EnsureInitialized $context))
    {
        return
    }

    if (Get-Item $context.BrokerCertPath -ErrorAction SilentlyContinue)
    {
        New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 @($context.BrokerCertPath)
    }
    else
    {
        Write-Information 'A broker certificate is not installed.'
    }
}

<#
.SYNOPSIS
Removes the broker certificate that is being used by the Remote Desktop web client
#>

function Remove-RDWebClientBrokerCert
{
    [CmdletBinding(SupportsShouldProcess=$true)]Param()

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess('The certificate that identifies the Remote Desktop Connection Broker',
        'Remove')
    if (!($initialized -and $continue))
    {
        return
    }

    if (Get-Item $context.BrokerCertPath -ErrorAction SilentlyContinue)
    {
        Remove-Item $context.BrokerCertPath
    }
    else
    {
        Write-Information 'A broker certificate is not installed.'
    }
}

<#
.SYNOPSIS
Sets a deployment setting for the Remote Desktop web client
.PARAMETER Name
Specifies the name of the setting
.PARAMETER Value
The new value for the setting
.EXAMPLE
Set-RDWebClientDeploymentSetting SuppressTelemetry $true
 
Description
-----------
Disables telemetry gathering in the client
#>

function Set-RDWebClientDeploymentSetting
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(mandatory=$true)]
        [string] $Name,
        [Parameter(mandatory=$true)]
        $Value
    )

    $context = GetModuleContext
    if (!(EnsureInitialized $context))
    {
        return
    }

    switch ($Name)
    {
        'SuppressTelemetry' {
            if ($Value -is [Boolean])
            {
                New-ItemProperty -Path $configKeyPath -Name $suppressTelemetryValueName -Value $Value -PropertyType DWORD -Force | Out-Null
                $context.DeploymentSettings[$Name] = $Value
                UpdateDeploymentSettingsFile $context
            }
            else
            {
                Write-Error 'This setting requires a value of type [Boolean].'
            }
        }
        default { Write-Error "Setting '$Name' does not exist." }
    }
}

<#
.SYNOPSIS
Gets the deployment settings for the Remote Desktop web client
.PARAMETER Name
Specifies the name of the setting to retrieve
.EXAMPLE
Get-RDWebClientDeploymentSetting
 
Description
-----------
Gets all deployment settings
 
.EXAMPLE
Get-RDWebClientDeploymentSetting SuppressTelemetry
 
Description
-----------
Gets the SuppressTelemetry deployment setting
#>

function Get-RDWebClientDeploymentSetting
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    [OutputType([System.Collections.DictionaryEntry])]
    Param(
        [string] $Name
    )

    $context = GetModuleContext
    if (!(EnsureInitialized $context))
    {
        return
    }

    if ($Name)
    {
        if ($context.DeploymentSettings.ContainsKey($Name))
        {
            @{$Name = $context.DeploymentSettings[$Name]}.GetEnumerator()
        }
        else
        {
            Write-Error "Setting '$Name' does not exist."
            return
        }
    }
    else
    {
        $context.DeploymentSettings.GetEnumerator()
    }
}

# SIG # Begin signature block
# MIIdeAYJKoZIhvcNAQcCoIIdaTCCHWUCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQURjRJXeTj5KcBMrAL4VmgAQUi
# MWKgghhUMIIEwjCCA6qgAwIBAgITMwAAAMRudtBNPf6pZQAAAAAAxDANBgkqhkiG
# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw
# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTYwOTA3MTc1ODUy
# WhcNMTgwOTA3MTc1ODUyWjCBsjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEMMAoGA1UECxMDQU9DMScwJQYDVQQLEx5uQ2lwaGVyIERTRSBFU046
# MjEzNy0zN0EwLTRBQUExJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl
# cnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoA5rFUpl2jKM9
# /L26GuVj6Beo87YdPTwuOL0C+QObtrYvih7LDNDAeWLw+wYlSkAmfmaSXFpiRHM1
# dBzq+VcuF8YGmZm/LKWIAB3VTj6df05JH8kgtp4gN2myPTR+rkwoMoQ3muR7zb1n
# vNiLsEpgJ2EuwX5M/71uYrK6DHAPbbD3ryFizZAfqYcGUWuDhEE6ZV+onexUulZ6
# DK6IoLjtQvUbH1ZMEWvNVTliPYOgNYLTIcJ5mYphnUMABoKdvGDcVpSmGn6sLKGg
# iFC82nun9h7koj7+ZpSHElsLwhWQiGVWCRVk8ZMbec+qhu+/9HwzdVJYb4HObmwN
# Daqpqe17AgMBAAGjggEJMIIBBTAdBgNVHQ4EFgQUiAUj6xG9EI77i5amFSZrXv1V
# 3lAwHwYDVR0jBBgwFoAUIzT42VJGcArtQPt2+7MrsMM1sw8wVAYDVR0fBE0wSzBJ
# oEegRYZDaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljcm9zb2Z0VGltZVN0YW1wUENBLmNybDBYBggrBgEFBQcBAQRMMEowSAYIKwYB
# BQUHMAKGPGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z
# b2Z0VGltZVN0YW1wUENBLmNydDATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG
# 9w0BAQUFAAOCAQEAcDh+kjmXvCnoEO5AcUWfp4/4fWCqiBQL8uUFq6cuBuYp8ML4
# UyHSLKNPOoJmzzy1OT3GFGYrmprgO6c2d1tzuSaN3HeFGENXDbn7N2RBvJtSl0Uk
# ahSyak4TsRUPk/WwMQ0GOGNbxjolrOR41LVsSmHVnn8IWDOCWBj1c+1jkPkzG51j
# CiAnWzHU1Q25A/0txrhLYjNtI4P3f0T0vv65X7rZAIz3ecQS/EglmADfQk/zrLgK
# qJdxZKy3tXS7+35zIrDegdAH2G7d3jvCNTjatrV7cxKH+ZX9oEsFl10uh/U83KA2
# QiQJQMtbjGSzQV2xRpcNf2GpHBRPW0sK4yL3wzCCBgEwggPpoAMCAQICEzMAAADE
# 6Yn4eoFQ6f8AAAAAAMQwDQYJKoZIhvcNAQELBQAwfjELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2ln
# bmluZyBQQ0EgMjAxMTAeFw0xNzA4MTEyMDIwMjRaFw0xODA4MTEyMDIwMjRaMHQx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xHjAcBgNVBAMTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
# ggEBAIiKuCTDB4+agHkV/CZg/HKILPr0o5eIlka3o8tfiS86My4ekXj6fKkfggG1
# essavAPKRuvFmff7BB3yhQr/Im6h8mc9xScY5Sgf9QSUQWPs47oVjO0TmjXeOHBU
# bzvsrUUJMEnBvo8wmQzLdsn3c5UWd9GLu5THCIUg7R6oNfFxwuB0AEuK0tyR69Z4
# /o36rWCIPb25H65il7/FhLGQrtavK9NU+zXazXGS5h7/7HFry38IdnTgEFFI1PEA
# yEhMowc15VkN/XycyOZa44X11poPH46m5IQXwdbKnx0Bx/1IpxOSM5chSDL4wiSi
# ALK+U8qDbilbge84boDzu+wTC+sCAwEAAaOCAYAwggF8MB8GA1UdJQQYMBYGCisG
# AQQBgjdMCAEGCCsGAQUFBwMDMB0GA1UdDgQWBBTL1mKEz2A56v9nwlzSyLurt8MT
# mDBSBgNVHREESzBJpEcwRTENMAsGA1UECxMETU9QUjE0MDIGA1UEBRMrMjMwMDEy
# K2M4MDRiNWVhLTQ5YjQtNDIzOC04MzYyLWQ4NTFmYTIyNTRmYzAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AAYWH9tXwlDII0+iUXjX7fj9zb3VwPH5G1btU8hpRwXVxMvs4vyZW5VfETgowAVF
# E+CaeYi8Zqvbu+sCVSO3PSN4QW2u+PEAWpSZihzMCZXQmhxEMKmlFse6R1v1KzSL
# n49YN8NOHK8iyhDN2IIQqTXwriLIjySmgYvfJxzkZh2JPi7/VwNNwW6DoDLrtLMv
# UFZdBrEVjMgdY7dzDOPWeiYPKpZFpzKDPpY+V0l3I4n+sRDHiuUIFVHFK1oxWzlq
# lqikiGuWKG/xxK7qvUUXzGJOgbVUGkeOmKVtwG4nxvgnH8jtIKkLsfHOC5qU4mqd
# aYOhNtdtIP6F1f/DuJc2Cf49FMGYFKnAhszvgsGrVSRDGLVIhXiG0PnSnT8Z2RSJ
# 542faCSIaDupx4BOJucIIUxj/ZyTFU0ztVZgT9dKuTiO/y7dsV+kQ2vJeM+xu2uP
# g2yHcqrqpfuf3RrWOfxkyW0+COV8g7GtvKO6e8+WVqR6WMsSR2LSIe/8PMQxC/cv
# PmSlN29gUD+3RJBPoAuLvn5Y9sdnh2HbnpjEyIzLb0fhwC6U7bH2sDBt7GpJqOmW
# dsi9CMT+O/WuczcGslbPGdS79ZTKhxzygGoBT7YbgXOz01siPzpYGN+I7mfESacv
# 3CWLPV7Q7DREkR28kQx2gj7vxNgtoQQCjkj5790CzwOiMIIGBzCCA++gAwIBAgIK
# YRZoNAAAAAAAHDANBgkqhkiG9w0BAQUFADBfMRMwEQYKCZImiZPyLGQBGRYDY29t
# MRkwFwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQDEyRNaWNyb3NvZnQg
# Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcwNDAzMTI1MzA5WhcNMjEw
# NDAzMTMwMzA5WjB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSEwHwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQCfoWyx39tIkip8ay4Z4b3i48WZUSNQrc7dGE4k
# D+7Rp9FMrXQwIBHrB9VUlRVJlBtCkq6YXDAm2gBr6Hu97IkHD/cOBJjwicwfyzMk
# h53y9GccLPx754gd6udOo6HBI1PKjfpFzwnQXq/QsEIEovmmbJNn1yjcRlOwhtDl
# KEYuJ6yGT1VSDOQDLPtqkJAwbofzWTCd+n7Wl7PoIZd++NIT8wi3U21StEWQn0gA
# SkdmEScpZqiX5NMGgUqi+YSnEUcUCYKfhO1VeP4Bmh1QCIUAEDBG7bfeI0a7xC1U
# n68eeEExd8yb3zuDk6FhArUdDbH895uyAc4iS1T/+QXDwiALAgMBAAGjggGrMIIB
# pzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQjNPjZUkZwCu1A+3b7syuwwzWz
# DzALBgNVHQ8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwgZgGA1UdIwSBkDCBjYAU
# DqyCYEBWJ5flJRP8KuEKU5VZ5KShY6RhMF8xEzARBgoJkiaJk/IsZAEZFgNjb20x
# GTAXBgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eYIQea0WoUqgpa1Mc1j0BxMuZTBQBgNV
# HR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9w
# cm9kdWN0cy9taWNyb3NvZnRyb290Y2VydC5jcmwwVAYIKwYBBQUHAQEESDBGMEQG
# CCsGAQUFBzAChjhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01p
# Y3Jvc29mdFJvb3RDZXJ0LmNydDATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG
# 9w0BAQUFAAOCAgEAEJeKw1wDRDbd6bStd9vOeVFNAbEudHFbbQwTq86+e4+4LtQS
# ooxtYrhXAstOIBNQmd16QOJXu69YmhzhHQGGrLt48ovQ7DsB7uK+jwoFyI1I4vBT
# Fd1Pq5Lk541q1YDB5pTyBi+FA+mRKiQicPv2/OR4mS4N9wficLwYTp2Oawpylbih
# OZxnLcVRDupiXD8WmIsgP+IHGjL5zDFKdjE9K3ILyOpwPf+FChPfwgphjvDXuBfr
# Tot/xTUrXqO/67x9C0J71FNyIe4wyrt4ZVxbARcKFA7S2hSY9Ty5ZlizLS/n+YWG
# zFFW6J1wlGysOUzU9nm/qhh6YinvopspNAZ3GmLJPR5tH4LwC8csu89Ds+X57H21
# 46SodDW4TsVxIxImdgs8UoxxWkZDFLyzs7BNZ8ifQv+AeSGAnhUwZuhCEl4ayJ4i
# IdBD6Svpu/RIzCzU2DKATCYqSCRfWupW76bemZ3KOm+9gSd0BhHudiG/m4LBJ1S2
# sWo9iaF2YbRuoROmv6pH8BJv/YoybLL+31HIjCPJZr2dHYcSZAI9La9Zj7jkIeW1
# sMpjtHhUBdRBLlCslLCleKuzoJZ1GtmShxN1Ii8yqAhuoFuMJb+g74TKIdbrHk/J
# mu5J4PcBZW+JC33Iacjmbuqnl84xKf8OxVtc2E0bodj6L54/LlUWa8kTo/0wggd6
# MIIFYqADAgECAgphDpDSAAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg
# Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDla
# Fw0yNjA3MDgyMTA5MDlaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS6
# 8rZYIZ9CGypr6VpQqrgGOBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15
# ZId+lGAkbK+eSZzpaF7S35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+er
# CFDPs0S3XdjELgN1q2jzy23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVc
# eaVJKecNvqATd76UPe/74ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGM
# XeiJT4Qa8qEvWeSQOy2uM1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/
# U7qcD60ZI4TL9LoDho33X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwj
# p6lm7GEfauEoSZ1fiOIlXdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwC
# gl/bwBWzvRvUVUvnOaEP6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1J
# MKerjt/sW5+v/N2wZuLBl4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3co
# KPHtbcMojyyPQDdPweGFRInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfe
# nk70lrC8RqBsmNLg1oiMCwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAw
# HQYDVR0OBBYEFEhuZOVQBdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoA
# UwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQY
# MBaAFHItOgIxkEO5FAVO4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1
# dDIwMTFfMjAxMV8wM18yMi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAC
# hkJodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1
# dDIwMTFfMjAxMV8wM18yMi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4D
# MIGDMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2RvY3MvcHJpbWFyeWNwcy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBs
# AF8AcABvAGwAaQBjAHkAXwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcN
# AQELBQADggIBAGfyhqWY4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjD
# ctFtg/6+P+gKyju/R6mj82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw
# /WvjPgcuKZvmPRul1LUdd5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkF
# DJvtaPpoLpWgKj8qa1hJYx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3z
# Dq+ZKJeYTQ49C/IIidYfwzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEn
# Gn+x9Cf43iw6IGmYslmJaG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1F
# p3blQCplo8NdUmKGwx1jNpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0Qax
# dR8UvmFhtfDcxhsEvt9Bxw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AAp
# xbGbpT9Fdx41xtKiop96eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//W
# syNodeav+vyL6wuA6mk7r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqx
# P/uozKRdwaGIm1dxVk5IRcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIEjjCC
# BIoCAQEwgZUwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO
# BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAMTp
# ifh6gVDp/wAAAAAAxDAJBgUrDgMCGgUAoIGiMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJ
# BDEWBBScZEE935Vir2tbN/eFZhaqGDxOJTBCBgorBgEEAYI3AgEMMTQwMqAUgBIA
# TQBpAGMAcgBvAHMAbwBmAHShGoAYaHR0cDovL3d3dy5taWNyb3NvZnQuY29tMA0G
# CSqGSIb3DQEBAQUABIIBABd4Or04W9N4aHynC9cSpVG6wlLq4gCgLsnhE0EILDTH
# 2aysu/Aml/NXIAJpa10WUMBxg9w94isrqQ/mfECG0Ol3igTqtbXowfN1zaxmkmQ8
# pyut8O3zYc9fC3IAN5RdL5oBXoyPTSJsiBjNXHU1qMB/a7JiI07WcSkE988IJP9p
# FDc/cWtXwIJui7U3P4VDLrDBW8sZJNO9F8ReF1mEUqVlMrrKrO5BmVHmRJ0xu7mo
# Tmk5oqae2DO2p5ZGLD6zIYqXiHnLTcp1KnGS+kFROxXgIMJcg0h2zCt7o9/NeiMI
# PEe9BMusobloiYQLyJdAetaNPJ6fVNQPSPZqyemtnD6hggIoMIICJAYJKoZIhvcN
# AQkGMYICFTCCAhECAQEwgY4wdzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBAhMzAAAA
# xG520E09/qllAAAAAADEMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZI
# hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xODA3MDMxNzEwNTBaMCMGCSqGSIb3DQEJ
# BDEWBBSEMJ/i2QLxTjlEOxSY/fON1bnu3TANBgkqhkiG9w0BAQUFAASCAQAEyPOg
# tZw7jkTjSAXGWAtruibe8ly/sDIqRslfIBPJLj5SsiGm8T5suEJiKp/i7exz4LtO
# rhKNMf7IQm3qz0Dpx6QPOqdVKwDbd84d8AT7Xdrs4+bseMFcVnCJAmsb/6fILIow
# ER+L4DSRMiwxANdljiQ5gbzxYPkvE/61lDSKOJNQCsZGstvSG88s5Fi5XUDOE2BK
# tgfwWG8+TV8ScKsNz0uAiyh0rQf/ZjeC6GtOaMWSgmb2WvU5rCgyqqDx/9F1d0RT
# eRWFk6cc/MwRhr/9QPPU0lo06oGD2kDjETVO6z116Az1d7wbvS8J0qw2xu5vqYy5
# y/Y0pAlPqqHhREV1
# SIG # End signature block