From 51c4d9cee3c989fac19f37ee007abac97767c1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 21 Oct 2023 01:45:17 -0400 Subject: [PATCH] feat(pwsh): verbosity profile, ngrok tunnel configuration (#577) --- .../DevolutionsGateway.psd1 | 1 + .../DevolutionsGateway/Public/DGateway.ps1 | 152 +++++++++++++++++- powershell/pester/Config.Tests.ps1 | 54 +++++++ powershell/pester/ImportCertificate.Tests.ps1 | 13 +- powershell/run-tests.ps1 | 3 +- 5 files changed, 210 insertions(+), 13 deletions(-) diff --git a/powershell/DevolutionsGateway/DevolutionsGateway.psd1 b/powershell/DevolutionsGateway/DevolutionsGateway.psd1 index e2c039092..93c349b33 100644 --- a/powershell/DevolutionsGateway/DevolutionsGateway.psd1 +++ b/powershell/DevolutionsGateway/DevolutionsGateway.psd1 @@ -67,6 +67,7 @@ FunctionsToExport = @( 'Find-DGatewayConfig', 'Enter-DGatewayConfig', 'Exit-DGatewayConfig', 'Set-DGatewayConfig', 'Get-DGatewayConfig', + 'New-DGatewayNgrokConfig', 'New-DGatewayNgrokTunnel', 'Set-DGatewayHostname', 'Get-DGatewayHostname', 'New-DGatewayListener', 'Get-DGatewayListeners', 'Set-DGatewayListeners', 'Get-DGatewayPath', 'Get-DGatewayRecordingPath', diff --git a/powershell/DevolutionsGateway/Public/DGateway.ps1 b/powershell/DevolutionsGateway/Public/DGateway.ps1 index 079939119..c58baf285 100644 --- a/powershell/DevolutionsGateway/Public/DGateway.ps1 +++ b/powershell/DevolutionsGateway/Public/DGateway.ps1 @@ -142,6 +142,125 @@ class DGatewaySubscriber { } } +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 +} + +enum VerbosityProfile { + Default + Debug + Tls + All + Quiet +} + class DGatewayConfig { [System.Nullable[Guid]] $Id [string] $Hostname @@ -159,7 +278,28 @@ class DGatewayConfig { [DGatewayListener[]] $Listeners [DGatewaySubscriber] $Subscriber + [DGatewayNgrokConfig] $Ngrok + [string] $LogDirective + [string] $VerbosityProfile +} + +Function Remove-NullObjectProperties { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline, Mandatory)] + [object[]] $InputObject + ) + process { + foreach ($OldObj in $InputObject) { + $NonNullProperties = $OldObj.PSObject.Properties.Name.Where( { -Not [string]::IsNullOrEmpty($OldObj.$_) }) + $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 { @@ -172,10 +312,8 @@ function Save-DGatewayConfig { $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName - - $Properties = $Config.PSObject.Properties.Name - $NonNullProperties = $Properties.Where( { -Not [string]::IsNullOrEmpty($Config.$_) }) - $ConfigData = $Config | Select-Object $NonNullProperties | ConvertTo-Json + $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)) } @@ -203,7 +341,11 @@ function Set-DGatewayConfig { [string] $DelegationPublicKeyFile, [string] $DelegationPrivateKeyFile, - [DGatewaySubProvisionerKey] $SubProvisionerPublicKey + [DGatewaySubProvisionerKey] $SubProvisionerPublicKey, + + [DGatewayNgrokConfig] $Ngrok, + + [VerbosityProfile] $VerbosityProfile ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath diff --git a/powershell/pester/Config.Tests.ps1 b/powershell/pester/Config.Tests.ps1 index f9a4ca033..a1d8d730d 100644 --- a/powershell/pester/Config.Tests.ps1 +++ b/powershell/pester/Config.Tests.ps1 @@ -8,6 +8,7 @@ Describe 'Devolutions Gateway config' { Context 'Fresh environment' { It 'Creates basic configuration' { + Remove-Item (Join-Path $ConfigPath 'gateway.json') -ErrorAction SilentlyContinue | Out-Null Set-DGatewayConfig -ConfigPath:$ConfigPath -Hostname 'gateway.local' $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Hostname | Should -Be 'gateway.local' } @@ -43,6 +44,59 @@ Describe 'Devolutions Gateway config' { Set-DGatewayConfig -ConfigPath:$ConfigPath -RecordingPath $RecordingPath $(Get-DGatewayConfig -ConfigPath:$ConfigPath).RecordingPath | Should -Be $RecordingPath } + + It 'Sets log verbosity profile' { + Set-DGatewayConfig -ConfigPath:$ConfigPath -VerbosityProfile 'All' + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).VerbosityProfile | Should -Be 'All' + Set-DGatewayConfig -ConfigPath:$ConfigPath -VerbosityProfile 'Default' + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).VerbosityProfile | Should -Be 'Default' + Set-DGatewayConfig -ConfigPath:$ConfigPath -VerbosityProfile 'Tls' + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).VerbosityProfile | Should -Be 'Tls' + { Set-DGatewayConfig -ConfigPath:$ConfigPath -VerbosityProfile 'yolo' } | Should -Throw + } + + It 'Sets ngrok configuration' { + $AuthToken = '4nq9771bPxe8ctg7LKr_2ClH7Y15Zqe4bWLWF9p' + $Metadata = '{"serial": "00012xa-33rUtz9", "comment": "For customer alan@example.com"}' + $HeartbeatInterval = 15 + $HeartbeatTolerance = 5 + $ngrok = New-DGatewayNgrokConfig -AuthToken $AuthToken + $ngrok.Metadata = $Metadata + $ngrok.HeartbeatInterval = $HeartbeatInterval + $ngrok.HeartbeatTolerance = $HeartbeatTolerance + $httpTunnelParams = @{ + Http = $true + Metadata = "c6481452-6f5d-11ee-b962-0242ac120002" + AllowCidrs = @("0.0.0.0/0") + Domain = "gateway.ngrok.io" + CircuitBreaker = 0.5 + Compression = $true + } + $tcpTunnelParams = @{ + Tcp = $true + AllowCidrs = @("0.0.0.0/0") + RemoteAddr = "7.tcp.ngrok.io:20560" + } + $httpTunnel = New-DGatewayNgrokTunnel @httpTunnelParams + $tcpTunnel = New-DGatewayNgrokTunnel @tcpTunnelParams + $ngrok.Tunnels = [PSCustomObject]@{ + "http-endpoint" = $httpTunnel + "tcp-endpoint" = $tcpTunnel + } + Set-DGatewayConfig -ConfigPath:$ConfigPath -Ngrok $ngrok + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Ngrok.AuthToken | Should -Be $AuthToken + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Ngrok.Metadata | Should -Be $Metadata + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Ngrok.HeartbeatInterval | Should -Be $HeartbeatInterval + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Ngrok.HeartbeatTolerance | Should -Be $HeartbeatTolerance + $Tunnels = $(Get-DGatewayConfig -ConfigPath:$ConfigPath).Ngrok.Tunnels + $Tunnels.'http-endpoint'.Proto | Should -Be 'http' + $Tunnels.'http-endpoint'.Domain | Should -Be $httpTunnel.Domain + $Tunnels.'http-endpoint'.Metadata | Should -Be $httpTunnel.Metadata + $Tunnels.'http-endpoint'.AllowCidrs | Should -Be $httpTunnel.AllowCidrs + $Tunnels.'tcp-endpoint'.Proto | Should -Be 'tcp' + $Tunnels.'tcp-endpoint'.RemoteAddr | Should -Be $tcpTunnel.RemoteAddr + $Tunnels.'tcp-endpoint'.AllowCidrs | Should -Be $tcpTunnel.AllowCidrs + } } } } diff --git a/powershell/pester/ImportCertificate.Tests.ps1 b/powershell/pester/ImportCertificate.Tests.ps1 index 6ea0a1740..cc47a6256 100644 --- a/powershell/pester/ImportCertificate.Tests.ps1 +++ b/powershell/pester/ImportCertificate.Tests.ps1 @@ -5,17 +5,17 @@ Describe 'Devolutions Gateway certificate import' { BeforeAll { $DummyPrivateKey = Join-Path $TestDrive 'dummy.key' New-Item -Path $DummyPrivateKey -Value 'dummy' - $ConfigPath = Join-Path $TestDrive 'Gateway' } It 'Smoke' { - ForEach ($certFile in Get-ChildItem -Path ./ImportCertificate/WellOrdered) { + (Get-Item -Path ".\ImportCertificate\WellOrdered\*.crt") | ForEach-Object { + $CertFile = $_.FullName $expected = Get-Content -Path $certFile Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $certFile -PrivateKeyFile $DummyPrivateKey - $resultingFile = Join-Path $TestDrive 'Gateway' 'server.crt' + $resultingFile = Join-Path $ConfigPath 'server.crt' $result = Get-Content -Path $resultingFile $result | Should -Be $expected @@ -23,13 +23,14 @@ Describe 'Devolutions Gateway certificate import' { } It 'Sorting' { - ForEach ($unorderedCertFile in Get-ChildItem -Path ./ImportCertificate/Unordered) { - $wellOrderedCertFile = Join-Path './ImportCertificate/WellOrdered' $unorderedCertFile.Name + (Get-Item -Path ".\ImportCertificate\Unordered\*.crt") | ForEach-Object { + $unorderedCertFile = $_.FullName + $wellOrderedCertFile = $_.FullName -Replace 'Unordered', 'WellOrdered' $expected = Get-Content -Path $wellOrderedCertFile Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $unorderedCertFile -PrivateKeyFile $DummyPrivateKey - $resultingFile = Join-Path $TestDrive 'Gateway' 'server.crt' + $resultingFile = Join-Path $ConfigPath 'server.crt' $result = Get-Content -Path $ResultingFile $result | Should -Be $expected diff --git a/powershell/run-tests.ps1 b/powershell/run-tests.ps1 index b3a529358..053d3ef25 100755 --- a/powershell/run-tests.ps1 +++ b/powershell/run-tests.ps1 @@ -7,8 +7,7 @@ Import-Module Pester Push-Location -Path $(Join-Path $PSScriptRoot 'pester') try { - Invoke-Pester . + Invoke-Pester . -Show All } finally { Pop-Location } -