lightweight-server/windows/panels/Install.ps1

169 lines
13 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

param([string]$SourceDir = $PSScriptRoot)
$ErrorActionPreference = 'Stop'
. ([scriptblock]::Create([IO.File]::ReadAllText((Join-Path $SourceDir 'Catalog.ps1'))))
if ($env:OS -ne 'Windows_NT') { throw 'Run Install.cmd on Windows.' }
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[Windows.Forms.Application]::EnableVisualStyles()
function Show-PanelError($Exception) {
[Windows.Forms.MessageBox]::Show([string]$Exception, '3x-ui panels', 'OK', 'Error') | Out-Null
}
function Protect-PanelDirectory([string]$Path) {
New-Item -ItemType Directory -Path $Path -Force | Out-Null
$acl = New-Object Security.AccessControl.DirectorySecurity
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
$acl.SetOwner($sid); $acl.SetAccessRuleProtection($true, $false)
$inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
foreach ($principal in @($sid, [Security.Principal.SecurityIdentifier]::new('S-1-5-18'))) {
$rule = [Security.AccessControl.FileSystemAccessRule]::new($principal, [Security.AccessControl.FileSystemRights]::FullControl, $inherit, [Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
$acl.AddAccessRule($rule)
}
Set-Acl -LiteralPath $Path -AclObject $acl
}
function Select-PanelFile([string]$Title, [string]$Filter) {
$dialog = New-Object Windows.Forms.OpenFileDialog
$dialog.Title=$Title; $dialog.Filter=$Filter; $dialog.CheckFileExists=$true
if ($dialog.ShowDialog() -ne 'OK') { return $null }
return $dialog.FileName
}
function Edit-PanelSession($Existing) {
$dialog = New-Object Windows.Forms.Form
$dialog.Text='Сервер 3x-ui'; $dialog.ClientSize=New-Object Drawing.Size(640,485)
$dialog.StartPosition='CenterParent'; $dialog.FormBorderStyle='FixedDialog'; $dialog.MaximizeBox=$false; $dialog.MinimizeBox=$false
$fields=@(
@('Role','ID ярлыка (например DK или DE2)',''), @('Label','Название в списке',''),
@('HostName','IP или DNS сервера',''), @('UserName','Пользователь SSH',''),
@('PortNumber','Порт SSH','22'), @('LocalPort','Свободный локальный порт','18055'),
@('RemotePort','Порт панели на сервере','2053'), @('PanelPath','Путь панели (например /secret/)','/'),
@('Fingerprint','SHA256 отпечаток ключа SSH','')
)
$boxes=@{}; $y=16
foreach ($field in $fields) {
$label=New-Object Windows.Forms.Label; $label.Text=$field[1]; $label.SetBounds(12,$y+4,265,22); $dialog.Controls.Add($label)
$box=New-Object Windows.Forms.TextBox; $box.SetBounds(280,$y,345,24)
$box.Text=[string](Get-PanelField $Existing $field[0] $field[2]); $boxes[$field[0]]=$box; $dialog.Controls.Add($box); $y+=40
}
$tip=New-Object Windows.Forms.Label; $tip.Text='Отпечаток проверьте в консоли сервера. Пароли и приватный ключ сюда не вводятся.'; $tip.SetBounds(12,380,610,38); $dialog.Controls.Add($tip)
$ok=New-Object Windows.Forms.Button; $ok.Text='Сохранить'; $ok.SetBounds(405,435,105,30)
$ok.Add_Click({
try {
$value=[ordered]@{}; foreach ($field in $fields) { $value[$field[0]]=$boxes[$field[0]].Text.Trim() }
$dialog.Tag=ConvertTo-PanelSession ([pscustomobject]$value); $dialog.DialogResult='OK'; $dialog.Close()
} catch { Show-PanelError $_.Exception.Message }
}); $dialog.Controls.Add($ok)
$cancel=New-Object Windows.Forms.Button; $cancel.Text='Отмена'; $cancel.SetBounds(520,435,105,30); $cancel.DialogResult='Cancel'; $dialog.Controls.Add($cancel)
$dialog.AcceptButton=$ok; $dialog.CancelButton=$cancel
if ($dialog.ShowDialog() -eq 'OK') { return $dialog.Tag }
return $null
}
function Install-PanelSelections($Catalog, [string[]]$Names) {
$selected=@(Select-PanelSessions $Catalog $Names)
$putty=$null; $found=Get-Command putty.exe -ErrorAction SilentlyContinue
if ($found) { $putty=$found.Source }
foreach ($folder in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
if ($folder -and -not $putty) {
$candidate=Join-Path $folder 'PuTTY\putty.exe'
if (Test-Path -LiteralPath $candidate -PathType Leaf) { $putty=$candidate }
}
}
if (-not $putty) { $putty=Select-PanelFile 'Выберите установленный putty.exe' 'PuTTY|putty.exe' }
if (-not $putty) { return }
$key=Select-PanelFile 'Выберите свой существующий приватный ключ PuTTY (.ppk)' 'PuTTY private key|*.ppk'
if (-not $key) { return }
$backup=Join-Path $script:DataDir ('backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [Guid]::NewGuid().ToString('N'))
Protect-PanelDirectory $backup
$desktop=[Environment]::GetFolderPath('Desktop'); $shell=New-Object -ComObject WScript.Shell
$completed=@()
try {
foreach ($entry in $selected) {
$plan=Get-PanelRegistryPlan $entry $key
$registry='HKCU:\Software\SimonTatham\PuTTY\Sessions\' + $entry.Name
$native='HKCU\Software\SimonTatham\PuTTY\Sessions\' + $entry.Name
if (Test-Path -LiteralPath $registry) {
& reg.exe export $native (Join-Path $backup ($entry.Role + '-previous.reg')) /y | Out-Null
if ($LASTEXITCODE -ne 0) { throw ('Не удалось сохранить прежнюю сессию ' + $entry.Role) }
}
$shortcutPath=Join-Path $desktop ('3x-ui ' + $entry.Role + '.lnk')
$bookmarkPath=Join-Path $desktop ('Панель ' + $entry.Role + '.url')
foreach ($path in @($shortcutPath,$bookmarkPath)) { if (Test-Path -LiteralPath $path) { Copy-Item -LiteralPath $path -Destination $backup } }
New-Item -Path $registry -Force | Out-Null
foreach ($name in $plan.Strings.Keys) { New-ItemProperty -Path $registry -Name $name -Value $plan.Strings[$name] -PropertyType String -Force | Out-Null }
foreach ($name in $plan.Numbers.Keys) { New-ItemProperty -Path $registry -Name $name -Value $plan.Numbers[$name] -PropertyType DWord -Force | Out-Null }
$shortcut=$shell.CreateShortcut($shortcutPath); $shortcut.TargetPath=$putty; $shortcut.Arguments=$plan.Arguments
$shortcut.WorkingDirectory=Split-Path -Parent $putty; $shortcut.IconLocation=$putty + ',0'; $shortcut.Description=$entry.Label + ' — SSH-туннель панели'; $shortcut.Save()
[IO.File]::WriteAllText($bookmarkPath, "[InternetShortcut]`r`nURL=" + $plan.PanelUrl + "`r`n", [Text.Encoding]::ASCII)
$completed += $entry.Role
}
} catch {
throw ('Установка остановлена: ' + $_.Exception.Message + "`r`nУстановлены: " + ($completed -join ', ') + "`r`nРезервные копии: " + $backup)
}
[Windows.Forms.MessageBox]::Show("Готово: " + ($completed -join ', ') + ".`r`nОткройте ярлык 3x-ui, оставьте PuTTY работающим и откройте ярлык Панель.`r`nПароль панели вводится в браузере.", '3x-ui panels') | Out-Null
}
$script:DataDir=Join-Path $env:LOCALAPPDATA 'KAgentPanels'
Protect-PanelDirectory $script:DataDir
$script:CatalogPath=Join-Path $script:DataDir 'catalog.json'
$sourceCatalog=Join-Path $SourceDir 'connections.json'
$script:Catalog=[pscustomobject]@{SchemaVersion=1; Sessions=@()}
try {
if (Test-Path -LiteralPath $script:CatalogPath) { $script:Catalog=Read-PanelCatalog $script:CatalogPath }
elseif (Test-Path -LiteralPath $sourceCatalog) { $script:Catalog=Read-PanelCatalog $sourceCatalog }
} catch { Show-PanelError ('Каталог не загружен: ' + $_.Exception.Message + '. Исправьте файл или импортируйте другой каталог. Исходный файл сохранён.') }
$form=New-Object Windows.Forms.Form; $form.Text='Панели 3x-ui — установка PuTTY'; $form.ClientSize=New-Object Drawing.Size(850,510)
$form.MinimumSize=$form.Size; $form.MaximumSize=$form.Size; $form.StartPosition='CenterScreen'; $form.MaximizeBox=$false
$title=New-Object Windows.Forms.Label; $title.Text='Отметьте серверы, для которых нужны сессии PuTTY и ярлыки:'; $title.SetBounds(16,16,800,28); $form.Controls.Add($title)
$list=New-Object Windows.Forms.CheckedListBox; $list.CheckOnClick=$true; $list.SetBounds(16,52,818,225); $form.Controls.Add($list)
function Refresh-PanelList {
$list.Items.Clear()
foreach ($entry in $script:Catalog.Sessions) { $null=$list.Items.Add(($entry.Role + ' — ' + $entry.Label + ' — ' + $entry.HostName + ':' + $entry.PortNumber),$false) }
}
function Save-PanelCatalog { Write-PanelCatalog $script:CatalogPath $script:Catalog }
function New-PanelButton([string]$Text,[int]$X,[int]$Y,[int]$Width,$Action) {
$button=New-Object Windows.Forms.Button; $button.Text=$Text; $button.SetBounds($X,$Y,$Width,32); $button.Add_Click($Action); $form.Controls.Add($button)
}
New-PanelButton 'Добавить сервер' 16 294 150 {
$entry=Edit-PanelSession ([pscustomobject]@{})
if ($entry) { try { $candidate=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($script:Catalog.Sessions)+@($entry)}); Write-PanelCatalog $script:CatalogPath $candidate; $script:Catalog=$candidate; Refresh-PanelList } catch { Show-PanelError $_.Exception.Message } }
}
New-PanelButton 'Изменить' 176 294 120 {
$index=$list.SelectedIndex
if ($index -lt 0) { return }
$entry=Edit-PanelSession $script:Catalog.Sessions[$index]
if ($entry) { try { $values=@($script:Catalog.Sessions); $values[$index]=$entry; $candidate=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=$values}); Write-PanelCatalog $script:CatalogPath $candidate; $script:Catalog=$candidate; Refresh-PanelList } catch { Show-PanelError $_.Exception.Message } }
}
New-PanelButton 'Убрать из списка' 306 294 155 {
$index=$list.SelectedIndex; if ($index -lt 0) { return }
if ([Windows.Forms.MessageBox]::Show('Убрать сервер из каталога? Уже установленные ярлыки и PuTTY-сессия сохранятся.','3x-ui panels','YesNo') -ne 'Yes') { return }
try { $values=@(); for ($i=0;$i -lt $script:Catalog.Sessions.Count;$i++) { if ($i -ne $index) { $values += $script:Catalog.Sessions[$i] } }; $candidate=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=$values}); Write-PanelCatalog $script:CatalogPath $candidate; $script:Catalog=$candidate; Refresh-PanelList } catch { Show-PanelError $_.Exception.Message }
}
New-PanelButton 'Импорт каталога' 471 294 170 {
$file=Select-PanelFile 'Выберите каталог подключений' 'JSON catalog|*.json'
if (-not $file) { return }
try {
$imported=Read-PanelCatalog $file; $values=@($script:Catalog.Sessions)
foreach ($entry in $imported.Sessions) {
$found=@($values | Where-Object { $_.Name -ieq $entry.Name })
if ($found.Count -gt 0) {
if ([Windows.Forms.MessageBox]::Show(('Заменить настройки ' + $entry.Role + '?'),'Импорт','YesNo') -ne 'Yes') { continue }
$values=@($values | Where-Object { $_.Name -ine $entry.Name })
}
$values += $entry
}
$candidate=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=$values}); Write-PanelCatalog $script:CatalogPath $candidate; $script:Catalog=$candidate; Refresh-PanelList
} catch { Show-PanelError $_.Exception.Message }
}
New-PanelButton 'Экспорт каталога' 651 294 183 {
$dialog=New-Object Windows.Forms.SaveFileDialog; $dialog.Filter='JSON catalog|*.json'; $dialog.FileName='connections.json'
if ($dialog.ShowDialog() -eq 'OK') { try { Write-PanelCatalog $dialog.FileName $script:Catalog } catch { Show-PanelError $_.Exception.Message } }
}
$note=New-Object Windows.Forms.Label; $note.Text="Новый сервер добавляется без пересборки установщика. Каталог хранится в %LOCALAPPDATA%\KAgentPanels\catalog.json.`r`nВыберите существующий .ppk при установке. Приватный ключ и пароли установщик не копирует и не сохраняет."; $note.SetBounds(16,346,815,65); $form.Controls.Add($note)
New-PanelButton 'Установить выбранные' 16 440 245 {
try {
$names=@(); foreach ($index in $list.CheckedIndices) { $names += $script:Catalog.Sessions[$index].Name }
$null=Select-PanelSessions $script:Catalog $names
Save-PanelCatalog
Install-PanelSelections $script:Catalog $names
} catch { Show-PanelError $_.Exception.Message }
}
New-PanelButton 'Закрыть' 699 440 135 { $form.Close() }
Refresh-PanelList
$null=$form.ShowDialog()