Public/DGateway.ps1


. "$PSScriptRoot/../Private/CertificateHelper.ps1"
. "$PSScriptRoot/../Private/PlatformHelper.ps1"
. "$PSScriptRoot/../Private/TokenHelper.ps1"

$script:DGatewayConfigFileName = 'gateway.json'
$script:DGatewayCertificateFileName = 'server.crt'
$script:DGatewayPrivateKeyFileName = 'server.key'
$script:DGatewayProvisionerPublicKeyFileName = 'provisioner.pem'
$script:DGatewayProvisionerPrivateKeyFileName = 'provisioner.key'
$script:DGatewayDelegationPublicKeyFileName = 'delegation.pem'
$script:DGatewayDelegationPrivateKeyFileName = 'delegation.key'
$script:DGatewayCustomUsersFileName = 'users.txt'

function Get-DGatewayVersion {
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet('PSModule', 'Installed')]
        [string] $Type
    )

    if ($Type -eq 'PSModule') {
        $ManifestPath = "$PSScriptRoot/../DevolutionsGateway.psd1"
        $Manifest = Import-PowerShellDataFile -Path $ManifestPath
        $DGatewayVersion = $Manifest.ModuleVersion
    } elseif ($Type -eq 'Installed') {
        if ($IsWindows) {
            $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
            | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' }
            if ($UninstallReg) {
                $DGatewayVersion = '20' + $UninstallReg.DisplayVersion
            }
        } elseif ($IsMacOS) {
            throw 'not supported'
        } elseif ($IsLinux) {
            $PackageName = 'devolutions-gateway'
            $DpkgStatus = $(dpkg -s $PackageName 2>$null)
            $DpkgMatches = $($DpkgStatus | Select-String -AllMatches -Pattern 'version: (\S+)').Matches
            if ($DpkgMatches) {
                $VersionQuad = $DpkgMatches.Groups[1].Value
                $VersionTriple = $VersionQuad -Replace '^(\d+)\.(\d+)\.(\d+)\.(\d+)$', "`$1.`$2.`$3"
                $DGatewayVersion = $VersionTriple
            }
        }
    }

    $DGatewayVersion
}

class DGatewayListener {
    [string] $InternalUrl
    [string] $ExternalUrl

    DGatewayListener() { }

    DGatewayListener([string] $InternalUrl, [string] $ExternalUrl) {
        $this.InternalUrl = $InternalUrl
        $this.ExternalUrl = $ExternalUrl
    }
}

function New-DGatewayListener() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $ListenerUrl,
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $ExternalUrl
    )

    return [DGatewayListener]::new($ListenerUrl, $ExternalUrl)
}

class DGatewaySubProvisionerKey {
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Id

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Value

    [ValidateNotNullOrEmpty()]
    [ValidateSet('Spki','Rsa')]
    [string] $Format

    [ValidateNotNullOrEmpty()]
    [ValidateSet('Multibase','Base64', 'Base64Pad', 'Base64Url', 'Base64UrlPad')]
    [string] $Encoding

    DGatewaySubProvisionerKey(
        [string] $Id,
        [string] $Value,
        [string] $Format = 'Spki',
        [string] $Encoding = 'Multibase'
    ) {
        $this.Id = $Id
        $this.Value = $Value
        $this.Format = $Format
        $this.Encoding = $Encoding
    }

    DGatewaySubProvisionerKey([PSCustomObject] $object) {
        $this.Id = $object.Id
        $this.Value = $object.Value
        $this.Format = $object.Format
        $this.Encoding = $object.Encoding
    }

    DGatewaySubProvisionerKey([Hashtable] $table) {
        $this.Id = $table.Id
        $this.Value = $table.Value
        $this.Format = $table.Format
        $this.Encoding = $table.Encoding
    }
}

class DGatewaySubscriber {
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [System.Uri] $Url

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Token

    DGatewaySubscriber(
        [System.Uri] $Url,
        [string] $Token
    ) {
        $this.Url = $Url
        $this.Token = $Token
    }

    DGatewaySubscriber([PSCustomObject] $object) {
        $this.Url = $object.Url
        $this.Token = $object.Token
    }

    DGatewaySubscriber([Hashtable] $table) {
        $this.Url = $table.Url
        $this.Token = $table.Token
    }
}

class DGatewayNgrokTunnel {
    [string] $Proto
    [string] $Metadata
    [string[]] $AllowCidrs
    [string[]] $DenyCidrs

    # HTTP tunnel
    [string] $Domain
    [System.Nullable[System.Single]] $CircuitBreaker
    [System.Nullable[System.Boolean]] $Compression

    # TCP tunnel
    [string] $RemoteAddr

    DGatewayNgrokTunnel() { }
}

class DGatewayNgrokConfig {
    [string] $AuthToken
    [System.Nullable[System.UInt32]] $HeartbeatInterval
    [System.Nullable[System.UInt32]] $HeartbeatTolerance
    [string] $Metadata
    [string] $ServerAddr
    [PSCustomObject] $Tunnels

    DGatewayNgrokConfig() { }
}

function New-DGatewayNgrokTunnel() {
    [CmdletBinding(DefaultParameterSetName = 'http')]
    param(
        [Parameter(Mandatory = $false, ParameterSetName = 'http',
            HelpMessage = "HTTP tunnel")]
        [switch] $Http,

        [Parameter(Mandatory = $false, ParameterSetName = 'tcp',
            HelpMessage = "TCP tunnel")]
        [switch] $Tcp,

        [Parameter(Mandatory = $false,
            HelpMessage = "User-defined metadata that appears when listing tunnel sessions with ngrok")]
        [string] $Metadata,

        [ValidateScript({
            $_ -match '^((\d{1,3}\.){3}\d{1,3}\/\d{1,2}|([\dA-Fa-f]{0,4}:){2,7}[\dA-Fa-f]{0,4}\/\d{1,3})$'
        })]
        [Parameter(Mandatory = $false,
            HelpMessage = "Reject connections that do not match the given CIDRs")]
        [string[]] $AllowCidrs,

        [ValidateScript({
            $_ -match '^((\d{1,3}\.){3}\d{1,3}\/\d{1,2}|([\dA-Fa-f]{0,4}:){2,7}[\dA-Fa-f]{0,4}\/\d{1,3})$'
        })]
        [Parameter(Mandatory = $false,
            HelpMessage = "Reject connections that match the given CIDRs")]
        [string[]] $DenyCidrs,

        [ValidateScript({
            $_ -match '^(\*\.)?([a-zA-Z0-9](-?[a-zA-Z0-9])*\.)*[a-zA-Z]{2,}$'
        })]
        [Parameter(Mandatory = $false, ParameterSetName = 'http',
            HelpMessage = "Any valid domain or hostname previously registered with ngrok")]
        [string] $Domain,

        [ValidateRange(0.0, 1.0)]
        [Parameter(Mandatory = $false, ParameterSetName = 'http',
            HelpMessage = "Reject requests when 5XX responses exceed this ratio")]
        [System.Single] $CircuitBreaker,

        [Parameter(Mandatory = $false, ParameterSetName = 'http',
            HelpMessage = "Use gzip compression on HTTP responses")]
        [System.Boolean] $Compression,

        [ValidateScript({
            $_ -match '^([a-zA-Z0-9](-?[a-zA-Z0-9])*\.)*[a-zA-Z]{2,}:\d{1,5}$'
        })]
        [Parameter(Mandatory = $false, ParameterSetName = 'tcp',
            HelpMessage = "The remote TCP address and port to bind. For example: remote_addr: 2.tcp.ngrok.io:21746")]
        [string] $RemoteAddr
    )

    $tunnel = [DGatewayNgrokTunnel]::new()

    if ($Tcp) {
        $tunnel.Proto = "tcp"
    } else {
        $tunnel.Proto = "http"
    }

    $properties = [DGatewayNgrokTunnel].GetProperties() | ForEach-Object { $_.Name }
    foreach ($param in $PSBoundParameters.GetEnumerator()) {
        if ($properties -Contains $param.Key) {
            $tunnel.($param.Key) = $param.Value
        }
    }

    $tunnel
}

function New-DGatewayNgrokConfig() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $AuthToken
    )

    $ngrok = [DGatewayNgrokConfig]::new()
    $ngrok.AuthToken = $AuthToken
    $ngrok
}

class DGatewayWebAppConfig {
    [bool] $Enabled
    [string] $Authentication
    [System.Nullable[System.UInt32]] $AppTokenMaximumLifetime
    [System.Nullable[System.UInt32]] $LoginLimitRate
    [string] $UsersFile

    DGatewayWebAppConfig() { }
}

