RDWebClientManagement.psm1

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

$defaultInstallationPath = $env:ProgramFiles + '\RemoteDesktopWeb'
$configKeyPath = 'HKLM:\Software\Microsoft\RemoteDesktopWeb'
$installedPathValueName = 'InstalledPath'
$managementToolsVersionName = 'ManagementToolsVersion'
$suppressTelemetryValueName = 'SuppressTelemetry'
$launchResourceInBrowserValueName = 'LaunchResourceInBrowser'
$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('System.Security.Cryptography.X509Certificates')

function GetIISServerManager
{
    $iisServer = $null
    try
    {
        $iisServer = Get-IISServerManager
    }
    catch
    {
        Write-Error "The operation cannot be completed. RD Web Access does not appear to be installed on the system."
        return $null
    }

    return $iisServer
}

function UpdateIISContext
{
    [OutputType([Boolean])]
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Low')]
    Param(
        [parameter(Mandatory=$true)]
        $iisContext
    )

    $iisContext.IISServerManager = GetIISServerManager
    if (!$iisContext.IISServerManager)
    {
        return $false
    }

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

            Break
        }        
    }

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

    $disableLaunchResourcesInBrowser = $context.ConfigKey.GetValue($launchResourceInBrowserValueName)
    if($disableLaunchResourcesInBrowser -ne $null) {
        $deploymentSettings.Add('launchResourceInBrowser', ($disableLaunchResourcesInBrowser -gt 0));
    }

    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=2005418'
    }

    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]'1.0'))
    {
        Write-Error ("The web client was installed using an older version of RDWebClientManagement and " +
        "must first be removed before deploying the new version. Using the old version of " +
        "RDWebClientManagement, uninstall the web client by running 'Uninstall-RDWebClient'. " +
        "Then reinstall the web client using this new module. For more information, " +
        "visit https://go.microsoft.com/fwlink/?linkid=870181 .")
        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)

    $disableLaunchResourcesInBrowser = $context.ConfigKey.GetValue($launchResourceInBrowserValueName)
    if($disableLaunchResourcesInBrowser -ne $null) {
        $context.DeploymentSettings.Add('LaunchResourceInBrowser', ($disableLaunchResourcesInBrowser -gt 0));
    }

    UpdateDeploymentSettingsFile $context
        
    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.PackageCatalogUrl = $null
    $contextProps.DeploymentSettings = @{}


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

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

    return $context;
}

