1498 lines
57 KiB
PowerShell
1498 lines
57 KiB
PowerShell
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
function New-RoundedRectPath {
|
|
param(
|
|
[System.Drawing.RectangleF]$Rect,
|
|
[float]$Radius
|
|
)
|
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
$diameter = $Radius * 2
|
|
$path.AddArc($Rect.X, $Rect.Y, $diameter, $diameter, 180, 90) | Out-Null
|
|
$path.AddArc($Rect.Right - $diameter, $Rect.Y, $diameter, $diameter, 270, 90) | Out-Null
|
|
$path.AddArc($Rect.Right - $diameter, $Rect.Bottom - $diameter, $diameter, $diameter, 0, 90) | Out-Null
|
|
$path.AddArc($Rect.X, $Rect.Bottom - $diameter, $diameter, $diameter, 90, 90) | Out-Null
|
|
$path.CloseFigure()
|
|
return $path
|
|
}
|
|
|
|
function New-CertyLogoBitmap {
|
|
param([int]$Size = 64)
|
|
$bmp = New-Object System.Drawing.Bitmap($Size, $Size)
|
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
$g.SmoothingMode = "AntiAlias"
|
|
$g.Clear([System.Drawing.Color]::Transparent)
|
|
|
|
$accent = [System.Drawing.Color]::FromArgb(32, 46, 77)
|
|
$accentSoft = [System.Drawing.Color]::FromArgb(41, 58, 96)
|
|
$paper = [System.Drawing.Color]::FromArgb(255, 255, 255)
|
|
$border = [System.Drawing.Color]::FromArgb(210, 214, 222)
|
|
|
|
$pad = [Math]::Floor($Size * 0.14)
|
|
$rect = New-Object System.Drawing.RectangleF($pad, $pad, ($Size - ($pad * 2)), ($Size - ($pad * 2)))
|
|
$radius = [Math]::Max(4, [Math]::Floor($Size * 0.1))
|
|
$path = New-RoundedRectPath -Rect $rect -Radius $radius
|
|
|
|
$g.FillPath((New-Object System.Drawing.SolidBrush($paper)), $path)
|
|
$g.DrawPath((New-Object System.Drawing.Pen($border, 1.5)), $path)
|
|
|
|
$sealSize = [Math]::Floor($Size * 0.26)
|
|
$sealX = $rect.Right - $sealSize - ($pad * 0.2)
|
|
$sealY = $rect.Bottom - $sealSize - ($pad * 0.2)
|
|
$sealRect = New-Object System.Drawing.RectangleF($sealX, $sealY, $sealSize, $sealSize)
|
|
$g.FillEllipse((New-Object System.Drawing.SolidBrush($accent)), $sealRect)
|
|
|
|
$tri1 = @(
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.18), $sealRect.Bottom + 2),
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.42), $sealRect.Bottom + 2),
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.30), $sealRect.Bottom + ($sealSize * 0.28))
|
|
)
|
|
$tri2 = @(
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.58), $sealRect.Bottom + 2),
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.82), $sealRect.Bottom + 2),
|
|
[System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.70), $sealRect.Bottom + ($sealSize * 0.28))
|
|
)
|
|
$g.FillPolygon((New-Object System.Drawing.SolidBrush($accentSoft)), $tri1)
|
|
$g.FillPolygon((New-Object System.Drawing.SolidBrush($accentSoft)), $tri2)
|
|
|
|
$fontSize = [Math]::Floor($Size * 0.36)
|
|
$font = New-Object System.Drawing.Font("Segoe UI Semibold", $fontSize, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel)
|
|
$format = New-Object System.Drawing.StringFormat
|
|
$format.Alignment = "Center"
|
|
$format.LineAlignment = "Center"
|
|
$textRect = New-Object System.Drawing.RectangleF($rect.X, $rect.Y, $rect.Width, $rect.Height)
|
|
$g.DrawString("C", $font, (New-Object System.Drawing.SolidBrush($accent)), $textRect, $format)
|
|
|
|
$g.Dispose()
|
|
return $bmp
|
|
}
|
|
|
|
function Split-List {
|
|
param([string]$Text)
|
|
if ([string]::IsNullOrWhiteSpace($Text)) { return @() }
|
|
return $Text -split '[,\r\n;]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
}
|
|
|
|
function Merge-Hostnames {
|
|
param(
|
|
[string[]]$Existing,
|
|
[string[]]$NewItems
|
|
)
|
|
$set = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($item in $Existing) { [void]$set.Add($item) }
|
|
foreach ($item in $NewItems) { [void]$set.Add($item) }
|
|
return $set | Sort-Object
|
|
}
|
|
|
|
function Get-LocalIpv4 {
|
|
try {
|
|
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
|
|
Where-Object {
|
|
$_.IPAddress -and
|
|
$_.IPAddress -notlike "169.254.*" -and
|
|
$_.IPAddress -ne "127.0.0.1" -and
|
|
$_.PrefixOrigin -in @("Dhcp", "Manual")
|
|
} |
|
|
Sort-Object -Property InterfaceMetric, SkipAsSource
|
|
|
|
$ip = $candidates | Select-Object -First 1 -ExpandProperty IPAddress
|
|
if ($ip) { return $ip }
|
|
} catch {
|
|
# Ignore and fallback to DNS lookup.
|
|
}
|
|
|
|
try {
|
|
$dnsIps = [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME) |
|
|
Where-Object {
|
|
$_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and
|
|
$_.ToString() -notlike "169.254.*" -and
|
|
$_.ToString() -ne "127.0.0.1"
|
|
} |
|
|
Select-Object -First 1
|
|
if ($dnsIps) { return $dnsIps.ToString() }
|
|
} catch {
|
|
# Ignore.
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
function Get-FilePreview {
|
|
param(
|
|
[string]$Path,
|
|
[int]$MaxLines = 200
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return "" }
|
|
if (-not (Test-Path -Path $Path -PathType Leaf)) { return "" }
|
|
try {
|
|
$lines = Get-Content -Path $Path -TotalCount $MaxLines -ErrorAction Stop
|
|
return ($lines -join [Environment]::NewLine)
|
|
} catch {
|
|
return "Failed to read file preview: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
function Get-CommonZoneFromHosts {
|
|
param([string[]]$Hosts)
|
|
$fqdnHosts = @($Hosts | Where-Object { $_ -and $_.Contains(".") })
|
|
if (-not $fqdnHosts -or $fqdnHosts.Count -eq 0) { return "" }
|
|
|
|
$commonSuffix = $null
|
|
foreach ($hostName in $fqdnHosts) {
|
|
$clean = $hostName.Trim().TrimEnd(".")
|
|
$labels = $clean -split "\."
|
|
if ($labels.Count -lt 2) { continue }
|
|
$zoneLabels = $labels[1..($labels.Count - 1)]
|
|
if (-not $commonSuffix) {
|
|
$commonSuffix = $zoneLabels
|
|
continue
|
|
}
|
|
|
|
$i = $commonSuffix.Count - 1
|
|
$j = $zoneLabels.Count - 1
|
|
$newSuffix = @()
|
|
while ($i -ge 0 -and $j -ge 0) {
|
|
if ($commonSuffix[$i].ToLower() -ne $zoneLabels[$j].ToLower()) { break }
|
|
$newSuffix = ,$zoneLabels[$j] + $newSuffix
|
|
$i--
|
|
$j--
|
|
}
|
|
$commonSuffix = $newSuffix
|
|
if (-not $commonSuffix -or $commonSuffix.Count -eq 0) { break }
|
|
}
|
|
|
|
if (-not $commonSuffix -or $commonSuffix.Count -eq 0) { return "" }
|
|
return ($commonSuffix -join ".")
|
|
}
|
|
|
|
function Get-DefaultsPath {
|
|
$dir = Join-Path $env:ProgramData "Certy"
|
|
return Join-Path $dir "defaults.json"
|
|
}
|
|
|
|
function Load-Defaults {
|
|
$path = Get-DefaultsPath
|
|
if (-not (Test-Path -Path $path -PathType Leaf)) { return $null }
|
|
try {
|
|
$raw = Get-Content -Path $path -Raw -ErrorAction Stop
|
|
return $raw | ConvertFrom-Json
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Save-Defaults {
|
|
param([pscustomobject]$Defaults)
|
|
$path = Get-DefaultsPath
|
|
$dir = Split-Path -Path $path -Parent
|
|
if (-not (Test-Path -Path $dir -PathType Container)) {
|
|
New-Item -Path $dir -ItemType Directory -Force | Out-Null
|
|
}
|
|
$json = $Defaults | ConvertTo-Json -Depth 6
|
|
Set-Content -Path $path -Value $json -Encoding ascii
|
|
}
|
|
|
|
function Get-ReplicationCredentialPath {
|
|
$dir = Join-Path $env:ProgramData "Certy"
|
|
return Join-Path $dir "replication-cred.xml"
|
|
}
|
|
|
|
function Load-ReplicationCredential {
|
|
$path = Get-ReplicationCredentialPath
|
|
if (-not (Test-Path -Path $path -PathType Leaf)) { return $null }
|
|
try {
|
|
$cred = Import-Clixml -Path $path
|
|
if ($cred -is [pscredential]) { return $cred }
|
|
} catch {
|
|
# Ignore load errors and treat as missing.
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Save-ReplicationCredential {
|
|
param([pscredential]$Credential)
|
|
$path = Get-ReplicationCredentialPath
|
|
$dir = Split-Path -Path $path -Parent
|
|
if (-not (Test-Path -Path $dir -PathType Container)) {
|
|
New-Item -Path $dir -ItemType Directory -Force | Out-Null
|
|
}
|
|
$Credential | Export-Clixml -Path $path
|
|
}
|
|
|
|
function Get-DefaultValue {
|
|
param(
|
|
[object]$Defaults,
|
|
[string]$Name
|
|
)
|
|
if ($null -eq $Defaults) { return $null }
|
|
$prop = $Defaults.PSObject.Properties[$Name]
|
|
if ($null -eq $prop) { return $null }
|
|
return $prop.Value
|
|
}
|
|
|
|
function Resolve-HostEntry {
|
|
param(
|
|
[string]$Name,
|
|
[string]$Zone,
|
|
[bool]$UseProvidedFqdn
|
|
)
|
|
|
|
$name = $Name.Trim()
|
|
if (-not $name) { return $null }
|
|
|
|
$zoneLower = $Zone.ToLower()
|
|
$nameLower = $name.ToLower()
|
|
|
|
if ($UseProvidedFqdn) {
|
|
if ($name -like "*.*") { $fqdn = $name } else { $fqdn = "$name.$Zone" }
|
|
} else {
|
|
if ($name -like "*.*") {
|
|
if ($nameLower.EndsWith(".$zoneLower") -or $nameLower -eq $zoneLower) {
|
|
$fqdn = $name
|
|
} else {
|
|
$fqdn = "$name.$Zone"
|
|
}
|
|
} else {
|
|
$fqdn = "$name.$Zone"
|
|
}
|
|
}
|
|
|
|
$fqdnLower = $fqdn.ToLower()
|
|
if ($fqdnLower.EndsWith(".$zoneLower")) {
|
|
$hostLabel = $fqdn.Substring(0, $fqdn.Length - $Zone.Length - 1)
|
|
} elseif ($fqdnLower -eq $zoneLower) {
|
|
$hostLabel = "@"
|
|
} else {
|
|
$hostLabel = $fqdn
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Input = $name
|
|
Fqdn = $fqdn
|
|
HostLabel = $hostLabel
|
|
}
|
|
}
|
|
|
|
function Get-DnsServerCandidates {
|
|
$servers = @()
|
|
try {
|
|
Import-Module ActiveDirectory -ErrorAction Stop | Out-Null
|
|
$servers += Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
|
|
} catch {
|
|
# Ignore AD lookup failures and fallback to local DNS client config.
|
|
}
|
|
|
|
try {
|
|
$local = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Stop |
|
|
ForEach-Object { $_.ServerAddresses } | Where-Object { $_ }
|
|
$servers += $local
|
|
} catch {
|
|
# Ignore if DnsClient module is unavailable.
|
|
}
|
|
|
|
return $servers | Sort-Object -Unique
|
|
}
|
|
|
|
function Ensure-ARecord {
|
|
param(
|
|
[string]$Zone,
|
|
[string]$HostLabel,
|
|
[string]$TargetIp,
|
|
[string]$DnsServer,
|
|
[scriptblock]$Log
|
|
)
|
|
|
|
$existing = Get-DnsServerResourceRecord -ZoneName $Zone -Name $HostLabel -RRType "A" -ComputerName $DnsServer -ErrorAction SilentlyContinue
|
|
if ($existing) {
|
|
$record = $existing | Select-Object -First 1
|
|
$currentIp = $record.RecordData.IPv4Address.ToString()
|
|
if ($currentIp -eq $TargetIp) {
|
|
& $Log "DNS A record exists: $HostLabel.$Zone -> $TargetIp"
|
|
return
|
|
}
|
|
|
|
$newRecord = $record.Clone()
|
|
$newRecord.RecordData.IPv4Address = [ipaddress]$TargetIp
|
|
Set-DnsServerResourceRecord -ZoneName $Zone -OldInputObject $record -NewInputObject $newRecord -ComputerName $DnsServer
|
|
& $Log "DNS A record updated: $HostLabel.$Zone -> $TargetIp"
|
|
return
|
|
}
|
|
|
|
Add-DnsServerResourceRecordA -Name $HostLabel -ZoneName $Zone -IPv4Address $TargetIp -ComputerName $DnsServer
|
|
& $Log "DNS A record added: $HostLabel.$Zone -> $TargetIp"
|
|
}
|
|
|
|
function Invoke-Replication {
|
|
param(
|
|
[string[]]$Servers,
|
|
[string]$Command,
|
|
[bool]$UseRemote,
|
|
[pscredential]$Credential,
|
|
[string]$SourceDc,
|
|
[scriptblock]$Log
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Command)) { return }
|
|
$targets = @($Servers | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
if ($targets.Count -eq 0) {
|
|
& $Log "Replication skipped: no target servers."
|
|
return
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($SourceDc)) {
|
|
& $Log "Replication skipped: source DC is empty."
|
|
return
|
|
}
|
|
|
|
$session = $null
|
|
try {
|
|
if ($UseRemote) {
|
|
try {
|
|
if ($Credential) {
|
|
$session = New-PSSession -ComputerName $SourceDc -Credential $Credential -ErrorAction Stop
|
|
} else {
|
|
$session = New-PSSession -ComputerName $SourceDc -ErrorAction Stop
|
|
}
|
|
& $Log "Replication session opened: $SourceDc"
|
|
} catch {
|
|
& $Log ("Replication session error on {0}: {1}" -f $SourceDc, $_.Exception.Message)
|
|
return
|
|
}
|
|
}
|
|
|
|
foreach ($server in $targets) {
|
|
$usesToken = $Command -match "\{server\}|\{dest\}"
|
|
$cmd = $Command.Replace("{server}", $server).Replace("{dest}", $server)
|
|
$cmd = $cmd.Trim()
|
|
if ($cmd -match "^(?i)\s*/repadmin\b") {
|
|
$cmd = $cmd -replace "^(?i)\s*/repadmin\b", "repadmin"
|
|
}
|
|
|
|
if ($UseRemote) {
|
|
$taskName = ("OneShot_AD_DNS_Repl_{0}" -f ($server -replace "[^A-Za-z0-9_-]", "_"))
|
|
$outFile = ("C:\Windows\Temp\repadmin-{0}.txt" -f $server)
|
|
$repadminCmd = $cmd
|
|
$repadminCmd = [regex]::Replace($repadminCmd, "\s{2,}", " ").Trim()
|
|
if ([string]::IsNullOrWhiteSpace($repadminCmd)) {
|
|
& $Log "Replication skipped: empty command for $server."
|
|
continue
|
|
}
|
|
|
|
& $Log "Replication (scheduled task): $SourceDc -> $server"
|
|
try {
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($DestDC, $TaskName, $OutFile, $RepadminCmd)
|
|
|
|
$cmdLine = "cmd.exe /c $RepadminCmd > `"$OutFile`" 2>&1"
|
|
schtasks /Create /F /TN $TaskName /RU SYSTEM /SC ONCE /ST 00:00 /TR $cmdLine | Out-Null
|
|
schtasks /Run /TN $TaskName | Out-Null
|
|
Start-Sleep 6
|
|
|
|
$output = if (Test-Path $OutFile) { Get-Content $OutFile } else { "No output file found" }
|
|
schtasks /Delete /F /TN $TaskName | Out-Null
|
|
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
|
|
$output
|
|
} -ArgumentList $server, $taskName, $outFile, $repadminCmd | ForEach-Object { & $Log $_ }
|
|
} catch {
|
|
& $Log ("Replication error on {0}: {1}" -f $server, $_.Exception.Message)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (-not $usesToken -and $server -and $cmd -match "(?i)\brepadmin\b" -and $cmd -match "(?i)\bsyncall\b") {
|
|
if ($cmd -notmatch "(?i)\\bsyncall\\s+\\S+") {
|
|
$cmd = $cmd -replace "(?i)\\bsyncall\\b", "syncall $server"
|
|
}
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($cmd)) { continue }
|
|
& $Log "Replication: $cmd"
|
|
& $env:ComSpec /c $cmd | ForEach-Object { & $Log $_ }
|
|
}
|
|
} finally {
|
|
if ($session) {
|
|
try { Remove-PSSession -Session $session } catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-Wacs {
|
|
param(
|
|
[string]$WacsPath,
|
|
[string[]]$HostFqdns,
|
|
[string]$OutputType,
|
|
[string]$OutputPath,
|
|
[string]$PfxPassword,
|
|
[string]$BaseUri,
|
|
[string]$Validation,
|
|
[string]$ValidationPort,
|
|
[bool]$Verbose,
|
|
[scriptblock]$Log
|
|
)
|
|
|
|
$args = @("--target", "manual")
|
|
foreach ($hostName in $HostFqdns) {
|
|
if (-not [string]::IsNullOrWhiteSpace($hostName)) {
|
|
$args += @("--host", $hostName)
|
|
}
|
|
}
|
|
|
|
if ($OutputType -eq "PEM") {
|
|
$args += @("--store", "pemfiles", "--pemfilespath", $OutputPath)
|
|
} else {
|
|
$args += @("--store", "pfxfile", "--pfxfilepath", $OutputPath)
|
|
if (-not [string]::IsNullOrWhiteSpace($PfxPassword)) {
|
|
$args += @("--pfxpassword", $PfxPassword)
|
|
}
|
|
}
|
|
|
|
$args += @(
|
|
"--baseuri", $BaseUri,
|
|
"--validation", $Validation,
|
|
"--validationport", $ValidationPort
|
|
)
|
|
if ($Verbose) { $args += "--verbose" }
|
|
|
|
& $Log "WACS: $WacsPath $($args -join ' ')"
|
|
& $WacsPath @args
|
|
}
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "Certy - WACS Helper"
|
|
$form.Size = [System.Drawing.Size]::new(1000, 860)
|
|
$form.StartPosition = "CenterScreen"
|
|
$form.AutoScaleMode = "Dpi"
|
|
|
|
$colorBg = [System.Drawing.Color]::FromArgb(130, 129, 129)
|
|
$colorPanel = [System.Drawing.Color]::FromArgb(255, 255, 255)
|
|
$colorText = [System.Drawing.Color]::FromArgb(30, 37, 45)
|
|
$colorMuted = [System.Drawing.Color]::FromArgb(90, 98, 110)
|
|
$colorAccent = [System.Drawing.Color]::FromArgb(32, 46, 77)
|
|
$colorAccentSoft = [System.Drawing.Color]::FromArgb(41, 58, 96)
|
|
$colorBorder = [System.Drawing.Color]::FromArgb(220, 224, 230)
|
|
$colorInput = [System.Drawing.Color]::FromArgb(255, 255, 255)
|
|
|
|
$form.BackColor = $colorBg
|
|
|
|
$logoSmall = New-CertyLogoBitmap -Size 32
|
|
$logoLarge = New-CertyLogoBitmap -Size 64
|
|
$form.Icon = [System.Drawing.Icon]::FromHandle($logoSmall.GetHicon())
|
|
|
|
$sidebarWidth = 170
|
|
$sidebar = New-Object System.Windows.Forms.Panel
|
|
$sidebar.Dock = "Left"
|
|
$sidebar.Width = $sidebarWidth
|
|
$sidebar.BackColor = $colorAccent
|
|
|
|
$panel = New-Object System.Windows.Forms.Panel
|
|
$panel.Dock = "Fill"
|
|
$panel.AutoScroll = $true
|
|
$panel.BackColor = $colorBg
|
|
$form.Controls.Add($panel)
|
|
$form.Controls.Add($sidebar)
|
|
|
|
$font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$labelWidth = 200
|
|
$inputWidth = 720
|
|
$xLabel = 20
|
|
$xInput = 230
|
|
$y = 20
|
|
$rowHeight = 24
|
|
$gap = 8
|
|
$leftMargin = 20
|
|
$rightMargin = 20
|
|
$buttonWidth = 110
|
|
$buttonGap = 10
|
|
$actionButtonWidth = 130
|
|
|
|
$navTitle = New-Object System.Windows.Forms.Label
|
|
$navTitle.Text = "CERTY"
|
|
$navTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 12)
|
|
$navTitle.ForeColor = [System.Drawing.Color]::White
|
|
$navTitle.Location = [System.Drawing.Point]::new(16, 16)
|
|
$navTitle.Size = [System.Drawing.Size]::new(130, 24)
|
|
$sidebar.Controls.Add($navTitle)
|
|
|
|
$navSub = New-Object System.Windows.Forms.Label
|
|
$navSub.Text = "Enterprise Console"
|
|
$navSub.Font = New-Object System.Drawing.Font("Segoe UI", 8)
|
|
$navSub.ForeColor = [System.Drawing.Color]::FromArgb(200, 214, 240)
|
|
$navSub.Location = [System.Drawing.Point]::new(16, 38)
|
|
$navSub.Size = [System.Drawing.Size]::new(140, 18)
|
|
$sidebar.Controls.Add($navSub)
|
|
|
|
$navItems = @("Input", "DNS", "ACME", "Run", "Logs")
|
|
$navY = 80
|
|
foreach ($item in $navItems) {
|
|
$navLabel = New-Object System.Windows.Forms.Label
|
|
$navLabel.Text = $item
|
|
$navLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 9)
|
|
$navLabel.ForeColor = [System.Drawing.Color]::FromArgb(210, 220, 236)
|
|
$navLabel.Location = [System.Drawing.Point]::new(16, $navY)
|
|
$navLabel.Size = [System.Drawing.Size]::new(140, 20)
|
|
$sidebar.Controls.Add($navLabel)
|
|
$navY += 26
|
|
}
|
|
|
|
function Style-ButtonPrimary {
|
|
param([System.Windows.Forms.Button]$Button)
|
|
$Button.BackColor = $colorAccent
|
|
$Button.ForeColor = [System.Drawing.Color]::White
|
|
$Button.FlatStyle = "Flat"
|
|
$Button.FlatAppearance.BorderSize = 0
|
|
$Button.UseVisualStyleBackColor = $false
|
|
}
|
|
|
|
function Style-ButtonSecondary {
|
|
param([System.Windows.Forms.Button]$Button)
|
|
$Button.BackColor = $colorPanel
|
|
$Button.ForeColor = $colorText
|
|
$Button.FlatStyle = "Flat"
|
|
$Button.FlatAppearance.BorderColor = $colorBorder
|
|
$Button.FlatAppearance.BorderSize = 1
|
|
$Button.UseVisualStyleBackColor = $false
|
|
}
|
|
|
|
function Add-Label {
|
|
param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H)
|
|
$label = New-Object System.Windows.Forms.Label
|
|
$label.Text = $Text
|
|
$label.Location = [System.Drawing.Point]::new($X, $Y)
|
|
$label.Size = [System.Drawing.Size]::new($W, $H)
|
|
$label.Font = $font
|
|
$label.ForeColor = $colorMuted
|
|
$panel.Controls.Add($label)
|
|
return $label
|
|
}
|
|
|
|
function Add-TextBox {
|
|
param([int]$X, [int]$Y, [int]$W, [int]$H, [bool]$Multiline = $false)
|
|
$tb = New-Object System.Windows.Forms.TextBox
|
|
$tb.Location = [System.Drawing.Point]::new($X, $Y)
|
|
$tb.Size = [System.Drawing.Size]::new($W, $H)
|
|
$tb.Font = $font
|
|
$tb.BackColor = $colorInput
|
|
$tb.ForeColor = $colorText
|
|
$tb.BorderStyle = "FixedSingle"
|
|
$tb.Multiline = $Multiline
|
|
if ($Multiline) {
|
|
$tb.ScrollBars = "Vertical"
|
|
}
|
|
$panel.Controls.Add($tb)
|
|
return $tb
|
|
}
|
|
|
|
function Add-CheckBox {
|
|
param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H)
|
|
$cb = New-Object System.Windows.Forms.CheckBox
|
|
$cb.Text = $Text
|
|
$cb.Location = [System.Drawing.Point]::new($X, $Y)
|
|
$cb.Size = [System.Drawing.Size]::new($W, $H)
|
|
$cb.Font = $font
|
|
$cb.ForeColor = $colorText
|
|
$panel.Controls.Add($cb)
|
|
return $cb
|
|
}
|
|
|
|
function Add-SectionHeader {
|
|
param([string]$Text)
|
|
$sectionLabel = New-Object System.Windows.Forms.Label
|
|
$sectionLabel.Text = $Text
|
|
$sectionLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
|
|
$sectionLabel.ForeColor = $colorText
|
|
$sectionLabel.Location = [System.Drawing.Point]::new($xLabel, $script:y)
|
|
$sectionLabel.Size = [System.Drawing.Size]::new(300, 20)
|
|
$panel.Controls.Add($sectionLabel)
|
|
|
|
$sectionLine = New-Object System.Windows.Forms.Panel
|
|
$sectionLine.BackColor = $colorBorder
|
|
$sectionLine.Location = [System.Drawing.Point]::new($xLabel, ($script:y + 22))
|
|
$sectionLine.Size = [System.Drawing.Size]::new(($inputWidth + ($xInput - $xLabel)), 1)
|
|
$panel.Controls.Add($sectionLine)
|
|
$script:sectionLines.Add($sectionLine) | Out-Null
|
|
|
|
$script:y += 30
|
|
}
|
|
|
|
$sectionLines = New-Object System.Collections.Generic.List[System.Windows.Forms.Panel]
|
|
|
|
$header = New-Object System.Windows.Forms.Panel
|
|
$header.Location = [System.Drawing.Point]::new($xLabel, $y)
|
|
$header.Size = [System.Drawing.Size]::new(($inputWidth + ($xInput - $xLabel)), 70)
|
|
$header.BackColor = $colorPanel
|
|
$header.BorderStyle = "FixedSingle"
|
|
$panel.Controls.Add($header)
|
|
|
|
$logoBox = New-Object System.Windows.Forms.PictureBox
|
|
$logoBox.Image = $logoLarge
|
|
$logoBox.SizeMode = "Zoom"
|
|
$logoBox.Location = [System.Drawing.Point]::new(12, 12)
|
|
$logoBox.Size = [System.Drawing.Size]::new(40, 40)
|
|
$header.Controls.Add($logoBox)
|
|
|
|
$headerTitle = New-Object System.Windows.Forms.Label
|
|
$headerTitle.Text = "Certy Enterprise"
|
|
$headerTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 16)
|
|
$headerTitle.ForeColor = $colorText
|
|
$headerTitle.Location = [System.Drawing.Point]::new(60, 10)
|
|
$headerTitle.Size = [System.Drawing.Size]::new(300, 28)
|
|
$header.Controls.Add($headerTitle)
|
|
|
|
$headerSub = New-Object System.Windows.Forms.Label
|
|
$headerSub.Text = "WACS helper for DNS + ACME proxy workflows"
|
|
$headerSub.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$headerSub.ForeColor = $colorMuted
|
|
$headerSub.Location = [System.Drawing.Point]::new(60, 38)
|
|
$headerSub.Size = [System.Drawing.Size]::new(600, 20)
|
|
$header.Controls.Add($headerSub)
|
|
|
|
$helpBtn = New-Object System.Windows.Forms.Button
|
|
$helpBtn.Text = "How to use me"
|
|
$helpBtn.Size = [System.Drawing.Size]::new(140, 28)
|
|
$helpBtn.Location = [System.Drawing.Point]::new(($header.Width - 152), 20)
|
|
$header.Controls.Add($helpBtn)
|
|
Style-ButtonSecondary $helpBtn
|
|
|
|
$y = $header.Bottom + 16
|
|
|
|
Add-SectionHeader "Input"
|
|
Add-Label "Hostnames (one per line)" $xLabel $y $labelWidth $rowHeight
|
|
$hostsBox = Add-TextBox $xInput $y $inputWidth 100 $true
|
|
$y += 100 + $gap
|
|
|
|
Add-Label "Hostnames file (optional)" $xLabel $y $labelWidth $rowHeight
|
|
$fileBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) $rowHeight $false
|
|
$browseBtn = New-Object System.Windows.Forms.Button
|
|
$browseBtn.Text = "Browse"
|
|
$browseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$browseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($browseBtn)
|
|
Style-ButtonSecondary $browseBtn
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "File preview (first 200 lines)" $xLabel $y $labelWidth $rowHeight
|
|
$filePreviewBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) 80 $true
|
|
$filePreviewBox.ReadOnly = $true
|
|
$filePreviewBtn = New-Object System.Windows.Forms.Button
|
|
$filePreviewBtn.Text = "1) Preview"
|
|
$filePreviewBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$filePreviewBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($filePreviewBtn)
|
|
Style-ButtonSecondary $filePreviewBtn
|
|
$y += 82 + $gap
|
|
|
|
$csrLabel = Add-Label "CSR folder (optional)" $xLabel $y $labelWidth $rowHeight
|
|
$csrFolderBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) $rowHeight $false
|
|
$csrBrowseBtn = New-Object System.Windows.Forms.Button
|
|
$csrBrowseBtn.Text = "Browse"
|
|
$csrBrowseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1))
|
|
$csrBrowseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($csrBrowseBtn)
|
|
Style-ButtonSecondary $csrBrowseBtn
|
|
$csrImportBtn = New-Object System.Windows.Forms.Button
|
|
$csrImportBtn.Text = "Import CSR"
|
|
$csrImportBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$csrImportBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($csrImportBtn)
|
|
Style-ButtonSecondary $csrImportBtn
|
|
$y += $rowHeight + $gap
|
|
|
|
$useFqdnBox = Add-CheckBox "Input contains FQDNs (otherwise default zone is appended)" $xInput $y $inputWidth $rowHeight
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-SectionHeader "DNS & Replication"
|
|
Add-Label "Default DNS zone" $xLabel $y $labelWidth $rowHeight
|
|
$zoneBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$zoneBox.Text = "record.domain.govt.nz"
|
|
$y += $rowHeight + $gap
|
|
|
|
$replicationEnabledBox = Add-CheckBox "Enable DNS replication" $xInput $y 200 $rowHeight
|
|
$replicationEnabledBox.Checked = $true
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Target IPv4 for A records" $xLabel $y $labelWidth $rowHeight
|
|
$ipBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) $rowHeight $false
|
|
$ipBox.Text = Get-LocalIpv4
|
|
$ipRefreshBtn = New-Object System.Windows.Forms.Button
|
|
$ipRefreshBtn.Text = "Use Local"
|
|
$ipRefreshBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$ipRefreshBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($ipRefreshBtn)
|
|
Style-ButtonSecondary $ipRefreshBtn
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Primary DNS server (source DC)" $xLabel $y $labelWidth $rowHeight
|
|
$dnsServerBox = New-Object System.Windows.Forms.ComboBox
|
|
$dnsServerBox.Location = [System.Drawing.Point]::new($xInput, $y)
|
|
$dnsServerBox.Size = [System.Drawing.Size]::new(($inputWidth - ($buttonWidth + $buttonGap)), $rowHeight)
|
|
$dnsServerBox.DropDownStyle = "DropDown"
|
|
$dnsServerBox.Text = "DC01.example.local"
|
|
$dnsServerBox.FlatStyle = "Flat"
|
|
$dnsServerBox.BackColor = $colorInput
|
|
$dnsServerBox.ForeColor = $colorText
|
|
$panel.Controls.Add($dnsServerBox)
|
|
$dnsScanBtn = New-Object System.Windows.Forms.Button
|
|
$dnsScanBtn.Text = "2) Scan"
|
|
$dnsScanBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$dnsScanBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($dnsScanBtn)
|
|
Style-ButtonSecondary $dnsScanBtn
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "DNS servers (select for replication)" $xLabel $y $labelWidth $rowHeight
|
|
$dnsListBox = New-Object System.Windows.Forms.ListBox
|
|
$dnsListBox.Location = [System.Drawing.Point]::new($xInput, $y)
|
|
$dnsListBox.Size = [System.Drawing.Size]::new($inputWidth, 80)
|
|
$dnsListBox.SelectionMode = "MultiExtended"
|
|
$dnsListBox.BackColor = $colorInput
|
|
$dnsListBox.ForeColor = $colorText
|
|
$dnsListBox.BorderStyle = "FixedSingle"
|
|
$panel.Controls.Add($dnsListBox)
|
|
$y += 82 + $gap
|
|
|
|
Add-Label "Replication targets (one per line)" $xLabel $y $labelWidth $rowHeight
|
|
$replicationTargetsBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) 70 $true
|
|
$replicationFromSelectedBtn = New-Object System.Windows.Forms.Button
|
|
$replicationFromSelectedBtn.Text = "Use Selected"
|
|
$replicationFromSelectedBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1))
|
|
$replicationFromSelectedBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($replicationFromSelectedBtn)
|
|
Style-ButtonSecondary $replicationFromSelectedBtn
|
|
$primaryFromSelectedBtn = New-Object System.Windows.Forms.Button
|
|
$primaryFromSelectedBtn.Text = "Use Primary"
|
|
$primaryFromSelectedBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
|
|
$primaryFromSelectedBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
|
|
$panel.Controls.Add($primaryFromSelectedBtn)
|
|
Style-ButtonSecondary $primaryFromSelectedBtn
|
|
$y += 70 + $gap
|
|
|
|
Add-Label "Replication command ({dest} optional)" $xLabel $y $labelWidth $rowHeight
|
|
$replicationCmdBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$replicationCmdBox.Text = "repadmin /syncall {dest} /AdeP"
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Replication wait (seconds)" $xLabel $y $labelWidth $rowHeight
|
|
$replicationDelayBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$replicationDelayBox.Text = "30"
|
|
$y += $rowHeight + $gap
|
|
|
|
$replicationRemoteBox = Add-CheckBox "Run replication via scheduled task on source DC" $xInput $y $inputWidth $rowHeight
|
|
$replicationRemoteBox.Checked = $true
|
|
$y += $rowHeight + $gap
|
|
|
|
$replicationCredBtn = New-Object System.Windows.Forms.Button
|
|
$replicationCredBtn.Text = "Set Replication Credentials"
|
|
$replicationCredBtn.Location = [System.Drawing.Point]::new($xInput, $y)
|
|
$replicationCredBtn.Size = [System.Drawing.Size]::new(220, 26)
|
|
$panel.Controls.Add($replicationCredBtn)
|
|
Style-ButtonSecondary $replicationCredBtn
|
|
$y += $rowHeight + $gap
|
|
|
|
$y += $gap
|
|
|
|
Add-SectionHeader "ACME / Output"
|
|
Add-Label "WACS path" $xLabel $y $labelWidth $rowHeight
|
|
$wacsPathBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$wacsPathBox.Text = "C:\ProgramData\Wacs\wacs.exe"
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Output type" $xLabel $y $labelWidth $rowHeight
|
|
$outputTypeBox = New-Object System.Windows.Forms.ComboBox
|
|
$outputTypeBox.Location = [System.Drawing.Point]::new($xInput, $y)
|
|
$outputTypeBox.Size = [System.Drawing.Size]::new($inputWidth, $rowHeight)
|
|
$outputTypeBox.DropDownStyle = "DropDownList"
|
|
$outputTypeBox.FlatStyle = "Flat"
|
|
$outputTypeBox.BackColor = $colorInput
|
|
$outputTypeBox.ForeColor = $colorText
|
|
[void]$outputTypeBox.Items.Add("PFX")
|
|
[void]$outputTypeBox.Items.Add("PEM")
|
|
$outputTypeBox.SelectedIndex = 0
|
|
$panel.Controls.Add($outputTypeBox)
|
|
$y += $rowHeight + $gap
|
|
|
|
$outputPathLabel = Add-Label "PFX output path" $xLabel $y $labelWidth $rowHeight
|
|
$outputPathBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$outputPathBox.Text = "C:\programdata\wacs\output\"
|
|
$y += $rowHeight + $gap
|
|
|
|
$pfxPasswordLabel = Add-Label "PFX password" $xLabel $y $labelWidth $rowHeight
|
|
$pfxPasswordBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$pfxPasswordBox.UseSystemPasswordChar = $true
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "ACME base URI" $xLabel $y $labelWidth $rowHeight
|
|
$baseUriBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$baseUriBox.Text = "https://acmeprod.wd.govt.nz:9999/acme/rsa/"
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Validation method" $xLabel $y $labelWidth $rowHeight
|
|
$validationBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$validationBox.Text = "selfhosting"
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-Label "Validation port" $xLabel $y $labelWidth $rowHeight
|
|
$validationPortBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
|
|
$validationPortBox.Text = "9998"
|
|
$y += $rowHeight + $gap
|
|
|
|
Add-SectionHeader "Run"
|
|
$verboseBox = Add-CheckBox "Verbose" $xInput $y 120 $rowHeight
|
|
$perHostBox = Add-CheckBox "One cert per host" ($xInput + 430) $y 180 $rowHeight
|
|
$disableCertsBox = Add-CheckBox "Turn off cert generation (DNS-only mode)" ($xInput + 140) $y 360 $rowHeight
|
|
$y += $rowHeight + ($gap * 2)
|
|
|
|
$runBtn = New-Object System.Windows.Forms.Button
|
|
$runBtn.Text = "5) Run"
|
|
$runBtn.Location = [System.Drawing.Point]::new($xInput, $y)
|
|
$runBtn.Size = [System.Drawing.Size]::new(120, 30)
|
|
$panel.Controls.Add($runBtn)
|
|
Style-ButtonPrimary $runBtn
|
|
|
|
$clearBtn = New-Object System.Windows.Forms.Button
|
|
$clearBtn.Text = "Clear Log"
|
|
$clearBtn.Location = [System.Drawing.Point]::new(($xInput + 140), $y)
|
|
$clearBtn.Size = [System.Drawing.Size]::new($actionButtonWidth, 30)
|
|
$panel.Controls.Add($clearBtn)
|
|
Style-ButtonSecondary $clearBtn
|
|
|
|
$saveDefaultsBtn = New-Object System.Windows.Forms.Button
|
|
$saveDefaultsBtn.Text = "Save Defaults"
|
|
$saveDefaultsBtn.Location = [System.Drawing.Point]::new(($xInput + 140 + $actionButtonWidth + $buttonGap), $y)
|
|
$saveDefaultsBtn.Size = [System.Drawing.Size]::new($actionButtonWidth, 30)
|
|
$panel.Controls.Add($saveDefaultsBtn)
|
|
Style-ButtonSecondary $saveDefaultsBtn
|
|
|
|
$y += 40
|
|
Add-SectionHeader "Activity"
|
|
Add-Label "Activity log" $xLabel $y $labelWidth $rowHeight
|
|
$logBox = Add-TextBox $xInput $y $inputWidth 200 $true
|
|
$logBox.ReadOnly = $true
|
|
|
|
$logAction = {
|
|
param([string]$Message)
|
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
|
$logBox.AppendText("[$timestamp] $Message`r`n")
|
|
}
|
|
|
|
function Update-OutputTypeUI {
|
|
if ($outputTypeBox.SelectedItem -eq "PEM") {
|
|
$outputPathLabel.Text = "PEM output path"
|
|
$pfxPasswordLabel.Visible = $false
|
|
$pfxPasswordBox.Visible = $false
|
|
$pfxPasswordBox.Text = ""
|
|
} else {
|
|
$outputPathLabel.Text = "PFX output path"
|
|
$pfxPasswordLabel.Visible = $true
|
|
$pfxPasswordBox.Visible = $true
|
|
}
|
|
}
|
|
|
|
function Show-HelpDialog {
|
|
$helpForm = New-Object System.Windows.Forms.Form
|
|
$helpForm.Text = "How it works"
|
|
$helpForm.Size = [System.Drawing.Size]::new(760, 560)
|
|
$helpForm.StartPosition = "CenterParent"
|
|
$helpForm.BackColor = $colorBg
|
|
|
|
$panelHelp = New-Object System.Windows.Forms.Panel
|
|
$panelHelp.Dock = "Fill"
|
|
$panelHelp.BackColor = $colorBg
|
|
$panelHelp.Padding = New-Object System.Windows.Forms.Padding(16)
|
|
$helpForm.Controls.Add($panelHelp)
|
|
|
|
$headerPanel = New-Object System.Windows.Forms.Panel
|
|
$headerPanel.BackColor = $colorPanel
|
|
$headerPanel.BorderStyle = "FixedSingle"
|
|
$headerPanel.Size = [System.Drawing.Size]::new(700, 70)
|
|
$headerPanel.Location = [System.Drawing.Point]::new(0, 0)
|
|
$panelHelp.Controls.Add($headerPanel)
|
|
|
|
$title = New-Object System.Windows.Forms.Label
|
|
$title.Text = "Certy - How it works"
|
|
$title.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
|
|
$title.ForeColor = $colorText
|
|
$title.Location = [System.Drawing.Point]::new(12, 12)
|
|
$title.Size = [System.Drawing.Size]::new(500, 24)
|
|
$headerPanel.Controls.Add($title)
|
|
|
|
$subtitle = New-Object System.Windows.Forms.Label
|
|
$subtitle.Text = "Follow these steps to create DNS records and issue certificates."
|
|
$subtitle.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$subtitle.ForeColor = $colorMuted
|
|
$subtitle.Location = [System.Drawing.Point]::new(12, 38)
|
|
$subtitle.Size = [System.Drawing.Size]::new(640, 18)
|
|
$headerPanel.Controls.Add($subtitle)
|
|
|
|
$contentPanel = New-Object System.Windows.Forms.Panel
|
|
$contentPanel.Location = [System.Drawing.Point]::new(0, 86)
|
|
$contentPanel.Size = [System.Drawing.Size]::new(700, 380)
|
|
$contentPanel.BackColor = $colorBg
|
|
$panelHelp.Controls.Add($contentPanel)
|
|
|
|
$stepTitle = New-Object System.Windows.Forms.Label
|
|
$stepTitle.Text = "Basic steps"
|
|
$stepTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
|
|
$stepTitle.ForeColor = $colorText
|
|
$stepTitle.Location = [System.Drawing.Point]::new(0, 0)
|
|
$stepTitle.Size = [System.Drawing.Size]::new(200, 20)
|
|
$contentPanel.Controls.Add($stepTitle)
|
|
|
|
$stepY = 28
|
|
$steps = @(
|
|
"Add hostnames (one per line, CSV, or CSR folder) then click 1) Preview.",
|
|
"Click 2) Scan and select the DNS server (Primary is 10.106.60.1). Replication runs from there.",
|
|
"Set the replication wait time. Typical value: 15 minutes.",
|
|
"Choose output type (PEM or PFX). If PFX, supply a password.",
|
|
"Click 5) Run."
|
|
)
|
|
$index = 1
|
|
foreach ($step in $steps) {
|
|
$num = New-Object System.Windows.Forms.Label
|
|
$num.Text = "$index."
|
|
$num.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 9)
|
|
$num.ForeColor = $colorText
|
|
$num.Location = [System.Drawing.Point]::new(0, $stepY)
|
|
$num.Size = [System.Drawing.Size]::new(24, 20)
|
|
$contentPanel.Controls.Add($num)
|
|
|
|
$text = New-Object System.Windows.Forms.Label
|
|
$text.Text = $step
|
|
$text.Font = $font
|
|
$text.ForeColor = $colorText
|
|
$text.Location = [System.Drawing.Point]::new(28, $stepY)
|
|
$text.Size = [System.Drawing.Size]::new(660, 32)
|
|
$contentPanel.Controls.Add($text)
|
|
|
|
$stepY += 34
|
|
$index++
|
|
}
|
|
|
|
$optionsTitle = New-Object System.Windows.Forms.Label
|
|
$optionsTitle.Text = "Options"
|
|
$optionsTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
|
|
$optionsTitle.ForeColor = $colorText
|
|
$optionsTitle.Location = [System.Drawing.Point]::new(0, ($stepY + 10))
|
|
$optionsTitle.Size = [System.Drawing.Size]::new(200, 20)
|
|
$contentPanel.Controls.Add($optionsTitle)
|
|
|
|
$optY = $stepY + 36
|
|
$options = @(
|
|
"Turn off cert generation (DNS-only mode) to add DNS now and generate certs later.",
|
|
"Disable DNS replication if records already point correctly and you only need a renewal."
|
|
)
|
|
foreach ($opt in $options) {
|
|
$bullet = New-Object System.Windows.Forms.Label
|
|
$bullet.Text = "•"
|
|
$bullet.Font = $font
|
|
$bullet.ForeColor = $colorText
|
|
$bullet.Location = [System.Drawing.Point]::new(0, $optY)
|
|
$bullet.Size = [System.Drawing.Size]::new(12, 20)
|
|
$contentPanel.Controls.Add($bullet)
|
|
|
|
$optLabel = New-Object System.Windows.Forms.Label
|
|
$optLabel.Text = $opt
|
|
$optLabel.Font = $font
|
|
$optLabel.ForeColor = $colorText
|
|
$optLabel.Location = [System.Drawing.Point]::new(16, $optY)
|
|
$optLabel.Size = [System.Drawing.Size]::new(660, 32)
|
|
$contentPanel.Controls.Add($optLabel)
|
|
$optY += 30
|
|
}
|
|
|
|
$closeBtn = New-Object System.Windows.Forms.Button
|
|
$closeBtn.Text = "Close"
|
|
$closeBtn.Size = [System.Drawing.Size]::new(100, 30)
|
|
$closeBtn.Location = [System.Drawing.Point]::new(600, 470)
|
|
$panelHelp.Controls.Add($closeBtn)
|
|
Style-ButtonSecondary $closeBtn
|
|
$closeBtn.Add_Click({ $helpForm.Close() })
|
|
|
|
$helpForm.ShowDialog() | Out-Null
|
|
}
|
|
|
|
function Update-ReplicationUI {
|
|
$enabled = $replicationEnabledBox.Checked
|
|
$replicationTargetsBox.Enabled = $enabled
|
|
$replicationFromSelectedBtn.Enabled = $enabled
|
|
$primaryFromSelectedBtn.Enabled = $enabled
|
|
$replicationCmdBox.Enabled = $enabled
|
|
$replicationDelayBox.Enabled = $enabled
|
|
$replicationRemoteBox.Enabled = $enabled
|
|
$replicationCredBtn.Enabled = $enabled
|
|
$dnsListBox.Enabled = $enabled
|
|
}
|
|
|
|
function Update-CertGenerationUI {
|
|
$disabled = $disableCertsBox.Checked
|
|
$perHostBox.Enabled = -not $disabled
|
|
|
|
$wacsPathBox.Enabled = -not $disabled
|
|
$outputTypeBox.Enabled = -not $disabled
|
|
$outputPathBox.Enabled = -not $disabled
|
|
$pfxPasswordBox.Enabled = -not $disabled
|
|
$baseUriBox.Enabled = -not $disabled
|
|
$validationBox.Enabled = -not $disabled
|
|
$validationPortBox.Enabled = -not $disabled
|
|
}
|
|
|
|
function Update-ZoneFromHostInput {
|
|
$hostList = @(Split-List $hostsBox.Text)
|
|
if ($hostList.Count -eq 0) { return }
|
|
$zoneGuess = Get-CommonZoneFromHosts -Hosts $hostList
|
|
if (-not [string]::IsNullOrWhiteSpace($zoneGuess) -and $zoneBox.Text -ne $zoneGuess) {
|
|
$zoneBox.Text = $zoneGuess
|
|
& $logAction "Default DNS zone set to $zoneGuess (from hostnames)."
|
|
}
|
|
}
|
|
|
|
$loadedDefaults = Load-Defaults
|
|
if ($loadedDefaults) {
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "DefaultZone"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $zoneBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "TargetIp"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $ipBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "DnsServer"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $dnsServerBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationTargets"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $replicationTargetsBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationCommand"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $replicationCmdBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationEnabled"
|
|
if ($null -ne $value) { $replicationEnabledBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationDelaySeconds"
|
|
if ($null -ne $value) { $replicationDelayBox.Text = $value.ToString() }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationRemote"
|
|
if ($null -ne $value) { $replicationRemoteBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "WacsPath"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $wacsPathBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "OutputPath"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $outputPathBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "PfxPassword"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $pfxPasswordBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "BaseUri"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $baseUriBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "Validation"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $validationBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "ValidationPort"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $validationPortBox.Text = $value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "UseProvidedFqdn"
|
|
if ($null -ne $value) { $useFqdnBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "Verbose"
|
|
if ($null -ne $value) { $verboseBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "PerHostCerts"
|
|
if ($null -ne $value) { $perHostBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "DisableCertGeneration"
|
|
if ($null -ne $value) { $disableCertsBox.Checked = [bool]$value }
|
|
|
|
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "OutputType"
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) { $outputTypeBox.SelectedItem = $value }
|
|
if (-not $outputTypeBox.SelectedItem) { $outputTypeBox.SelectedIndex = 0 }
|
|
|
|
Update-OutputTypeUI
|
|
if (Test-Path function:Update-ReplicationUI) { Update-ReplicationUI }
|
|
Update-CertGenerationUI
|
|
& $logAction "Defaults loaded from $(Get-DefaultsPath)."
|
|
}
|
|
|
|
function Apply-Layout {
|
|
if ($panel.ClientSize.Width -le 0) { return }
|
|
$contentWidth = $panel.ClientSize.Width - $leftMargin - $rightMargin
|
|
$inputWidthCalc = [Math]::Max(420, ($contentWidth - ($xInput - $xLabel)))
|
|
$script:inputWidth = $inputWidthCalc
|
|
|
|
$header.Width = $contentWidth
|
|
foreach ($line in $sectionLines) {
|
|
$line.Width = $contentWidth
|
|
}
|
|
$helpBtn.Left = $header.Width - $helpBtn.Width - 12
|
|
|
|
$hostsBox.Width = $inputWidthCalc
|
|
$fileBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap)
|
|
$browseBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$filePreviewBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap)
|
|
$filePreviewBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$csrFolderBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
|
|
$csrBrowseBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
|
|
$csrImportBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$zoneBox.Width = $inputWidthCalc
|
|
$ipBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap)
|
|
$ipRefreshBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$dnsServerBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap)
|
|
$dnsScanBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$dnsListBox.Width = $inputWidthCalc
|
|
$replicationTargetsBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
|
|
$replicationFromSelectedBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
|
|
$primaryFromSelectedBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
|
|
$replicationCmdBox.Width = $inputWidthCalc
|
|
$replicationDelayBox.Width = $inputWidthCalc
|
|
$replicationRemoteBox.Width = $inputWidthCalc
|
|
$replicationCredBtn.Width = [Math]::Min($inputWidthCalc, 260)
|
|
$wacsPathBox.Width = $inputWidthCalc
|
|
$outputTypeBox.Width = $inputWidthCalc
|
|
$outputPathBox.Width = $inputWidthCalc
|
|
$pfxPasswordBox.Width = $inputWidthCalc
|
|
$baseUriBox.Width = $inputWidthCalc
|
|
$validationBox.Width = $inputWidthCalc
|
|
$validationPortBox.Width = $inputWidthCalc
|
|
$logBox.Width = $inputWidthCalc
|
|
$clearBtn.Left = $xInput + 140
|
|
$saveDefaultsBtn.Left = $clearBtn.Left + $clearBtn.Width + $buttonGap
|
|
}
|
|
|
|
$browseBtn.Add_Click({
|
|
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
|
$dialog.Filter = "Text/CSV files (*.txt;*.csv)|*.txt;*.csv|All files (*.*)|*.*"
|
|
$dialog.Multiselect = $false
|
|
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
$fileBox.Text = $dialog.FileName
|
|
$filePreviewBox.Text = Get-FilePreview -Path $dialog.FileName
|
|
$fileHosts = Get-Content -Path $dialog.FileName | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
$zoneGuess = Get-CommonZoneFromHosts -Hosts $fileHosts
|
|
if ($zoneGuess) {
|
|
$zoneBox.Text = $zoneGuess
|
|
& $logAction "Default DNS zone set to $zoneGuess (from file)."
|
|
}
|
|
}
|
|
})
|
|
|
|
$replicationEnabledBox.Add_CheckedChanged({
|
|
if (Test-Path function:Update-ReplicationUI) { Update-ReplicationUI }
|
|
})
|
|
|
|
$filePreviewBtn.Add_Click({
|
|
$path = $fileBox.Text.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($path)) {
|
|
$previewHosts = @(Split-List $hostsBox.Text)
|
|
if ($previewHosts.Count -gt 0) {
|
|
$filePreviewBox.Text = ($previewHosts | Select-Object -First 200) -join [Environment]::NewLine
|
|
return
|
|
}
|
|
$filePreviewBox.Text = ""
|
|
& $logAction "No file selected and no hostnames to preview."
|
|
return
|
|
}
|
|
|
|
$filePreviewBox.Text = Get-FilePreview -Path $path
|
|
if (Test-Path -Path $path -PathType Leaf) {
|
|
$fileHosts = Get-Content -Path $path | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
$zoneGuess = Get-CommonZoneFromHosts -Hosts $fileHosts
|
|
if ($zoneGuess) {
|
|
$zoneBox.Text = $zoneGuess
|
|
& $logAction "Default DNS zone set to $zoneGuess (from file)."
|
|
}
|
|
}
|
|
})
|
|
|
|
$csrBrowseBtn.Add_Click({
|
|
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
$csrFolderBox.Text = $dialog.SelectedPath
|
|
}
|
|
})
|
|
|
|
$csrImportBtn.Add_Click({
|
|
try {
|
|
$folder = $csrFolderBox.Text.Trim()
|
|
if (-not $folder) { throw "CSR folder is empty." }
|
|
if (-not (Test-Path -Path $folder -PathType Container)) { throw "CSR folder not found: $folder" }
|
|
|
|
$csrFiles = Get-ChildItem -Path $folder -Include *.csr, *.pem -File -Recurse
|
|
if (-not $csrFiles) {
|
|
& $logAction "No CSR files found in $folder"
|
|
return
|
|
}
|
|
|
|
$csrHosts = @($csrFiles | ForEach-Object { $_.BaseName.Trim() } | Where-Object { $_ })
|
|
$currentHosts = Split-List $hostsBox.Text
|
|
$merged = Merge-Hostnames -Existing $currentHosts -NewItems $csrHosts
|
|
$hostsBox.Text = ($merged -join [Environment]::NewLine)
|
|
& $logAction "Imported $($csrHosts.Count) CSR hostname(s)."
|
|
} catch {
|
|
& $logAction "Error: $($_.Exception.Message)"
|
|
}
|
|
})
|
|
|
|
$dnsScanBtn.Add_Click({
|
|
try {
|
|
$dnsListBox.Items.Clear()
|
|
$servers = @(Get-DnsServerCandidates)
|
|
if (-not $servers -or $servers.Count -eq 0) {
|
|
& $logAction "No DNS servers found. Enter servers manually."
|
|
return
|
|
}
|
|
foreach ($server in $servers) {
|
|
[void]$dnsListBox.Items.Add($server)
|
|
}
|
|
$dnsServerBox.Items.Clear()
|
|
foreach ($server in $servers) {
|
|
[void]$dnsServerBox.Items.Add($server)
|
|
}
|
|
if ($dnsServerBox.Items.Count -gt 0) {
|
|
$dnsServerBox.SelectedIndex = 0
|
|
}
|
|
& $logAction "Loaded $($servers.Count) DNS server(s)."
|
|
} catch {
|
|
& $logAction "Error: $($_.Exception.Message)"
|
|
}
|
|
})
|
|
|
|
$ipRefreshBtn.Add_Click({
|
|
$ipBox.Text = Get-LocalIpv4
|
|
})
|
|
|
|
$replicationFromSelectedBtn.Add_Click({
|
|
$selected = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() })
|
|
if ($selected.Count -eq 0) {
|
|
& $logAction "No DNS servers selected."
|
|
return
|
|
}
|
|
$replicationTargetsBox.Text = ($selected -join [Environment]::NewLine)
|
|
& $logAction "Replication targets set from selected DNS servers."
|
|
})
|
|
|
|
$primaryFromSelectedBtn.Add_Click({
|
|
$selected = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() })
|
|
if ($selected.Count -eq 0) {
|
|
& $logAction "No DNS servers selected."
|
|
return
|
|
}
|
|
$dnsServerBox.Text = $selected[0]
|
|
& $logAction "Primary DNS server set to $($selected[0])."
|
|
})
|
|
|
|
$outputTypeBox.Add_SelectedIndexChanged({
|
|
Update-OutputTypeUI
|
|
})
|
|
|
|
$helpBtn.Add_Click({
|
|
Show-HelpDialog
|
|
})
|
|
|
|
$replicationCredBtn.Add_Click({
|
|
$cred = Get-Credential -Message "Enter credentials for replication sessions."
|
|
if (-not $cred) {
|
|
& $logAction "Replication credentials not set."
|
|
return
|
|
}
|
|
Save-ReplicationCredential -Credential $cred
|
|
& $logAction "Replication credentials saved for this user."
|
|
})
|
|
|
|
$hostsBox.Add_TextChanged({
|
|
Update-ZoneFromHostInput
|
|
})
|
|
|
|
$disableCertsBox.Add_CheckedChanged({
|
|
Update-CertGenerationUI
|
|
})
|
|
|
|
$saveDefaultsBtn.Add_Click({
|
|
$defaults = [pscustomobject]@{
|
|
DefaultZone = $zoneBox.Text
|
|
TargetIp = $ipBox.Text
|
|
DnsServer = $dnsServerBox.Text
|
|
ReplicationTargets = $replicationTargetsBox.Text
|
|
ReplicationCommand = $replicationCmdBox.Text
|
|
ReplicationEnabled = $replicationEnabledBox.Checked
|
|
ReplicationDelaySeconds = $replicationDelayBox.Text
|
|
ReplicationRemote = $replicationRemoteBox.Checked
|
|
WacsPath = $wacsPathBox.Text
|
|
OutputType = $outputTypeBox.SelectedItem.ToString()
|
|
OutputPath = $outputPathBox.Text
|
|
PfxPassword = $pfxPasswordBox.Text
|
|
BaseUri = $baseUriBox.Text
|
|
Validation = $validationBox.Text
|
|
ValidationPort = $validationPortBox.Text
|
|
UseProvidedFqdn = $useFqdnBox.Checked
|
|
Verbose = $verboseBox.Checked
|
|
PerHostCerts = $perHostBox.Checked
|
|
DisableCertGeneration = $disableCertsBox.Checked
|
|
}
|
|
Save-Defaults -Defaults $defaults
|
|
& $logAction "Defaults saved to $(Get-DefaultsPath)."
|
|
if (-not [string]::IsNullOrWhiteSpace($pfxPasswordBox.Text)) {
|
|
& $logAction "Warning: PFX password is stored in plaintext."
|
|
}
|
|
})
|
|
|
|
$clearBtn.Add_Click({
|
|
$logBox.Clear()
|
|
})
|
|
|
|
$runBtn.Add_Click({
|
|
$runBtn.Enabled = $false
|
|
try {
|
|
$hosts = @()
|
|
$hosts += Split-List $hostsBox.Text
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($fileBox.Text)) {
|
|
if (-not (Test-Path -Path $fileBox.Text -PathType Leaf)) {
|
|
throw "Hostnames file not found: $($fileBox.Text)"
|
|
}
|
|
$fileHosts = Get-Content -Path $fileBox.Text | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
$hosts += $fileHosts
|
|
}
|
|
|
|
if ($hosts.Count -eq 0) { throw "No hostnames provided." }
|
|
$zone = $zoneBox.Text.Trim()
|
|
if (-not $zone) { throw "Default DNS zone is required." }
|
|
|
|
$targetIp = $ipBox.Text.Trim()
|
|
if (-not $targetIp) { throw "Target IPv4 is required." }
|
|
|
|
$selectedReplicationTargets = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() })
|
|
$dnsServer = $dnsServerBox.Text.Trim()
|
|
if (-not $dnsServer -and $selectedReplicationTargets.Count -gt 0) {
|
|
$dnsServer = $selectedReplicationTargets[0]
|
|
}
|
|
if (-not $dnsServer) { throw "Primary DNS server is required." }
|
|
|
|
$outputType = $outputTypeBox.SelectedItem.ToString()
|
|
$outputPath = $outputPathBox.Text.Trim()
|
|
if (-not $outputPath) { throw "Output path is required." }
|
|
if ($outputType -eq "PFX" -and [string]::IsNullOrWhiteSpace($pfxPasswordBox.Text)) {
|
|
throw "PFX password is required."
|
|
}
|
|
|
|
$hostEntries = @($hosts | ForEach-Object { Resolve-HostEntry -Name $_ -Zone $zone -UseProvidedFqdn $useFqdnBox.Checked } | Where-Object { $_ })
|
|
|
|
& $logAction "Processing $($hostEntries.Count) hostname(s)."
|
|
|
|
foreach ($entry in $hostEntries) {
|
|
Ensure-ARecord -Zone $zone -HostLabel $entry.HostLabel -TargetIp $targetIp -DnsServer $dnsServer -Log $logAction
|
|
}
|
|
|
|
if ($replicationEnabledBox.Checked) {
|
|
$replicationDelaySeconds = 0
|
|
$delayRaw = $replicationDelayBox.Text.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($delayRaw)) {
|
|
if (-not [int]::TryParse($delayRaw, [ref]$replicationDelaySeconds) -or $replicationDelaySeconds -lt 0) {
|
|
throw "Replication wait seconds must be a non-negative integer."
|
|
}
|
|
}
|
|
if ($selectedReplicationTargets.Count -gt 0) {
|
|
$replicationTargets = $selectedReplicationTargets
|
|
} else {
|
|
$replicationTargets = @(Split-List $replicationTargetsBox.Text)
|
|
if ($replicationTargets.Count -eq 0 -and $dnsServer) {
|
|
$replicationTargets = @($dnsServer)
|
|
& $logAction "Replication targets empty; using primary DNS server $dnsServer."
|
|
}
|
|
}
|
|
$replicationCredential = $null
|
|
if ($replicationRemoteBox.Checked) {
|
|
$replicationCredential = Load-ReplicationCredential
|
|
if (-not $replicationCredential) {
|
|
$replicationCredential = Get-Credential -Message "Enter credentials for replication targets."
|
|
if (-not $replicationCredential) {
|
|
& $logAction "Replication canceled: credentials not provided."
|
|
return
|
|
}
|
|
Save-ReplicationCredential -Credential $replicationCredential
|
|
& $logAction "Replication credentials saved for this user."
|
|
}
|
|
}
|
|
Invoke-Replication `
|
|
-Servers $replicationTargets `
|
|
-Command $replicationCmdBox.Text `
|
|
-UseRemote $replicationRemoteBox.Checked `
|
|
-Credential $replicationCredential `
|
|
-SourceDc $dnsServer `
|
|
-Log $logAction
|
|
if ($replicationDelaySeconds -gt 0) {
|
|
& $logAction "Waiting $replicationDelaySeconds seconds for replication."
|
|
Start-Sleep -Seconds $replicationDelaySeconds
|
|
}
|
|
} else {
|
|
& $logAction "Replication disabled."
|
|
}
|
|
|
|
if ($disableCertsBox.Checked) {
|
|
& $logAction "Cert generation disabled; DNS updates/replication only."
|
|
} else {
|
|
$wacsPath = $wacsPathBox.Text.Trim()
|
|
if (-not (Test-Path -Path $wacsPath -PathType Leaf)) {
|
|
throw "WACS not found at: $wacsPath"
|
|
}
|
|
if ($perHostBox.Checked) {
|
|
foreach ($entry in $hostEntries) {
|
|
& $logAction "Requesting certificate for $($entry.Fqdn)."
|
|
Invoke-Wacs `
|
|
-WacsPath $wacsPath `
|
|
-HostFqdns @($entry.Fqdn) `
|
|
-OutputType $outputType `
|
|
-OutputPath $outputPath `
|
|
-PfxPassword $pfxPasswordBox.Text `
|
|
-BaseUri $baseUriBox.Text.Trim() `
|
|
-Validation $validationBox.Text.Trim() `
|
|
-ValidationPort $validationPortBox.Text.Trim() `
|
|
-Verbose $verboseBox.Checked `
|
|
-Log $logAction
|
|
}
|
|
} else {
|
|
$hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ })
|
|
& $logAction "Requesting one certificate with $($hostList.Count) hostname(s)."
|
|
Invoke-Wacs `
|
|
-WacsPath $wacsPath `
|
|
-HostFqdns $hostList `
|
|
-OutputType $outputType `
|
|
-OutputPath $outputPath `
|
|
-PfxPassword $pfxPasswordBox.Text `
|
|
-BaseUri $baseUriBox.Text.Trim() `
|
|
-Validation $validationBox.Text.Trim() `
|
|
-ValidationPort $validationPortBox.Text.Trim() `
|
|
-Verbose $verboseBox.Checked `
|
|
-Log $logAction
|
|
}
|
|
}
|
|
|
|
& $logAction "Done."
|
|
} catch {
|
|
& $logAction "Error: $($_.Exception.Message)"
|
|
} finally {
|
|
$runBtn.Enabled = $true
|
|
}
|
|
})
|
|
|
|
[void]$form.Add_Load({
|
|
try { Apply-Layout } catch {}
|
|
})
|
|
[void]$form.Add_Shown({
|
|
try { $form.BeginInvoke([Action]{ try { Apply-Layout } catch {} }) } catch {}
|
|
})
|
|
[void]$panel.Add_SizeChanged({
|
|
try { Apply-Layout } catch {}
|
|
})
|
|
[void]$form.Add_Resize({
|
|
try { Apply-Layout } catch {}
|
|
})
|
|
|
|
[void]$form.Add_FormClosed({
|
|
try { $logoBox.Image = $null } catch {}
|
|
try { $logoLarge.Dispose() } catch {}
|
|
try { $logoSmall.Dispose() } catch {}
|
|
})
|
|
|
|
[void]$form.ShowDialog()
|