function New-DGatewayWebAppConfig() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [bool] $Enabled,

        [Parameter(Mandatory = $true)]
        [ValidateSet("None", "Custom")]
        [string] $Authentication
    )

    $webapp = [DGatewayWebAppConfig]::new()
    $webapp.Enabled = $Enabled
    $webapp.Authentication = $Authentication
    $webapp
}

class DGatewayAgentTunnelConfig {
    [bool] $Enabled = $false

    [ValidateRange(1, 65535)]
    [System.UInt16] $ListenPort = 4433

    DGatewayAgentTunnelConfig() { }

    DGatewayAgentTunnelConfig([bool] $Enabled, [System.UInt16] $ListenPort) {
        $this.Enabled = $Enabled
        $this.ListenPort = $ListenPort
    }
}

function New-DGatewayAgentTunnelConfig() {
    [CmdletBinding()]
    [OutputType('DGatewayAgentTunnelConfig')]
    param(
        [switch] $Enabled,
        [ValidateRange(1, 65535)]
        [System.UInt16] $ListenPort = 4433
    )

    [DGatewayAgentTunnelConfig]::new($Enabled.IsPresent, $ListenPort)
}

enum VerbosityProfile {
    Default
    Debug
    Tls
    All
    Quiet
}

class DGatewayConfig {
    [System.Nullable[Guid]] $Id
    [string] $Hostname

    [string] $RecordingPath

    [string] $TlsCertificateFile
    [string] $TlsPrivateKeyFile
    [string] $TlsPrivateKeyPassword

    [string] $TlsCertificateSource
    [string] $TlsCertificateSubjectName
    [string] $TlsCertificateStoreName
    [string] $TlsCertificateStoreLocation

    [string] $ProvisionerPublicKeyFile
    [string] $ProvisionerPrivateKeyFile
    [string] $DelegationPublicKeyFile
    [string] $DelegationPrivateKeyFile
    [DGatewaySubProvisionerKey] $SubProvisionerPublicKey

    [DGatewayListener[]] $Listeners
    [DGatewaySubscriber] $Subscriber

    [DGatewayNgrokConfig] $Ngrok
    
    [DGatewayWebAppConfig] $WebApp

    [DGatewayAgentTunnelConfig] $AgentTunnel

    [string] $LogDirective
    [string] $VerbosityProfile
}

function Remove-NullObjectProperties {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, Mandatory)]
        [object[]] $InputObject
    )
    process {
        foreach ($OldObj in $InputObject) {
            $NonNullProperties = $OldObj.PSObject.Properties | Where-Object {
                ($_.Value -is [Array] -and $_.Value.Count -gt 0) -or
                (-Not [string]::IsNullOrEmpty($_.Value))
            } | Select-Object -ExpandProperty Name
            $NewObj = $OldObj | Select-Object $NonNullProperties
            $NewObj.PSObject.Properties |
                Where-Object { $_.TypeNameOfValue.EndsWith('PSCustomObject') } |
                ForEach-Object {
                    $NewObj."$($_.Name)" = $NewObj."$($_.Name)" | Remove-NullObjectProperties
                }
            $NewObj
        }
    }
}

function Save-DGatewayConfig {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory = $true)]
        [DGatewayConfig] $Config
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName
    $ConfigClean = $Config | ConvertTo-Json -Depth 4 | ConvertFrom-Json # drop class type info
    $ConfigData = $ConfigClean | Remove-NullObjectProperties | ConvertTo-Json -Depth 4

    [System.IO.File]::WriteAllLines($ConfigFile, $ConfigData, $(New-Object System.Text.UTF8Encoding $False))
}

function Set-DGatewayConfig {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $Force,

        [Guid] $Id,
        [string] $Hostname,

        [string] $RecordingPath,

        [DGatewayListener[]] $Listeners,
        [DGatewaySubscriber] $Subscriber,

        [string] $TlsCertificateFile,
        [string] $TlsPrivateKeyFile,
        [string] $TlsPrivateKeyPassword,

        [ValidateSet("External", "System")]
        [string] $TlsCertificateSource,
        [string] $TlsCertificateSubjectName,
        [string] $TlsCertificateStoreName,
        [ValidateSet("CurrentUser", "LocalMachine", "CurrentService")]
        [string] $TlsCertificateStoreLocation,

        [string] $ProvisionerPublicKeyFile,
        [string] $ProvisionerPrivateKeyFile,

        [string] $DelegationPublicKeyFile,
        [string] $DelegationPrivateKeyFile,

        [DGatewaySubProvisionerKey] $SubProvisionerPublicKey,

        [DGatewayNgrokConfig] $Ngrok,

        [DGatewayWebAppConfig] $WebApp,

        [DGatewayAgentTunnelConfig] $AgentTunnel,

        [VerbosityProfile] $VerbosityProfile
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    if (-Not (Test-Path -Path $ConfigPath -PathType 'Container')) {
        New-Item -Path $ConfigPath -ItemType 'Directory' | Out-Null
    }

    $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName

    if (-Not (Test-Path -Path $ConfigFile -PathType 'Leaf')) {
        $config = [DGatewayConfig]::new()
    } else {
        $config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    }

    $properties = [DGatewayConfig].GetProperties() | ForEach-Object { $_.Name }
    foreach ($param in $PSBoundParameters.GetEnumerator()) {
        if ($properties -Contains $param.Key) {
            $config.($param.Key) = $param.Value
        }
    }

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Get-DGatewayConfig {
    [CmdletBinding()]
    [OutputType('DGatewayConfig')]
    param(
        [string] $ConfigPath,
        [switch] $NullProperties,
        [switch] $Expand
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName

    $config = [DGatewayConfig]::new()

    if (-Not (Test-Path -Path $ConfigFile -PathType 'Leaf')) {
        if ($NullProperties) {
            return $config
        }
    }

    $ConfigData = Get-Content -Path $ConfigFile -Encoding UTF8
    $json = $ConfigData | ConvertFrom-Json

    [DGatewayConfig].GetProperties() | ForEach-Object {
        $Name = $_.Name
        if ($json.PSObject.Properties[$Name]) {
            $Property = $json.PSObject.Properties[$Name]
            $Value = $Property.Value
            $config.$Name = $Value
        }
    }

    if ($Expand) {
        Expand-DGatewayConfig $config
    }

    if (-Not $NullProperties) {
        $Properties = $Config.PSObject.Properties.Name
        $NonNullProperties = $Properties.Where( { -Not [string]::IsNullOrEmpty($Config.$_) })
        $Config = $Config | Select-Object $NonNullProperties
    }

    return $config
}

function Expand-DGatewayConfig {
    param(
        [DGatewayConfig] $Config
    )
}

function Find-DGatewayConfig {
    [CmdletBinding()]
    param(
        [string] $ConfigPath
    )

    if (-Not $ConfigPath) {
        $CurrentPath = Get-Location
        $ConfigFile = Join-Path $CurrentPath $DGatewayConfigFileName

        if (Test-Path -Path $ConfigFile -PathType 'Leaf') {
            $ConfigPath = $CurrentPath
        }
    }

    if (-Not $ConfigPath) {
        $ConfigPath = Get-DGatewayPath
    }

    if ($Env:DGATEWAY_CONFIG_PATH) {
        $ConfigPath = $Env:DGATEWAY_CONFIG_PATH
    }

    return $ConfigPath
}

function Enter-DGatewayConfig {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [switch] $ChangeDirectory
    )

    if ($ConfigPath) {
        $ConfigPath = Resolve-Path $ConfigPath
        $Env:DGATEWAY_CONFIG_PATH = $ConfigPath
    }

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    if ($ChangeDirectory) {
        Set-Location $ConfigPath
    }
}

function Exit-DGatewayConfig {
    Remove-Item Env:DGATEWAY_CONFIG_PATH
}

function Get-DGatewayPath() {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [ValidateSet('ConfigPath')]
        [string] $PathType = 'ConfigPath'
    )

    $DisplayName = 'Gateway'
    $CompanyName = 'Devolutions'

    if ($IsWindows) {
        $ConfigPath = $Env:ProgramData + "\${CompanyName}\${DisplayName}"
    } elseif ($IsMacOS) {
        $ConfigPath = "/Library/Application Support/${CompanyName} ${DisplayName}"
    } elseif ($IsLinux) {
        $ConfigPath = '/etc/devolutions-gateway'
    }

    switch ($PathType) {
        'ConfigPath' { $ConfigPath }
        default { throw("Invalid path type: $PathType") }
    }
}

