89 lines
5.5 KiB
PowerShell
89 lines
5.5 KiB
PowerShell
# 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)
|
|
}
|
|
}
|