# Pure catalog/registry-plan functions. Compatible with Windows PowerShell 5.1. Set-StrictMode -Version 2 function Get-PanelField($Record, [string]$Name, $Default = '') { $property = $Record.PSObject.Properties[$Name] if ($null -eq $property -or $null -eq $property.Value) { return $Default } return $property.Value } function ConvertTo-PanelSession($Record) { $role = [string](Get-PanelField $Record 'Role') if ($role -cnotmatch '^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$') { throw 'ID: use 1-48 Latin letters, digits, hyphen or underscore.' } $name = 'KAgent-' + $role + '-panel' $supplied = [string](Get-PanelField $Record 'Name' $name) if ($supplied -cne $name) { throw 'Session name must match its ID.' } $label = [string](Get-PanelField $Record 'Label' $role) if ([string]::IsNullOrWhiteSpace($label) -or $label.Length -gt 120 -or $label -match '[\x00-\x1f]') { throw 'Invalid display label.' } $serverAddress = [string](Get-PanelField $Record 'HostName') $address = $null if ([Net.IPAddress]::TryParse($serverAddress, [ref]$address)) { $serverAddress = $address.ToString() } elseif ($serverAddress.Length -gt 253 -or $serverAddress -cnotmatch '^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$') { throw 'Enter a server IP or DNS name, without a URL, path or port.' } $userName = [string](Get-PanelField $Record 'UserName') if ($userName -cnotmatch '^[a-z_][a-z0-9_-]{0,31}$') { throw 'Invalid SSH user name.' } $ports = @{} foreach ($field in @('PortNumber', 'LocalPort', 'RemotePort')) { $value = [string](Get-PanelField $Record $field) if ($value -cnotmatch '^\d{1,5}$' -or [int]$value -lt 1 -or [int]$value -gt 65535) { throw ('Invalid port: ' + $field) } $ports[$field] = [int]$value } $fingerprint = [string](Get-PanelField $Record 'Fingerprint') if ($fingerprint -cnotmatch '^SHA256:[A-Za-z0-9+/]{43}$') { throw 'Enter the verified SHA256 SSH host-key fingerprint.' } $panelPath = [string](Get-PanelField $Record 'PanelPath' '/') if ($panelPath -cnotmatch '^/[A-Za-z0-9._~/-]*$' -or $panelPath.Contains('//') -or @($panelPath.Split('/') | Where-Object { $_ -eq '.' -or $_ -eq '..' }).Count -gt 0) { throw 'Panel path must start with / and contain safe path characters only.' } return [pscustomobject][ordered]@{ Role=$role; Name=$name; Label=$label; HostName=$serverAddress; UserName=$userName PortNumber=$ports.PortNumber; LocalPort=$ports.LocalPort; RemotePort=$ports.RemotePort Fingerprint=$fingerprint; PanelPath=$panelPath } } function ConvertTo-PanelCatalog($Value) { $version = Get-PanelField $Value 'SchemaVersion' 1 if ([string]$version -cne '1') { throw 'Unsupported catalog schema version.' } if ($null -eq $Value.PSObject.Properties['Sessions']) { throw 'Catalog must contain Sessions.' } $sessions = @(); $names = @{}; $ports = @{} foreach ($record in @($Value.Sessions)) { $entry = ConvertTo-PanelSession $record if ($names.ContainsKey($entry.Name)) { throw ('Duplicate ID: ' + $entry.Role) } if ($ports.ContainsKey([string]$entry.LocalPort)) { throw ('Duplicate local port: ' + $entry.LocalPort) } $names[$entry.Name] = $true; $ports[[string]$entry.LocalPort] = $true; $sessions += $entry } return [pscustomobject]@{SchemaVersion=1; Sessions=@($sessions)} } function Read-PanelCatalog([string]$Path) { return ConvertTo-PanelCatalog ([IO.File]::ReadAllText($Path) | ConvertFrom-Json) } function Write-PanelCatalog([string]$Path, $Value) { $catalog = ConvertTo-PanelCatalog $Value $temporary = $Path + '.' + [Guid]::NewGuid().ToString('N') + '.tmp' try { [IO.File]::WriteAllText($temporary, ($catalog | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false)) if ([IO.File]::Exists($Path)) { [IO.File]::Replace($temporary, $Path, ($Path + ".previous")) } else { [IO.File]::Move($temporary, $Path) } } finally { if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) } } } function Select-PanelSessions($Catalog, [string[]]$Names) { $catalogData = ConvertTo-PanelCatalog $Catalog if (@($Names).Count -eq 0) { throw 'Select at least one server.' } $result = @(); $seen = @{} foreach ($name in $Names) { if ($seen.ContainsKey($name)) { throw 'Duplicate selection.' } $matches = @($catalogData.Sessions | Where-Object { $_.Name -ceq $name }) if ($matches.Count -ne 1) { throw 'Selected server does not exist in the catalog.' } $seen[$name] = $true; $result += $matches[0] } return $result } function Get-PanelRegistryPlan($Record, [string]$KeyPath) { $entry = ConvertTo-PanelSession $Record if ([string]::IsNullOrWhiteSpace($KeyPath) -or $KeyPath -match '[\x00-\x1f]') { throw 'Select your existing private key file.' } return [pscustomobject]@{ Strings=@{HostName=$entry.HostName; Protocol='ssh'; UserName=$entry.UserName; PublicKeyFile=$KeyPath; SSHManualHostKeys=$entry.Fingerprint; PortForwardings=('L127.0.0.1:' + $entry.LocalPort + '=127.0.0.1:' + $entry.RemotePort)} Numbers=@{PortNumber=$entry.PortNumber; LocalPortAcceptAll=0; RemotePortAcceptAll=0; SshNoShell=1; PingInterval=0; PingIntervalSecs=30; TCPKeepalives=1; AgentFwd=0; GssapiFwd=0; ProxyMethod=0; LogType=0} Arguments=('-load ' + $entry.Name + ' -N -hostkey ' + $entry.Fingerprint) PanelUrl=('http://127.0.0.1:' + $entry.LocalPort + $entry.PanelPath) } }