function Get-DGatewayRecordingPath {
    [CmdletBinding()]
    param(
        [string] $ConfigPath
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    $RecordingPath = $Config.RecordingPath

    if ([string]::IsNullOrEmpty($RecordingPath)) {
        $RecordingPath = Join-Path $ConfigPath "recordings"
    }

    $RecordingPath
}

function Set-DGatewayRecordingPath {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $RecordingPath
    )

    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    $Config.RecordingPath = $RecordingPath
    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Reset-DGatewayRecordingPath {
    [CmdletBinding()]
    param(
        [string] $ConfigPath
    )

    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    $Config.RecordingPath = $null
    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Get-DGatewayHostname {
    [CmdletBinding()]
    param(
        [string] $ConfigPath
    )

    $(Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties).Hostname
}

function Set-DGatewayHostname {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Hostname
    )

    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    $Config.Hostname = $Hostname
    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Get-DGatewayListeners {
    [CmdletBinding()]
    [OutputType('DGatewayListener[]')]
    param(
        [string] $ConfigPath
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    $Config.Listeners
}

function Set-DGatewayListeners {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        [DGatewayListener[]] $Listeners
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    $Config.Listeners = $Listeners
    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function New-DGatewayCertificate {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $Hostname,
        [switch] $Force
    )

    if (-Not $IsWindows) {
        throw "unsupported platform"
    }

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if ([string]::IsNullOrEmpty($Hostname)) {
        $Hostname = $Config.Hostname
    }

    if ([string]::IsNullOrEmpty($Hostname)) {
        $Hostname = [System.Environment]::MachineName
    }

    Set-DGatewayHostname -ConfigPath:$ConfigPath $Hostname

    $Password = "cert123!" # dummy password (it's just a self-signed certificate)
    $SecurePassword = ConvertTo-SecureString -String $Password -Force -AsPlainText

    # Create a self-signed certificate for the specified hostname and export to a .pfx file
    $NotBefore = Get-Date
    $ExtendedKeyUsage = "2.5.29.37={text}1.3.6.1.5.5.7.3.1"
    $Params = @{
        DnsName = $Hostname
        CertStoreLocation = "cert:\CurrentUser\My"
        KeyExportPolicy = "Exportable"
        KeyAlgorithm = "RSA"
        KeyLength = 2048
        HashAlgorithm = 'SHA256'
        TextExtension = @($ExtendedKeyUsage)
        KeyUsageProperty = "All"
        KeyUsage = 'CertSign', 'DigitalSignature', 'KeyEncipherment'
        NotBefore = $NotBefore.AddHours(-1)
        NotAfter = $NotBefore.AddYears(5)
    }
    $Certificate = New-SelfSignedCertificate @Params
    
    $PfxCertificateFile = Join-Path ([System.IO.Path]::GetTempPath()) "gateway-$Hostname.pfx"
    Export-PfxCertificate -Cert $Certificate -FilePath $PfxCertificateFile -Password $securePassword | Out-Null
    Remove-Item -Path ("cert:\CurrentUser\My\" + $Certificate.Thumbprint) | Out-Null

    Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $PfxCertificateFile -Password $Password
    Remove-Item $PfxCertificateFile | Out-Null # remove temporary .pfx file
}

function Import-DGatewayCertificate {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $CertificateFile,
        [string] $PrivateKeyFile,
        [string] $Password
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    $result = Get-PemCertificate -CertificateFile:$CertificateFile `
        -PrivateKeyFile:$PrivateKeyFile -Password:$Password
        
    $CertificateData = $result.Certificate
    $PrivateKeyData = $result.PrivateKey

    New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null

    $CertificateFile = Join-Path $ConfigPath $DGatewayCertificateFileName
    $PrivateKeyFile = Join-Path $ConfigPath $DGatewayPrivateKeyFileName

    Set-Content -Path $CertificateFile -Value $CertificateData -Force
    Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force

    $Config.TlsCertificateFile = $DGatewayCertificateFileName
    $Config.TlsPrivateKeyFile = $DGatewayPrivateKeyFileName

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function New-DGatewayProvisionerKeyPair {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [int] $KeySize = 2048,
        [switch] $Force
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if (-Not (Test-Path -Path $ConfigPath)) {
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
    }

    $PublicKeyFile = Join-Path $ConfigPath $DGatewayProvisionerPublicKeyFileName
    $PrivateKeyFile = Join-Path $ConfigPath $DGatewayProvisionerPrivateKeyFileName

    if ((Test-Path -Path $PublicKeyFile) -Or (Test-Path -Path $PrivateKeyFile)) {
        if (-Not $Force) {
            throw "$PublicKeyFile or $PrivateKeyFile already exists, use -Force to overwrite"
        }

        Remove-Item $PublicKeyFile -Force | Out-Null
        Remove-Item $PrivateKeyFile -Force | Out-Null
    }

    $KeyPair = New-RsaKeyPair -KeySize:$KeySize

    $PublicKeyData = $KeyPair.PublicKey
    $Config.ProvisionerPublicKeyFile = $DGatewayProvisionerPublicKeyFileName
    Set-Content -Path $PublicKeyFile -Value $PublicKeyData -Force

    $PrivateKeyData = $KeyPair.PrivateKey
    $Config.ProvisionerPrivateKeyFile = $DGatewayProvisionerPrivateKeyFileName
    Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Import-DGatewayProvisionerKey {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $PublicKeyFile,
        [string] $PrivateKeyFile
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if ($PublicKeyFile) {
        if (-Not (Test-Path -Path $PublicKeyFile)) {
            throw "$PublicKeyFile doesn't exist"
        }

        $PublicKeyData = Get-Content -Path $PublicKeyFile -Encoding UTF8

        if (!$PublicKeyData) {
            throw "$PublicKeyFile appears to be empty"          
        }

        $OutputFile = Join-Path $ConfigPath $DGatewayProvisionerPublicKeyFileName
        $Config.ProvisionerPublicKeyFile = $DGatewayProvisionerPublicKeyFileName
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
        Set-Content -Path $OutputFile -Value $PublicKeyData -Force
    }

    if ($PrivateKeyFile) {
        if (-Not (Test-Path -Path $PrivateKeyFile)) {
            throw "$PrivateKeyFile doesn't exist"
        }

        $PrivateKeyData = Get-Content -Path $PrivateKeyFile -Encoding UTF8

        if (!$PrivateKeyData) {
            throw "$PrivateKeyFile appears to be empty"          
        }

        $OutputFile = Join-Path $ConfigPath $DGatewayProvisionerPrivateKeyFileName
        $Config.ProvisionerPrivateKeyFile = $DGatewayProvisionerPrivateKeyFileName
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
        Set-Content -Path $OutputFile -Value $PrivateKeyData -Force
    }

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function New-DGatewayDelegationKeyPair {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [int] $KeySize = 2048,
        [switch] $Force
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if (-Not (Test-Path -Path $ConfigPath)) {
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
    }

    $PublicKeyFile = Join-Path $ConfigPath $DGatewayDelegationPublicKeyFileName
    $PrivateKeyFile = Join-Path $ConfigPath $DGatewayDelegationPrivateKeyFileName

    if ((Test-Path -Path $PublicKeyFile) -Or (Test-Path -Path $PrivateKeyFile)) {
        if (-Not $Force) {
            throw "$PublicKeyFile or $PrivateKeyFile already exists, use -Force to overwrite"
        }

        Remove-Item $PublicKeyFile -Force | Out-Null
        Remove-Item $PrivateKeyFile -Force | Out-Null
    }

    $KeyPair = New-RsaKeyPair -KeySize:$KeySize

    $PublicKeyData = $KeyPair.PublicKey
    $Config.DelegationPublicKeyFile = $DGatewayDelegationPublicKeyFileName
    Set-Content -Path $PublicKeyFile -Value $PublicKeyData -Force

    $PrivateKeyData = $KeyPair.PrivateKey
    $Config.DelegationPrivateKeyFile = $DGatewayDelegationPrivateKeyFileName
    Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function Import-DGatewayDelegationKey {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $PublicKeyFile,
        [string] $PrivateKeyFile
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if ($PublicKeyFile) {
        $PublicKeyData = Get-Content -Path $PublicKeyFile -Encoding UTF8
        $OutputFile = Join-Path $ConfigPath $DGatewayDelegationPublicKeyFileName
        $Config.DelegationPublicKeyFile = $DGatewayDelegationPublicKeyFileName
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
        Set-Content -Path $OutputFile -Value $PublicKeyData -Force
    }

    if ($PrivateKeyFile) {
        $PrivateKeyData = Get-Content -Path $PrivateKeyFile -Encoding UTF8
        $OutputFile = Join-Path $ConfigPath $DGatewayDelegationPrivateKeyFileName
        $Config.DelegationPrivateKeyFile = $DGatewayDelegationPrivateKeyFileName
        New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null
        Set-Content -Path $OutputFile -Value $PrivateKeyData -Force
    }

    Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config
}

function New-DGatewayToken {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,

        [ValidateSet('ASSOCIATION', 'SCOPE', 'BRIDGE', 'JMUX', 'JREC')]
        [Parameter(Mandatory = $true)]
        [string] $Type, # token type

        # public common claims
        [DateTime] $ExpirationTime, # exp
        [DateTime] $NotBefore, # nbf
        [DateTime] $IssuedAt, # iat

        # private association claims
        [string] $AssociationId, # jet_aid
        [ValidateSet('unknown', 'wayk', 'rdp', 'ard', 'vnc', 'ssh', 'ssh-pwsh', 'sftp', 'scp',
            'winrm-http-pwsh', 'winrm-https-pwsh', 'http', 'https', 'ldap', 'ldaps')]
        [string] $ApplicationProtocol, # jet_ap
        [ValidateSet('fwd', 'rdv')]
        [string] $ConnectionMode, # jet_cm
        [string] $DestinationHost, # dst_hst

        # private jrec claims
        [ValidateSet('push', 'pull')]
        [string] $RecordingOperation = 'push', # jet_rop

        # private scope claims
        [string] $Scope, # scope

        # private bridge claims
        [string] $Target, # target

        # signature parameters
        [string] $PrivateKeyFile
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties

    if (-Not $PrivateKeyFile) {
        if (-Not $Config.ProvisionerPrivateKeyFile) {
            throw "Config file is missing ``ProvisionerPrivateKeyFile``. Alternatively, use -PrivateKeyFile argument."
        }

        if ([System.IO.Path]::IsPathRooted($Config.ProvisionerPrivateKeyFile)) {
            $PrivateKeyFile = $Config.ProvisionerPrivateKeyFile
        } else {
            $PrivateKeyFile = Join-Path $ConfigPath $Config.ProvisionerPrivateKeyFile
        }
    }

    if (-Not (Test-Path -Path $PrivateKeyFile -PathType 'Leaf')) {
        throw "$PrivateKeyFile cannot be found."
    }

    $PrivateKey = ConvertTo-RsaPrivateKey $(Get-Content $PrivateKeyFile -Raw)

    $CurrentTime = Get-Date

    if (-Not $NotBefore) {
        $NotBefore = $CurrentTime
    }

    if (-Not $IssuedAt) {
        $IssuedAt = $CurrentTime
    }

    if (-Not $ExpirationTime) {
        $ExpirationTime = $CurrentTime.AddMinutes(2)
    }

    $iat = [System.DateTimeOffset]::new($IssuedAt).ToUnixTimeSeconds()
    $nbf = [System.DateTimeOffset]::new($NotBefore).ToUnixTimeSeconds()
    $exp = [System.DateTimeOffset]::new($ExpirationTime).ToUnixTimeSeconds()
    $jti = (New-Guid).ToString()

    $Header = [PSCustomObject]@{
        alg = 'RS256'
        typ = 'JWT'
        cty = $Type
    }

    $Payload = [PSCustomObject]@{
        iat    = $iat
        nbf    = $nbf
        exp    = $exp
        jti    = $jti
    }

    if ($Type -eq 'ASSOCIATION') {
        if (-Not $ApplicationProtocol) {
            if ($ConnectionMode -eq 'fwd') {
                $ApplicationProtocol = 'rdp'
            } else {
                $ApplicationProtocol = 'wayk'
            }
        }

        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol
        
        if (-Not $ConnectionMode) {
            if ($DestinationHost) {
                $ConnectionMode = 'fwd'
            } else {
                $ConnectionMode = 'rdv'
            }
        }
            
        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_cm' -Value $ConnectionMode

        if (-Not $AssociationId) {
            $AssociationId = New-Guid
        }

        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId

        if ($DestinationHost) {
            $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost
        }
    }

    if ($Type -eq 'JMUX') {
        if (-Not $DestinationHost) {
            throw "DestinationHost is required"
        }

        if ($ApplicationProtocol) {
            $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol
        }
        
        if (-Not $AssociationId) {
            $AssociationId = New-Guid
        }

        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId

        $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost
    }

    if ($Type -eq 'JREC') {
        if (-Not $RecordingOperation) {
            throw "RecordingOperation is required"
        }

        if ($ApplicationProtocol) {
            $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol
        }

        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_rop' -Value $RecordingOperation.ToLower()
        
        if (-Not $AssociationId) {
            $AssociationId = New-Guid
        }

        $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId

        if ($DestinationHost) {
            $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost
        }
    }

    if (($Type -eq 'SCOPE') -and ($Scope)) {
        $Payload | Add-Member -MemberType NoteProperty -Name 'scope' -Value $Scope
    }

    if (($Type -eq 'BRIDGE') -and ($Target)) {
        $Payload | Add-Member -MemberType NoteProperty -Name 'target' -Value $Target
    }

    New-JwtRs256 -Header $Header -Payload $Payload -PrivateKey $PrivateKey
}

function ConvertTo-DGatewayHash
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string] $Password
    )

    $parameters = [Devolutions.Picky.Argon2Params]::New.Invoke(@())
    $algorithm = [Devolutions.Picky.Argon2Algorithm]::Argon2id
    $argon2 = [Devolutions.Picky.Argon2]::New.Invoke(@($algorithm, $parameters))
    $argon2.HashPassword($Password)
}

function Get-DGatewayUsersFilePath
{
    [CmdletBinding()]
    param(
        [string] $ConfigPath
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath
    $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties
    
    if ($Config.WebApp.UsersFile) {
        $fileName = $Config.WebApp.UsersFile
    } else {
        $fileName = $script:DGatewayCustomUsersFileName
    }

    $filePath = Join-Path -Path $ConfigPath -ChildPath $fileName
    return $filePath
}

function Set-DGatewayUser {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory=$true, Position=0)]
        [string] $Username,
        [Parameter(Mandatory=$true, Position=1)]
        [string] $Password
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath
    $hash = ConvertTo-DGatewayHash -Password $Password

    $fileContent = @()
    if (Test-Path $filePath) {
        try {
            $fileContent = [System.IO.File]::ReadLines($filePath)
        }
        catch {
            Write-Host "Error reading file: $_"
            return
        }
    }

    $entry = "$Username`:$hash"
    $updated = $false

    $fileContentList = New-Object System.Collections.Generic.List[System.String]
    foreach ($line in $fileContent) {
        $fileContentList.Add($line)
    }

    for ($i = 0; $i -lt $fileContentList.Count; $i++) {
        if ((-Not [string]::IsNullOrEmpty($fileContentList[$i])) -And
            $fileContentList[$i].StartsWith("${Username}:")) {
            $fileContentList[$i] = $entry
            $updated = $true
            break
        }
    }

    if (-Not $updated) {
        $fileContentList.Add($entry)
    }

    [System.IO.File]::WriteAllLines($filePath, $fileContentList)
}

function Remove-DGatewayUser {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [Parameter(Mandatory=$true, Position=0)]
        [string] $Username
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath
    $fileContent = Get-Content $filePath

    $newContent = $fileContent | Where-Object { $_ -notmatch "^${Username}:" }
    Set-Content -Path $filePath -Value $newContent
}

function Get-DGatewayUser {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $Username
    )

    $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath

    $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath
    $fileContent = Get-Content $filePath
    $users = @()

    foreach ($line in $fileContent) {
        # Splitting at the first ':' character
        $splitIndex = $line.IndexOf(':')
        if ($splitIndex -lt 0) { continue }

        $user = $line.Substring(0, $splitIndex)
        $hash = $line.Substring($splitIndex + 1)

        $users += New-Object PSObject -Property @{
            User = $user
            Hash = $hash
        }
    }

    if ($Username) {
        $user = $users | Where-Object { $_.User -eq $Username }
        return $user
    } else {
        return $users
    }
}

function Get-DGatewayPackage {
    [CmdletBinding()]
    param(
        [string] $RequiredVersion,
        [ValidateSet('Windows', 'Linux')]
        [string] $Platform
    )

    $Version = Get-DGatewayVersion 'PSModule'

    if ($RequiredVersion) {
        $Version = $RequiredVersion
    }

    if (-Not $Platform) {
        if ($IsWindows) {
            $Platform = 'Windows'
        } else {
            $Platform = 'Linux'
        }
    }

    $GitHubDownloadUrl = 'https://github.com/Devolutions/devolutions-gateway/releases/download/'

    if ($Platform -eq 'Windows') {
        $PackageFileName = "DevolutionsGateway-${Version}-x64.msi"
    } elseif ($Platform -eq 'Linux') {
        $PackageFileName = "devolutions-gateway-${Version}-x64.deb"
    }

    $DownloadUrl = "${GitHubDownloadUrl}v${Version}/$PackageFileName"

    [PSCustomObject]@{
        Url     = $DownloadUrl;
        Version = $Version;
    }
}

function Install-DGatewayPackage {
    <#
    .SYNOPSIS
    Downloads and installs (or upgrades) the Devolutions Gateway package.

    .PARAMETER ServiceAccount
    Windows only. The account the Gateway service logs on as, in 'DOMAIN\Name' or '.\Name' form,
    such as a group managed service account ('CONTOSO\gateway$') or the virtual account
    'NT SERVICE\DevolutionsGateway'. Defaults to the account of the existing service on upgrade,
    and to 'NT AUTHORITY\NetworkService' on a first install.

    .PARAMETER ServiceCredential
    Windows only. Credential of a service account that logs on with a password. The user name is
    used as the service account when -ServiceAccount is not specified. Passwordless accounts
    (NETWORK SERVICE, virtual accounts, managed service accounts) do not need it.
    Required again for a major upgrade under a password-based account.
    If ServiceAccount is also supplied, it must match the credential user name.
    The password is visible in the installer process command line; avoid logging that command line.
    #>

    [CmdletBinding()]
    param(
        [string] $RequiredVersion,
        [switch] $Quiet,
        [switch] $Force,
        [string] $ServiceAccount,
        [PSCredential] $ServiceCredential
    )

    if (($ServiceAccount -or $ServiceCredential) -and -not $IsWindows) {
        throw 'ServiceAccount and ServiceCredential are supported only on Windows'
    }

    if ($ServiceAccount -and $ServiceCredential -and $ServiceAccount -ine $ServiceCredential.UserName) {
        throw 'ServiceAccount must match ServiceCredential.UserName; omit ServiceAccount to use the credential user name'
    }

    $Version = Get-DGatewayVersion 'PSModule'

    if ($RequiredVersion) {
        $Version = $RequiredVersion
    }

    $InstalledVersion = Get-DGatewayVersion 'Installed'

    if (($InstalledVersion -eq $Version) -and (-Not $Force)) {
        Write-Host "Devolutions Gateway is already installed ($Version)"
        return
    }

    $TempPath = Join-Path $([System.IO.Path]::GetTempPath()) "dgateway-${Version}"
    New-Item -ItemType Directory -Path $TempPath -ErrorAction SilentlyContinue | Out-Null

    $Package = Get-DGatewayPackage -RequiredVersion $Version
    $DownloadUrl = $Package.Url

    $DownloadFile = Split-Path -Path $DownloadUrl -Leaf
    $DownloadFilePath = Join-Path $TempPath $DownloadFile
    Write-Host "Downloading $DownloadUrl"

    $WebClient = [System.Net.WebClient]::new()
    $WebClient.DownloadFile($DownloadUrl, $DownloadFilePath)
    $WebClient.Dispose()

    $DownloadFilePath = Resolve-Path $DownloadFilePath

    if ($IsWindows) {
        $Display = '/passive'
        if ($Quiet) {
            $Display = '/quiet'
        }
        $InstallLogFile = Join-Path $TempPath 'DGateway_Install.log'
        $MsiArgs = @(
            '/i', "`"$DownloadFilePath`"",
            $Display,
            '/norestart',
            '/log', "`"$InstallLogFile`""
        )

        if ($ServiceCredential -and -Not $ServiceAccount) {
            $ServiceAccount = $ServiceCredential.UserName
        }

        if ($ServiceAccount) {
            $MsiArgs += ConvertTo-DGatewayMsiProperty -Name 'P.SERVICEACCOUNT' -Value $ServiceAccount
        }

        if ($ServiceCredential) {
            # Hidden MSI properties do not protect process command lines or the Debug=7 logging policy.
            $MsiArgs += ConvertTo-DGatewayMsiProperty -Name 'P.SERVICEPASSWORD' -Value $ServiceCredential.GetNetworkCredential().Password
        }

        $Process = Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -eq 3010) {
            Write-Warning 'Devolutions Gateway installation requires a restart'
        } elseif ($Process.ExitCode -ne 0) {
            throw "Devolutions Gateway installation failed (MSI exit code $($Process.ExitCode)). Log: $InstallLogFile"
        }

        Remove-Item -Path $InstallLogFile -Force -ErrorAction SilentlyContinue
    } elseif ($IsMacOS) {
        throw  'unsupported platform'
    } elseif ($IsLinux) {
        $DpkgArgs = @(
            '-i', $DownloadFilePath
        )
        if ((id -u) -eq 0) {
            Start-Process 'dpkg' -ArgumentList $DpkgArgs -Wait
        } else {
            $DpkgArgs = @('dpkg') + $DpkgArgs
            Start-Process 'sudo' -ArgumentList $DpkgArgs -Wait
        }
    }

    Remove-Item -Path $TempPath -Force -Recurse
}

function Uninstall-DGatewayPackage {
    [CmdletBinding()]
    param(
        [switch] $Quiet
    )

    if ($IsWindows) {
        $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
        | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' }
        if ($UninstallReg) {
            $UninstallString = $($UninstallReg.UninstallString `
                    -Replace 'msiexec.exe', '' -Replace '/I', '' -Replace '/X', '').Trim()
            $Display = '/passive'
            if ($Quiet) {
                $Display = '/quiet'
            }
            $MsiArgs = @(
                '/X', $UninstallString, $Display
            )
            Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait
        }
    } elseif ($IsMacOS) {
        throw  'unsupported platform'
    } elseif ($IsLinux) {
        if (Get-DGatewayVersion 'Installed') {
            $AptArgs = @(
                '-y', 'remove', 'devolutions-gateway', '--purge'
            )
            if ((id -u) -eq 0) {
                Start-Process 'apt-get' -ArgumentList $AptArgs -Wait
            } else {
                $AptArgs = @('apt-get') + $AptArgs
                Start-Process 'sudo' -ArgumentList $AptArgs -Wait
            }
        }
    }
}

function Start-DGateway {
    [CmdletBinding()]

    param(
        [string]$ServiceName
    )

    if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) {
        $DGatewayService = $ServiceName
    } elseif ($IsWindows) {
        $DGatewayService = 'devolutionsgateway'
    } elseif ($IsLinux) {
        $DGatewayService = 'devolutions-gateway.service'  
    } else {
        throw 'Service name is empty'
    }

    if ($IsWindows) {
        Start-Service -Name $DGatewayService
    } elseif ($IsLinux) {
        & systemctl start $DGatewayService
    } else {
        throw 'Not implemented'
    }
}

function Stop-DGateway {
    [CmdletBinding()]

    param(
        [string]$ServiceName
    )

    if (-Not [String]::IsNullOrWhiteSpace($serviceName)) {
        $DGatewayService = $ServiceName
    } elseif ($IsWindows) {
        $DGatewayService = 'devolutionsgateway'
    } elseif ($IsLinux) {
        $DGatewayService = 'devolutions-gateway.service'  
    } else {
        throw 'Service name is empty'
    }

    if ($IsWindows) {
        Stop-Service -Name $DGatewayService
    } elseif ($IsLinux) {
        & systemctl stop $DGatewayService
    } else {
        throw 'Not implemented'
    }
}

function Restart-DGateway {
    [CmdletBinding()]

    param(
        [string]$ServiceName
    )

    if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) {
        $DGatewayService = $ServiceName
    } elseif ($IsWindows) {
        $DGatewayService = 'devolutionsgateway'
    } elseif ($IsLinux) {
        $DGatewayService = 'devolutions-gateway.service'  
    } else {
        throw 'Service name is empty'
    }

    if ($IsWindows) {
        Restart-Service -Name $DGatewayService
    } elseif ($IsLinux) {
        & systemctl restart $DGatewayService
    } else {
        throw 'Not implemented'
    }    
}

function Get-DGatewayService {
    [CmdletBinding()]

    param(
        [string]$ServiceName
    )

    if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) {
        $DGatewayService = $ServiceName
    } elseif ($IsWindows) {
        $DGatewayService = 'devolutionsgateway'
    } elseif ($IsLinux) {
        $DGatewayService = 'devolutions-gateway.service'  
    } else {
        throw 'Service name is empty'
    }

    if ($IsWindows) {
        $Result = Get-Service -Name $DGatewayService

        If ($Result) {
            [PSCustomObject]@{
                "Name"   = $Result.Name
                "Status" = $Result.Status
            }
        } else {
            throw 'Service not found'
        }
    } elseif ($IsLinux) {
        $Result = & systemctl list-units --type=service --all --no-pager $DGatewayService --no-legend

        if ($Result) {
            $ID, $Load, $Active, $Status, $Description = ($Result.Trim()) -Split '\s+', 5

            If ($ID -And $Active) {
                [PSCustomObject]@{
                    "Name"   = $ID
                    "Status" = if ($Active -EQ 'active') { 'Running' } else { 'Stopped' }
                }
            } else {
                throw 'Unable to return service status'
            }
        } else {
            throw 'Service not found'
        }
    } else {
        throw 'Not implemented'
    }
}
# SIG # Begin signature block
# MIIvWwYJKoZIhvcNAQcCoIIvTDCCL0gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDnx/4Wek2wLsZw
# Tddq6apPcHopxIn5qCERf9NQdnc1XaCCFBkwggVyMIIDWqADAgECAhB2U/6sdUZI
# k/Xl10pIOk74MA0GCSqGSIb3DQEBDAUAMFMxCzAJBgNVBAYTAkJFMRkwFwYDVQQK
# ExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIENvZGUgU2ln
# bmluZyBSb290IFI0NTAeFw0yMDAzMTgwMDAwMDBaFw00NTAzMTgwMDAwMDBaMFMx
# CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQD
# EyBHbG9iYWxTaWduIENvZGUgU2lnbmluZyBSb290IFI0NTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBALYtxTDdeuirkD0DcrA6S5kWYbLl/6VnHTcc5X7s
# k4OqhPWjQ5uYRYq4Y1ddmwCIBCXp+GiSS4LYS8lKA/Oof2qPimEnvaFE0P31PyLC
# o0+RjbMFsiiCkV37WYgFC5cGwpj4LKczJO5QOkHM8KCwex1N0qhYOJbp3/kbkbuL
# ECzSx0Mdogl0oYCve+YzCgxZa4689Ktal3t/rlX7hPCA/oRM1+K6vcR1oW+9YRB0
# RLKYB+J0q/9o3GwmPukf5eAEh60w0wyNA3xVuBZwXCR4ICXrZ2eIq7pONJhrcBHe
# OMrUvqHAnOHfHgIB2DvhZ0OEts/8dLcvhKO/ugk3PWdssUVcGWGrQYP1rB3rdw1G
# R3POv72Vle2dK4gQ/vpY6KdX4bPPqFrpByWbEsSegHI9k9yMlN87ROYmgPzSwwPw
# jAzSRdYu54+YnuYE7kJuZ35CFnFi5wT5YMZkobacgSFOK8ZtaJSGxpl0c2cxepHy
# 1Ix5bnymu35Gb03FhRIrz5oiRAiohTfOB2FXBhcSJMDEMXOhmDVXR34QOkXZLaRR
# kJipoAc3xGUaqhxrFnf3p5fsPxkwmW8x++pAsufSxPrJ0PBQdnRZ+o1tFzK++Ol+
# A/Tnh3Wa1EqRLIUDEwIrQoDyiWo2z8hMoM6e+MuNrRan097VmxinxpI68YJj8S4O
# JGTfAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G
# A1UdDgQWBBQfAL9GgAr8eDm3pbRD2VZQu86WOzANBgkqhkiG9w0BAQwFAAOCAgEA
# Xiu6dJc0RF92SChAhJPuAW7pobPWgCXme+S8CZE9D/x2rdfUMCC7j2DQkdYc8pzv
# eBorlDICwSSWUlIC0PPR/PKbOW6Z4R+OQ0F9mh5byV2ahPwm5ofzdHImraQb2T07
# alKgPAkeLx57szO0Rcf3rLGvk2Ctdq64shV464Nq6//bRqsk5e4C+pAfWcAvXda3
# XaRcELdyU/hBTsz6eBolSsr+hWJDYcO0N6qB0vTWOg+9jVl+MEfeK2vnIVAzX9Rn
# m9S4Z588J5kD/4VDjnMSyiDN6GHVsWbcF9Y5bQ/bzyM3oYKJThxrP9agzaoHnT5C
# JqrXDO76R78aUn7RdYHTyYpiF21PiKAhoCY+r23ZYjAf6Zgorm6N1Y5McmaTgI0q
# 41XHYGeQQlZcIlEPs9xOOe5N3dkdeBBUO27Ql28DtR6yI3PGErKaZND8lYUkqP/f
# obDckUCu3wkzq7ndkrfxzJF0O2nrZ5cbkL/nx6BvcbtXv7ePWu16QGoWzYCELS/h
# AtQklEOzFfwMKxv9cW/8y7x1Fzpeg9LJsy8b1ZyNf1T+fn7kVqOHp53hWVKUQY9t
# W76GlZr/GnbdQNJRSnC0HzNjI3c/7CceWeQIh+00gkoPP/6gHcH1Z3NFhnj0qinp
# J4fGGdvGExTDOUmHTaCX4GUT9Z13Vunas1jHOvLAzYIwggboMIIE0KADAgECAhB3
# vQ4Ft1kLth1HYVMeP3XtMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNVBAYTAkJFMRkw
# FwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIENv
# ZGUgU2lnbmluZyBSb290IFI0NTAeFw0yMDA3MjgwMDAwMDBaFw0zMDA3MjgwMDAw
# MDBaMFwxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTIw
# MAYDVQQDEylHbG9iYWxTaWduIEdDQyBSNDUgRVYgQ29kZVNpZ25pbmcgQ0EgMjAy
# MDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMsg75ceuQEyQ6BbqYoj
# /SBerjgSi8os1P9B2BpV1BlTt/2jF+d6OVzA984Ro/ml7QH6tbqT76+T3PjisxlM
# g7BKRFAEeIQQaqTWlpCOgfh8qy+1o1cz0lh7lA5tD6WRJiqzg09ysYp7ZJLQ8LRV
# X5YLEeWatSyyEc8lG31RK5gfSaNf+BOeNbgDAtqkEy+FSu/EL3AOwdTMMxLsvUCV
# 0xHK5s2zBZzIU+tS13hMUQGSgt4T8weOdLqEgJ/SpBUO6K/r94n233Hw0b6nskEz
# IHXMsdXtHQcZxOsmd/KrbReTSam35sOQnMa47MzJe5pexcUkk2NvfhCLYc+YVaMk
# oog28vmfvpMusgafJsAMAVYS4bKKnw4e3JiLLs/a4ok0ph8moKiueG3soYgVPMLq
# 7rfYrWGlr3A2onmO3A1zwPHkLKuU7FgGOTZI1jta6CLOdA6vLPEV2tG0leis1Ult
# 5a/dm2tjIF2OfjuyQ9hiOpTlzbSYszcZJBJyc6sEsAnchebUIgTvQCodLm3HadNu
# twFsDeCXpxbmJouI9wNEhl9iZ0y1pzeoVdwDNoxuz202JvEOj7A9ccDhMqeC5LYy
# AjIwfLWTyCH9PIjmaWP47nXJi8Kr77o6/elev7YR8b7wPcoyPm593g9+m5XEEofn
# GrhO7izB36Fl6CSDySrC/blTAgMBAAGjggGtMIIBqTAOBgNVHQ8BAf8EBAMCAYYw
# EwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUJZ3Q/FkJhmPF7POxEztXHAOSNhEwHwYDVR0jBBgwFoAUHwC/RoAK/Hg5t6W0
# Q9lWULvOljswgZMGCCsGAQUFBwEBBIGGMIGDMDkGCCsGAQUFBzABhi1odHRwOi8v
# b2NzcC5nbG9iYWxzaWduLmNvbS9jb2Rlc2lnbmluZ3Jvb3RyNDUwRgYIKwYBBQUH
# MAKGOmh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2NvZGVzaWdu
# aW5ncm9vdHI0NS5jcnQwQQYDVR0fBDowODA2oDSgMoYwaHR0cDovL2NybC5nbG9i
# YWxzaWduLmNvbS9jb2Rlc2lnbmluZ3Jvb3RyNDUuY3JsMFUGA1UdIAROMEwwQQYJ
# KwYBBAGgMgECMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24u
# Y29tL3JlcG9zaXRvcnkvMAcGBWeBDAEDMA0GCSqGSIb3DQEBCwUAA4ICAQAldaAJ
# yTm6t6E5iS8Yn6vW6x1L6JR8DQdomxyd73G2F2prAk+zP4ZFh8xlm0zjWAYCImbV
# YQLFY4/UovG2XiULd5bpzXFAM4gp7O7zom28TbU+BkvJczPKCBQtPUzosLp1pnQt
# pFg6bBNJ+KUVChSWhbFqaDQlQq+WVvQQ+iR98StywRbha+vmqZjHPlr00Bid/XSX
# hndGKj0jfShziq7vKxuav2xTpxSePIdxwF6OyPvTKpIz6ldNXgdeysEYrIEtGiH6
# bs+XYXvfcXo6ymP31TBENzL+u0OF3Lr8psozGSt3bdvLBfB+X3Uuora/Nao2Y8nO
# ZNm9/Lws80lWAMgSK8YnuzevV+/Ezx4pxPTiLc4qYc9X7fUKQOL1GNYe6ZAvytOH
# X5OKSBoRHeU3hZ8uZmKaXoFOlaxVV0PcU4slfjxhD4oLuvU/pteO9wRWXiG7n9dq
# cYC/lt5yA9jYIivzJxZPOOhRQAyuku++PX33gMZMNleElaeEFUgwDlInCI2Oor0i
# xxnJpsoOqHo222q6YV8RJJWk4o5o7hmpSZle0LQ0vdb5QMcQlzFSOTUpEYck08T7
# qWPLd0jV+mL8JOAEek7Q5G7ezp44UCb0IXFl1wkl1MkHAHq4x/N36MXU4lXQ0x72
# f1LiSY25EXIMiEQmM2YBRN/kMw4h3mKJSAfa9TCCB7MwggWboAMCAQICDEwWQjHr
# NX4fj8JFKDANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQ
# R2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFsU2lnbiBHQ0MgUjQ1IEVW
# IENvZGVTaWduaW5nIENBIDIwMjAwHhcNMjUxMjA0MjEwMjI0WhcNMjgxMjA0MjEw
# MjI0WjCB8zEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xEzARBgNVBAUT
# CjExNjI1NDQ2ODkxEzARBgsrBgEEAYI3PAIBAxMCQ0ExFzAVBgsrBgEEAYI3PAIB
# AhMGUXVlYmVjMQswCQYDVQQGEwJDQTEPMA0GA1UECBMGUXVlYmVjMRIwEAYDVQQH
# EwlMYXZhbHRyaWUxGTAXBgNVBAoTEERFVk9MVVRJT05TIElOQy4xGTAXBgNVBAMT
# EERFVk9MVVRJT05TIElOQy4xJzAlBgkqhkiG9w0BCQEWGHNlY3VyaXR5QGRldm9s
# dXRpb25zLm5ldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvGSmps
# CDxiV4fapsFPCVhNMpfdlgrzFhCiXnX0Gy7Zu2e7cDMl2RNo0H8hpKNHhSODkguW
# YNLZfmM+bpvE2FbLN0oYxUmlEHiBZWN8+0dq10ybkfQZp+O+95S3mX3ecdKTBhPY
# B7I5HYNWBPR1Au86Y68x+LBaTtM3AY/zvwB2gMbefZsKAa9L9np1l7UjN6qfZskQ
# G+zRxCj+88Cg6srkCYyNu/LMnFtWInbbxuqkh1ddTTAmVVSUOXXn+vKtUE2hTw/2
# 5x3psbHLLgcLbq0E4ZwzmRPB2ps972aouEaMkPhYXO4KTtTZFj6GoYy2sAUaGxgP
# gADEeZY/guhqSBANnn2Wa6jaocgMvvNhSJd7aQfldvps0ZQyerVra7zgoqT0JkyL
# NHm9AKiNV+mJQHqirXxL+Y70vO4uFJVukyvceNO+C+y0Re77pb8BntRfuSOdcySj
# 1v/NQ7eHAV6/zY21xlYswIz7uysoHQeZEMWlEX8bjJNjDvkT1BUF9j655hVeyxpZ
# zvaSgLdpdpZVSOQzSkdiH0KOhBiQ9R5Ppa2/W9qhdLNi+sKjr1/xEKSOd1fFG56R
# iN4ems9YdEQR9KrnGrVpQQ0QKSutqFiWUlUqCgWuDwTPe2V+UvHa2xAlZlbYAzGf
# X8c6OIniFLjWcxXKV21R7VP8UfQxrf38PapJAgMBAAGjggHbMIIB1zAOBgNVHQ8B
# Af8EBAMCB4AwgZ8GCCsGAQUFBwEBBIGSMIGPMEwGCCsGAQUFBzAChkBodHRwOi8v
# c2VjdXJlLmdsb2JhbHNpZ24uY29tL2NhY2VydC9nc2djY3I0NWV2Y29kZXNpZ25j
# YTIwMjAuY3J0MD8GCCsGAQUFBzABhjNodHRwOi8vb2NzcC5nbG9iYWxzaWduLmNv
# bS9nc2djY3I0NWV2Y29kZXNpZ25jYTIwMjAwVQYDVR0gBE4wTDBBBgkrBgEEAaAy
# AQIwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xvYmFsc2lnbi5jb20vcmVw
# b3NpdG9yeS8wBwYFZ4EMAQMwCQYDVR0TBAIwADBHBgNVHR8EQDA+MDygOqA4hjZo
# dHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL2dzZ2NjcjQ1ZXZjb2Rlc2lnbmNhMjAy
# MC5jcmwwIwYDVR0RBBwwGoEYc2VjdXJpdHlAZGV2b2x1dGlvbnMubmV0MBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMB8GA1UdIwQYMBaAFCWd0PxZCYZjxezzsRM7VxwDkjYR
# MB0GA1UdDgQWBBQc9AV01XqvHzkkeRDj0FeokGPnwDANBgkqhkiG9w0BAQsFAAOC
# AgEAuuy64I5XgYWoKznXOKzBMUz4NTGnv1cN8ovX9pJlVUfeIhipffE6WZH9weHZ
# +3OheToB4v8JWEobOBfZbz/8FI20WiXgtPvqU4T/ju8ZnVUv+XidlTdTRn6AzXjy
# kpyJaLXMJA4BMvl5ISxUVYgnScOpP6hAqWsPDyp5CVOLmYWzZNKWvfBGMiEIwC+h
# M+mkqKBnDu1V0tU9QDbqXgBqwBp1BKxWg5bxMYCNkC15WwqP8VVXRk/354x1Ua87
# LHGOSHBIobnySRbK+onnGESqKeVCzqL5DvTVBGZyjRARcYU0mY51raBnJ5/Yad3y
# 7R+eSIuUjh35JNXwYAHYlWXYsVzfaH4OgFSaSkJcL0ieH4MT2xT8kITOb+9KElvc
# YyFlxWhgIPW/vvY6vqU5aMlMCTcIWTZBbzp9dwCNsAYhhq4Okn5J1e5j+baIT58N
# gOhsZjsME6VO1sYj1zUqKJnDza8vViZO7kKaHXtg6w2CgvfPNKO+tZncWEX3YIEa
# dpt/t0UFlJ5mAWtlIzxf+79A/a8L7JWxcwNcZ1zUvy/mj+b/0G1S3rPCtsh7BaUs
# sMVcHRzuanz+zMMBcQcdwxwyM1U5fnyFjBMbvsAhRJB6c68m1IbB8rM9QNrjZX4Y
# 6YTxz1UOCMk2DusI+QHYIlidzJl16tYD3DlF5xefxyQcdEsxghqYMIIalAIBATBs
# MFwxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTIwMAYD
# VQQDEylHbG9iYWxTaWduIEdDQyBSNDUgRVYgQ29kZVNpZ25pbmcgQ0EgMjAyMAIM
# TBZCMes1fh+PwkUoMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKHjOxbaeVyB8JhscFj/
# l/PY9bAX8Y7da5kR8riKsxSuMA0GCSqGSIb3DQEBAQUABIICAIy6ror95ILt+AVV
# S2VWsp6/1CmR3KFmj/vGRZRHNLeC7tuvxRdi8thS2P3G0PdPsodNTe9lkdb2Aw2r
# +OXAWqNYOcJ+5UJ/U/k4yPi42+X4OxPN/7uM9cXVWXL75v1WLCPzKprz4mlaqkX8
# 0OjoxonMDeOFoWYLJ0FL/HG6Sbfrl3+0RifCh2R9ZDiN419jlR+yZmSVfl/dWA3z
# gz2ur2t29UHqP738kSq4WH6FF8AxJm/QknwAQzrDSkI8TEvFUHcdReu8q8Cx0sl5
# SZOYQwLOQZF6GdDJJ7OGausqru6e1uCdXvkNPd/Vg7QZHe8KyKwDQAOvD6C7RFll
# mh8DNhSXYw8L9mU1+yBUZQOc4stzqA662T+yfxQ1I1Q97NeB1AAxFf3YN4kOQZOl
# tmIcbFXKs9HtCX2MdF/R/5sISib9NUQpA8Vx44qA+h2MI3y6aWviA+Ywmn5+mNKh
# O36s81CpCWyZ0sMDem6Q4/GVsJR2HqEeRZ0YqSGmyhbba1/+l/DtM3Q9uboxA8tj
# fE2JCsb8sMisq0Pdb3yRKK0qq69lnGR0H2duV27XIVlHf3wEYSOhg56+Zox83C3A
# WhIILqrafXcmcXywBksFuCgQ6I8q39cgUoU6UMYEGyh6qBhEFH+E53PZw+j9wDyC
# y1nQcNniYnDnkpXmAXaF/qV9oR4OoYIXdjCCF3IGCisGAQQBgjcDAwExghdiMIIX
# XgYJKoZIhvcNAQcCoIIXTzCCF0sCAQMxDzANBglghkgBZQMEAgEFADB3BgsqhkiG
# 9w0BCRABBKBoBGYwZAIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIHDV
# 7U5XpMtTln0IpbW+ICipM/+zps7ZbAJJ63H1GQz8AhBPZwuJe4xrExo5VEyfxz5X
# GA8yMDI2MTAxMDAxMjI1N1qgghM6MIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P
# +ZIYNTANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0
# YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoX
# DTM3MTEwNDIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3Rh
# bXAgUmVzcG9uZGVyIDIwMjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBALZ7pvLJ/s1K+NSbTGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fc
# pKXlgRiky4WAawDfU3YP0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0
# ASURML5uFIS80sZsDwO3BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGE
# XiMlneJIC5/YKuizVzNFKZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXe
# Ig283hw7TmmsZXR+SMbjbrEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPA
# o9b75Zr4Ta6Mj3URm26QZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0e
# eOzmZ6QTFg33otX+bFRsIolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1Zj
# kEyZtJrLl5ya0m5doH/ScpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsn
# b/ItVKi/2nXpQNMu1PT5prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQ
# QP9u3Ai9t+5eQ02GDwy0ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklk
# ZQvw3+kv3vUPItdwroczk9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIB
# kTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAf
# BgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4Aw
# FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsG
# AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0
# dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVT
# dGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQ
# hk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1l
# U3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+Z
# rAmAfn0oQLFr0mLywSLaDXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os
# 5ipDCy35RBCg2UMeApEtrfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H6
# 00u4D7RnOGf8Wj4UNgAdZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh
# /thn6hH23d+VsPAr6GAYyzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0Pxp
# awPtGRyaBVGzq+UiTfM8pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2
# oRVBm3JETjajHug2AL+v5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6
# WcwTwXmQ9lOCLLYcsQeSWmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkR
# YVnhA2D73FLje1O5b3HR5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZ
# M5dqO94lbUveOiJBI+xZEbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDb
# D6rgg39H5Mh7QfieAIjWagkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdY
# eeDRGCyatf8R1hSjzSvdN8yWQPT9gzCCBrQwggScoAMCAQICEA3HrFcF/yGZLkBD
# Igw6SYYwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp
# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMY
# RGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTI1MDUwNzAwMDAwMFoXDTM4MDEx
# NDIzNTk1OVowaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0
# MDk2IFNIQTI1NiAyMDI1IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBALR4MdMKmEFyvjxGwBysddujRmh0tFEXnU2tjQ2UtZmWgyxU7UNqEY81FzJs
# Qqr5G7A6c+Gh/qm8Xi4aPCOo2N8S9SLrC6Kbltqn7SWCWgzbNfiR+2fkHUiljNOq
# nIVD/gG3SYDEAd4dg2dDGpeZGKe+42DFUF0mR/vtLa4+gKPsYfwEu7EEbkC9+0F2
# w4QJLVSTEG8yAR2CQWIM1iI5PHg62IVwxKSpO0XaF9DPfNBKS7Zazch8NF5vp7ea
# Z2CVNxpqumzTCNSOxm+SAWSuIr21Qomb+zzQWKhxKTVVgtmUPAW35xUUFREmDrMx
# SNlr/NsJyUXzdtFUUt4aS4CEeIY8y9IaaGBpPNXKFifinT7zL2gdFpBP9qh8SdLn
# Eut/GcalNeJQ55IuwnKCgs+nrpuQNfVmUB5KlCX3ZA4x5HHKS+rqBvKWxdCyQEEG
# cbLe1b8Aw4wJkhU1JrPsFfxW1gaou30yZ46t4Y9F20HHfIY4/6vHespYMQmUiote
# 8ladjS/nJ0+k6MvqzfpzPDOy5y6gqztiT96Fv/9bH7mQyogxG9QEPHrPV6/7umw0
# 52AkyiLA6tQbZl1KhBtTasySkuJDpsZGKdlsjg4u70EwgWbVRSX1Wd4+zoFpp4Ra
# +MlKM2baoD6x0VR4RjSpWM8o5a6D8bpfm4CLKczsG7ZrIGNTAgMBAAGjggFdMIIB
# WTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTvb1NK6eQGfHrK4pBW9i/U
# SezLTjAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8E
# BAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsG
# AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0
# dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQu
# Y3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsG
# CWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAF877FoAc/gc9EXZxML2+C8i1
# NKZ/zdCHxYgaMH9Pw5tcBnPw6O6FTGNpoV2V4wzSUGvI9NAzaoQk97frPBtIj+ZL
# zdp+yXdhOP4hCFATuNT+ReOPK0mCefSG+tXqGpYZ3essBS3q8nL2UwM+NMvEuBd/
# 2vmdYxDCvwzJv2sRUoKEfJ+nN57mQfQXwcAEGCvRR2qKtntujB71WPYAgwPyWLKu
# 6RnaID/B0ba2H3LUiwDRAXx1Neq9ydOal95CHfmTnM4I+ZI2rVQfjXQA1WSjjf4J
# 2a7jLzWGNqNX+DF0SQzHU0pTi4dBwp9nEC8EAqoxW6q17r0z0noDjs6+BFo+z7bK
# SBwZXTRNivYuve3L2oiKNqetRHdqfMTCW/NmKLJ9M+MtucVGyOxiDf06VXxyKkOi
# rv6o02OoXN4bFzK0vlNMsvhlqgF2puE6FndlENSmE+9JGYxOGLS/D284NHNboDGc
# mWXfwXRy4kbu4QFhOm0xJuF2EZAOk5eCkhSxZON3rGlHqhpB/8MluDezooIs8CVn
# rpHMiD2wL40mm53+/j7tFaxYKIqL0Q4ssd8xHZnIn/7GELH3IdvG2XlM9q7WP/Uw
# gOkw/HQtyRN62JK4S1C8uw3PdBunvAZapsiI5YKdvlarEvf8EA+8hcpSM9LHJmyr
# xaFtoza2zNaQ9k+5t1wwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0G
# CSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0
# IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5
# NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNV
# BAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQg
# Um9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvk
# XUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdt
# HauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu
# 34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0
# QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2
# kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM
# 1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmI
# dph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZ
# K37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72
# gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqs
# X40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyh
# HsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8E
# BTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAW
# gBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUH
# AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI
# KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz
# c3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAE
# CjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX
# 979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offy
# ct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3
# J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0
# d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6ts
# ds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQx
# ggN8MIIDeAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg
# UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mSGDUwDQYJYIZI
# AWUDBAIBBQCggdEwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3
# DQEJBTEPFw0yNjEwMTAwMTIyNTdaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFFHZ
# q9oDSXPYT0JmrKSCSOazacQ5MC8GCSqGSIb3DQEJBDEiBCAczJCliBVhHRKgMWQp
# Xe7wfJ2sTkVmxPGaQhEwva0jiTA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCAtoJ2n
# 9BMfn+cttsXm6cllZ1WvBD8ep0LMDSEg4UHr/DANBgkqhkiG9w0BAQEFAASCAgAo
# q0Wj6AfuSN+sRfeva33nrE9DIfHY72cx9EPs6Ff10DZ9buvmAAPYj7YzrRAp4V4i
# gRKMiHM504fBgHK0lW/k/e/8AZZm5UxgI5a71EC3yKRnuzI9pHTXNdaD4Nw9eOTf
# +Z9GPk0BZpBDo0G0aRB437w4qmu11BRC2j5Wh8B2jOeSb7rQmFm2Z5vmlLfL1Jq0
# 91hbAAKpd+HEDIPFUqOWH/VCvgGf6BrmTIgNBTxecb98PgO2ElrlyYoIorxrVtXq
# a6KIhfuasMWfNA4IJWGew32wEuOjxTVH7b0BU+6pLUKNm9eFY+P5OOQ5D9yg1awg
# V0lrDT1/WfrJIcccmL0weK4JHdQFS/hWxBtdYZ1u/Bu0royRf5FVCMolzk/qac0y
# EpQ1TRrvGzoD2d03mI2TRPzoYzNXdTnps0uyBc7ngaMownQvrrcccF5e0cpFMQj7
# rkqFa4e49DiMQ4nXF3CHLXKgflNTgdnTkIBn/qdi4DQ8hFYqTzk04gsb0/aPzi87
# Qd6mOh9wmiW5fhtNN75Ii1HTWy5/ROket0gL37oM8wS0n3lnPGw9qXELwjDkAqxi
# QqnXPuYLhyRzuBKBsZNDVKwG8gt3CCpGUpYIkbm6qrd3Q8yAcCeWoD0KtbLqbqYl
# UPzpCuA1GsylY5Yw3JiHzpqpt11AECm7i6osICAqKg==
# SIG # End signature block