169 lines
13 KiB
PowerShell
169 lines
13 KiB
PowerShell
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()
|