function GetModuleIISContext
{
    $contextProps = @{};

    $contextProps.IISServerManager = $null
    $contextProps.RdWebApplication = $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

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

    return $iisContext;
}

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 a version of RDWebClientManagement lower than '$($package.minRDWebClientManagementVersion)'."
        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 installed 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 installed client packages and configured settings.',
        'Uninstall'
    ))
    {
        return
    }

    if ($context.InstalledPath)
    {
        $iis = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration')
        if ($iis)
        {
            $iisContext = GetModuleIISContext
            if (!(UpdateIISContext $iisContext))
            {
                Write-Warning "Installed web client packages not found. Continuing with rest of uninstallation."
                $iisContext = $null
            }
        }
        else 
        {
            $iisContext = $null
        }

        if ($iisContext -and $iisContext.RdWebApplication -and $iisContext.IISServerManager)
        {
            $confChanged = $FALSE

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

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

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

            if ($confChanged)
            {
                $iisContext.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.
 
.PARAMETER Source
File path for the RD web client package to install. The Save-RDWebClientPackage command can be used to download the RD web client package.
 
.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
 
.EXAMPLE
Install-RDWebClientPackage -Source c:\rdwebclientpackages\1.0.0.zip
 
Description
-----------
Installs the client package located at c:\rdwebclientpackages\1.0.0.zip
 
#>

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

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

    $iis = GetIISServerManager
    if (!$iis)
    {
        return
    }

    if ($Source)
    {
        $newClientPath = Join-Path -Path $context.ClientPath -ChildPath ([System.IO.Path]::GetRandomFileName())
        Expand-Archive $Source $newClientPath | Out-Null

        $newClientObject = Get-ChildItem $newClientPath $manifestFileName -File | Get-Content -ErrorAction Continue | ConvertFrom-Json -ErrorAction Continue
        if ($newClientObject -and $newClientObject.version)
        {
            if ($RequiredVersion -and $RequiredVersion -ne $newClientObject.version)
            {
                Write-Warning 'The requested Remote Desktop web client version was not found.'
                Remove-Item $newClientPath -Recurse -Force
                return
            }

            $clients = GetRDWebClientPackagesInternal($context)
            foreach ($client in $clients)
            {
                if ($client.version -eq $newClientObject.version -and $client.path -ne $newClientPath)
                {
                    Write-Warning 'The requested Remote Desktop web client is already installed.'
                    Remove-Item $newClientPath -Recurse -Force
                    return
                }
            }
        }
        else
        {
            Write-Warning 'The source Remote Desktop web client package is not valid.'
            Remove-Item $newClientPath -Recurse -Force
            return
        }
    }
    else
    {
        $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
Saves a Remote Desktop web client package locally for installing later on same or different machine.
 
.PARAMETER RequiredVersion
Specifies the exact version of the package to install. If you do not add this parameter, Save-RDWebClientPackage saves the highest available version of the client.
 
.PARAMETER Path
Folder path for saving the web client package
 
.EXAMPLE
 
Save-RDWebClientPackage c:\rdwebclientpackages
 
Description
-----------
Saves the client package with the highest version to c:\rdwebclientpackages
 
.EXAMPLE
Save-RDWebClientPackage c:\rdwebclientpackages -RequiredVersion 1.0.0
 
Description
-----------
Saves the client package with version number 1.0.0, if it is available, to c:\rdwebclientpackages directory
#>

function Save-RDWebClientPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(mandatory=$true, Position=0)]
        [string] $Path,        
        [Parameter(mandatory=$false)]
        [string]$RequiredVersion
    )

    $context = GetModuleContext
    $initialized = !!(EnsureInitialized $context)
    $continue = $PSCmdlet.ShouldProcess('The Remote Desktop web client package','Save')
    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'
            return
        }
    }
    
    $archiveFileName = Join-Path -Path $Path -ChildPath ('rdwebclient-' + $packageObject.version + '.zip')
    $webclient = New-Object System.Net.WebClient
    $webclient.DownloadFile($packageObject.url, $archiveFileName);

    Write-Information "RD web client package saved '$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)
    {
        $iis = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration')
        if (!$iis)
        {
            Write-Error "IIS must be installed to perform this operation"
            return
        }

        $iisContext = GetModuleIISContext
        if (!(UpdateIISContext $iisContext))
        {            
            return
        }
        
        foreach ($client in $clients)
        {
            $client | Add-Member 'publishedAs' @()
            $clientContent = Join-Path $client.path $clientContentDirName
            if ($iisContext.TestClientVdir -and $iisContext.TestClientVdir.PhysicalPath -eq $clientContent)
            {
                $client.publishedAs += 'Test'
            }

            if ($iisContext.ProdClientVdir -and $iisContext.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
    }

    $iis = GetIISServerManager
    if (!$iis)
    {
        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
    }
     
    $iis = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration')
    if (!$iis)
    {
        Write-Error "IIS must be installed to perform this operation"
        return
    }

    $iisContext = GetModuleIISContext
    if (!(UpdateIISContext $iisContext))
    {
        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 _baseVersion -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) ) with (version = $($targetClient.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
    $targetVdirName = $null
    $targetConfigVdirPath = $null
    if ($Type -eq 'Test')
    {
        $targetVdir = $iisContext.TestClientVdir
        $targetVdirName = $testClientVdirName
        $targetConfigVdir = $iisContext.TestClientConfigVdir
        $targetConfigVdirPath = "/$testClientVdirName/config"
    }
    else
    {
        $targetVdir = $iisContext.ProdClientVdir
        $targetVdirName = $prodClientVdirName
        $targetConfigVdir = $iisContext.ProdClientConfigVdir
        $targetConfigVdirPath = "/$prodClientVdirName/config"
    }

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

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

    Set-WebConfigurationProperty -PSPath "IIS:\Sites\$($iisContext.RdWebSite)\RDWeb" -Location $targetVdirName -Filter 'system.webServer/httpRedirect' -Name '.' -Value @{enabled = 'false'}


    $mimeTypeConfig = Get-WebConfigurationProperty -PSPath "IIS:\Sites\$($iisContext.RdWebSite)\RDWeb" -Filter "system.webServer/staticContent" -Name "."
    if(!($mimeTypeConfig.Collection | Where-Object { $_.fileExtension -eq ".wasm"}  | select fileExtension))
    {
        Add-WebConfigurationProperty -PSPath "IIS:\Sites\$($iisContext.RdWebSite)\RDWeb" -Filter "system.webServer/staticContent" -Name "." -Value @{ fileExtension='.wasm'; mimeType='application/wasm' }
    }

    
    $iisContext.IISServerManager.CommitChanges()
    UpdateIISContext($iisContext) | 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)
    
    $iis = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration')
    if (!$iis)
    {
        Write-Error "IIS must be installed to perform this operation"
        return
    }

    $iisContext = GetModuleIISContext
    if (!(UpdateIISContext $iisContext))
    {
        return
    }   

    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 = $iisContext.TestClientVdir
        $targetConfigVdir =$iisContext.TestClientConfigVdir
    }
    else
    {
        $targetVdir = $iisContext.ProdClientVdir
        $targetConfigVdir =$iisContext.ProdClientConfigVdir
    }

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

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

        Remove-WebConfigurationLocation -PSPath "IIS:\Sites\$($iisContext.RdWebSite)\RDWeb" -Name $vdirName | Out-Null

        $iisContext.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
    }
    
    $iis = GetIISServerManager
    if (!$iis)
    {
        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
    }

    $iis = GetIISServerManager
    if (!$iis)
    {
        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
    }

    $iis = GetIISServerManager
    if (!$iis)
    {
        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
    }

    $iis = GetIISServerManager
    if (!$iis)
    {
        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].'
            }
        }
        'LaunchResourceInBrowser' {
            if ($Value -is [Boolean])
            {
                New-ItemProperty -Path $configKeyPath -Name $launchResourceInBrowserValueName -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
Resets a deployment setting for the Remote Desktop web client to its default value
.PARAMETER Name
Specifies the name of the setting
.EXAMPLE
Reset-RDWebClientDeploymentSetting LaunchResourceInBrowser
 
Description
-----------
Allows the user to choose how to launch resources enumerated by the client
#>

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

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

    $iis = GetIISServerManager
    if (!$iis)
    {
        return
    }

    switch ($Name)
    {
        'SuppressTelemetry' {
            New-ItemProperty -Path $configKeyPath -Name $suppressTelemetryValueName -Value $DefaultValue -PropertyType DWORD -Force | Out-Null
            $context.DeploymentSettings[$Name] = $false
            UpdateDeploymentSettingsFile $context
        }
        'LaunchResourceInBrowser' {
            if($context.ConfigKey.GetValue($launchResourceInBrowserValueName) -ne $null)
            {
                Remove-ItemProperty -Path $configKeyPath -Name $launchResourceInBrowserValueName
            }
            if ($context.DeploymentSettings[$Name] -ne $null) 
            {
                $context.DeploymentSettings.Remove($Name)
                UpdateDeploymentSettingsFile $context
            }
        }
        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
    }

    $iis = GetIISServerManager
    if (!$iis)
    {
        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
# MIIjkgYJKoZIhvcNAQcCoIIjgzCCI38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDPVinbZPfjExhc
# d7nWiZwLl0yvWkIkvOZ/4Ip8HF1yrqCCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVZzCCFWMCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgLy/JtDnk
# /Ae5TqAH2cs8qCHBMlA7Xti+oZpTb/e+t1AwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQAmGEQg2EavwmIaOPYZCGzJWf2SAxvzlV0OK1euYEJf
# p8HbvaNck6LiiBwdCz8cpzPlllJ0EkjCrX+nUWr2vnqmz5P05+dyg2KmY6Yx7yxX
# +tRni7mfitnj38nzqUtOp2UwB4hrnOjjfn/Zt1QWcKDcVleX5zet8+Qcs07JWGdc
# Uk01hvdwjdT2FwoLS5MyYe+ND/Myq44BH9U7Crh31hR1pBixGVdEOqxYiqImjzDf
# RKJ4QgiAj5E6ioXIlJXUkVkcZL5w0l9T+phGutzdh2ejqVEWSEMoRAjO2F95zZtV
# ARzz6rHma0uur6W7wAFEg8YQC+pbIjkALZEqK/JJLfPwoYIS8TCCEu0GCisGAQQB
# gjcDAwExghLdMIIS2QYJKoZIhvcNAQcCoIISyjCCEsYCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIKju0JdzAT/sO1Lq6sPBUZVzmtLooJdirspfZrDr
# xXIvAgZgrrtzEtoYEzIwMjEwNjE2MDAzMzM5LjQ5MlowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjozMkJELUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABYtD+AvMB5c1JAAAA
# AAFiMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIxMDExNDE5MDIyMloXDTIyMDQxMTE5MDIyMlowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozMkJE
# LUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+GodT2ucL3Mr2DQsv2
# ELNbSvKyBpYdUKtUBWiZmFVy18pG/pucgkrc5i9tu8CY7GpWV/CQNmHG2mVeSHMJ
# vbwCc/AAv7JP3bFCt6Zg75IbVSNOGA1eqLbmQiC6UAfSKXLN3dHtQ5diihb3Ymzp
# NP9K0cVPZfv2MXm+ZVU0RES8cyPkXel7+UEGE+kqdiBNDdb8yBXd8sju+90+V4nz
# YC+ZWW7SFJ2FFZlASpVaHpjv+eGohXlQaSBvmM4Q0xe3LhzQM8ViGz9cLeFSKgFf
# SY7qizL7wUg+eqYvDUyjPX8axEQHmk0th23wWH5p0Wduws43qNIo0OQ0mRotBK71
# nykCAwEAAaOCARswggEXMB0GA1UdDgQWBBTLxEoRYEpDtzp84B5WlZN2kP4qazAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQAtQa3DoXYbW/cXACbcVSFGe4gC8GXs
# FxSHT3JgwFU/NdJOcbkcFTVvTp6vlmTvHm6sIjknRBB0Xi1NBTqPw20u6u/T7Cnc
# /z0gT6mf9crI0VR9C+R1CtjezYKZEdZZ7fuNQWjsyftNDhQy+Rqnqryt0VoezLal
# heiinHzZD/4Y4hZYPf0u8TSv1ZfKtdBweWG3QU0Lp/I9SbIoemDG97RULMcPvq2u
# fhUp3OMiYQGL1WqkykSnqRJsM2IcA4l4dmoPNP6dLg5Dr7NVoYKIMInaQVZjSwDM
# ZhWryvfizX0SrzyLgkMPhLMVkfLxQQSQ37NeFk7F1RfeAkNWAh6mCORBMIIGcTCC
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
# NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoz
# MkJELUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAmrP6Chrbz0ax7s57n5Pop3VC8gyggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AORzl2swIhgPMjAyMTA2MTYwMTE3MzFaGA8yMDIxMDYxNzAxMTczMVowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA5HOXawIBADAKAgEAAgIcnQIB/zAHAgEAAgIRgTAK
# AgUA5HTo6wIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAH7PfmF1/cmP6g7M
# LnQZBuroEt3e7n8HaQfgQxB/ry9VAgX/gCjzNlNgnAu7jk8rmhCdt2qOCiWKQnao
# g6u5P1ROMwl2MxSHwPzB7FabamQl0Wntrov94OEE4FVExHDeC9Ri+EACH5rPN3w3
# vmToz+mZYkmCOcxnuSVZD3XGcl+gMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAFi0P4C8wHlzUkAAAAAAWIwDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgD4jhmVp+Bb2+PT0si2xyDs/RQHQrwbNlH+t0T494v3EwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCCKqhiV+zwNDrpU7DRB7Mi57xi6GBNYsGjgZqq2
# qVMKMjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# YtD+AvMB5c1JAAAAAAFiMCIEIHeOTH0ZO+VAn/o9lcoxReV3LIWyONAsdM87qeEv
# nvCFMA0GCSqGSIb3DQEBCwUABIIBAEri2YfX6ZGA6KObtFUuNs+/s/f7l1snfs41
# v1FkEtqe4PZYAiN8v5AXJVE28SqBoXy3uIG+VYv0PukLFsCHyTBr5pQvDOwzkSEE
# 0MsSwmqDCKK9Po9oxMZj1AVfLVda4h80GEK5UkQrXdF5nZ8EoBEXBKwJmBDpLuWw
# 5kQTq8bnYfKlFiOc7ouFwBEi8kZGddbrFy/ly/IZVe6ajZR8NxaDoMok8CKrOBVN
# ZcvQRKfQyCpR53A67BbXzEdowVt5draGwnoxnt+5u3/aAZ2ybaGK1SWTkH2TtiKc
# r9UqN71Jep092c94QXdleQBmU2DaaxJR1F2GrXDDQdu8F03bYY8=
# SIG # End signature block