Files
123123/ZA.CoreService.ESBCertificateManager/Form1.cs
T
GizzlerandCursor 12eebfc78a Simplify app to certificate recognition and MfApi restart only.
Remove XApi, WinRM, LocalCmd, HTTP API, TLS probe, SQL deploy model, and deploy UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:06:10 +02:00

650 lines
22 KiB
C#
Raw 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.
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ZA.CoreService.ESBCertificateManager.Configuration;
using ZA.CoreService.ESBCertificateManager.Data;
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager;
public partial class Form1 : Form
{
private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34);
private readonly Color SidebarColor = Color.FromArgb(7, 23, 45);
private readonly Color CardColor = Color.FromArgb(16, 38, 65);
private readonly Color BorderColor = Color.FromArgb(36, 74, 117);
private readonly Color BlueColor = Color.FromArgb(0, 110, 182);
private readonly Color GoldColor = Color.FromArgb(208, 171, 57);
private readonly Color TextColor = Color.FromArgb(241, 245, 249);
private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200);
private readonly Color GreenColor = Color.FromArgb(60, 203, 127);
private readonly Color RedColor = Color.FromArgb(239, 106, 106);
private readonly AppSettings _settings;
private readonly DeploymentOrchestrator _orchestrator;
private readonly SonicContainerDiscovery _sonicDiscovery;
private TextBox txtCertificatePath = null!;
private Label lblCertificateSubject = null!;
private Label lblCertificateIssuer = null!;
private Label lblCertificateExpiry = null!;
private Label lblCertificateFingerprint = null!;
private Label lblCertificateValidity = null!;
private Label lblStatus = null!;
private Label lblSonicStatus = null!;
private ComboBox cmbSonicConnection = null!;
private DataGridView dgvTargets = null!;
private Button btnRestartOnly = null!;
private Button btnCancelRun = null!;
private CertificateInfo? _loadedCertificateInfo;
private IReadOnlyList<DeploymentTarget> _loadedTargets = [];
private bool _isOperationRunning;
private CancellationTokenSource? _runCts;
public Form1()
{
InitializeComponent();
_settings = AppSettingsLoader.Load();
_orchestrator = new DeploymentOrchestrator(_settings);
_sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections);
BuildUi();
Shown += async (_, _) => await LoadTargetsAsync();
}
private void BuildUi()
{
SuspendLayout();
Text = "ESB Certificate Manager";
FormBorderStyle = FormBorderStyle.Sizable;
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(960, 640);
Size = new Size(1100, 720);
BackColor = BackgroundColor;
ForeColor = TextColor;
Font = new Font("Segoe UI", 9.5f);
Panel sidebar = new()
{
Dock = DockStyle.Left,
Width = 220,
BackColor = SidebarColor,
Padding = new Padding(16)
};
sidebar.Controls.Add(new Label
{
Text = "ESB Certificate\nManager",
Dock = DockStyle.Top,
Height = 70,
Font = new Font("Segoe UI Semibold", 14f),
ForeColor = GoldColor
});
sidebar.Controls.Add(new Label
{
Text = "1. Zertifikat erkennen\n2. Container neu starten",
Dock = DockStyle.Top,
Height = 80,
ForeColor = MutedTextColor
});
Panel main = new()
{
Dock = DockStyle.Fill,
Padding = new Padding(20),
BackColor = BackgroundColor
};
TableLayoutPanel layout = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
BackColor = BackgroundColor
};
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 160));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 56));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 70));
layout.Controls.Add(BuildCertificateCard(), 0, 0);
layout.Controls.Add(BuildSonicBar(), 0, 1);
layout.Controls.Add(BuildTargetsCard(), 0, 2);
layout.Controls.Add(BuildActionBar(), 0, 3);
main.Controls.Add(layout);
Controls.Add(main);
Controls.Add(sidebar);
ResumeLayout();
}
private Panel BuildCertificateCard()
{
Panel card = MakeCard();
card.Controls.Add(SectionTitle("Zertifikat erkennen", 12));
txtCertificatePath = new TextBox
{
Left = 16,
Top = 48,
Width = 620,
Height = 28,
ReadOnly = true,
BackColor = Color.FromArgb(12, 28, 48),
ForeColor = TextColor,
BorderStyle = BorderStyle.FixedSingle
};
Button btnSelect = MakeButton("Datei wählen…", BlueColor);
btnSelect.Left = 650;
btnSelect.Top = 46;
btnSelect.Width = 140;
btnSelect.Click += (_, _) => TrySelectCertificateFile();
lblCertificateSubject = MetaLabel(16, 90, "Subject: ");
lblCertificateIssuer = MetaLabel(16, 112, "Issuer: ");
lblCertificateExpiry = MetaLabel(400, 90, "Gültig bis: ");
lblCertificateFingerprint = MetaLabel(400, 112, "SHA-256: ");
lblCertificateValidity = MetaLabel(16, 134, "Status: ");
card.Controls.AddRange([
txtCertificatePath, btnSelect,
lblCertificateSubject, lblCertificateIssuer,
lblCertificateExpiry, lblCertificateFingerprint, lblCertificateValidity
]);
return card;
}
private Panel BuildSonicBar()
{
Panel bar = MakeCard();
Label lbl = new()
{
Text = "Sonic:",
Left = 16,
Top = 16,
AutoSize = true,
ForeColor = MutedTextColor
};
cmbSonicConnection = new ComboBox
{
Left = 70,
Top = 12,
Width = 200,
DropDownStyle = ComboBoxStyle.DropDownList,
BackColor = Color.FromArgb(12, 28, 48),
ForeColor = TextColor,
FlatStyle = FlatStyle.Flat
};
foreach (SonicConnection c in _settings.SonicConnections)
{
cmbSonicConnection.Items.Add(c.Name);
}
if (cmbSonicConnection.Items.Count > 0)
{
cmbSonicConnection.SelectedIndex = 0;
}
Button btnLoad = MakeButton("Container laden", BlueColor);
btnLoad.Left = 290;
btnLoad.Top = 10;
btnLoad.Width = 140;
btnLoad.Click += async (_, _) => await LoadFromSonicAsync();
lblSonicStatus = new Label
{
Left = 450,
Top = 16,
AutoSize = true,
ForeColor = MutedTextColor,
Text = "MfApi"
};
bar.Controls.AddRange([lbl, cmbSonicConnection, btnLoad, lblSonicStatus]);
return bar;
}
private Panel BuildTargetsCard()
{
Panel card = MakeCard();
card.Controls.Add(SectionTitle("Ziele / Container", 12));
dgvTargets = new DataGridView
{
Left = 16,
Top = 44,
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
Width = 780,
Height = 220,
BackgroundColor = Color.FromArgb(12, 28, 48),
ForeColor = TextColor,
GridColor = BorderColor,
BorderStyle = BorderStyle.None,
RowHeadersVisible = false,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
ReadOnly = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
EnableHeadersVisualStyles = false
};
dgvTargets.ColumnHeadersDefaultCellStyle.BackColor = SidebarColor;
dgvTargets.ColumnHeadersDefaultCellStyle.ForeColor = GoldColor;
dgvTargets.DefaultCellStyle.BackColor = Color.FromArgb(12, 28, 48);
dgvTargets.DefaultCellStyle.ForeColor = TextColor;
dgvTargets.DefaultCellStyle.SelectionBackColor = BlueColor;
dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn
{
Name = "Selected",
HeaderText = "",
Width = 40,
FillWeight = 15
});
dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Name", HeaderText = "Name", ReadOnly = true });
dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Container", HeaderText = "Container", ReadOnly = true });
dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Connection", HeaderText = "Verbindung", ReadOnly = true });
dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Status", HeaderText = "Status", ReadOnly = true });
dgvTargets.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "TargetId",
Visible = false
});
card.Resize += (_, _) =>
{
dgvTargets.Width = Math.Max(200, card.ClientSize.Width - 32);
dgvTargets.Height = Math.Max(80, card.ClientSize.Height - 60);
};
card.Controls.Add(dgvTargets);
return card;
}
private Panel BuildActionBar()
{
Panel bar = MakeCard();
btnRestartOnly = MakeButton("ESB neu starten", GoldColor);
btnRestartOnly.ForeColor = Color.Black;
btnRestartOnly.Left = 16;
btnRestartOnly.Top = 14;
btnRestartOnly.Width = 180;
btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync();
btnCancelRun = MakeButton("Abbrechen", RedColor);
btnCancelRun.Left = 210;
btnCancelRun.Top = 14;
btnCancelRun.Width = 120;
btnCancelRun.Enabled = false;
btnCancelRun.Click += (_, _) => _runCts?.Cancel();
lblStatus = new Label
{
Left = 350,
Top = 20,
AutoSize = true,
ForeColor = MutedTextColor,
Text = "Bereit."
};
bar.Controls.AddRange([btnRestartOnly, btnCancelRun, lblStatus]);
return bar;
}
private async Task LoadTargetsAsync()
{
try
{
(ITargetRepository repo, string msg) = await TargetRepositoryFactory.CreateAsync(_settings);
_loadedTargets = await repo.GetActiveTargetsAsync();
BindTargets(_loadedTargets);
SetStatus(msg, false);
}
catch (Exception ex)
{
SetStatus($"Ziele laden fehlgeschlagen: {ex.Message}", true);
}
}
private async Task LoadFromSonicAsync()
{
if (cmbSonicConnection.SelectedItem is not string name)
{
SetStatus("Keine Sonic-Verbindung gewählt.", true);
return;
}
SetStatus($"Lade Container von '{name}'…", false);
SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync(name, _loadedTargets);
if (!result.Success)
{
SetStatus(result.ErrorMessage ?? "Discovery fehlgeschlagen", true);
return;
}
_loadedTargets = result.DiscoveredTargets;
BindTargets(_loadedTargets);
lblSonicStatus.Text = $"Domain={result.DomainName}; {result.DiscoveredTargets.Count} Container";
SetStatus(result.ErrorMessage ?? $"Container geladen ({result.DiscoveredTargets.Count}).", false);
}
private void BindTargets(IReadOnlyList<DeploymentTarget> targets)
{
dgvTargets.Rows.Clear();
foreach (DeploymentTarget t in targets)
{
int row = dgvTargets.Rows.Add(true, t.Name, t.ContainerName, t.SonicConnectionName, "bereit", t.Id);
dgvTargets.Rows[row].Tag = t;
}
}
private List<DeploymentTarget> GetSelectedTargets()
=> dgvTargets.Rows.Cast<DataGridViewRow>()
.Where(r => !r.IsNewRow && r.Cells["Selected"].Value is true)
.Select(r => r.Tag)
.OfType<DeploymentTarget>()
.ToList();
private async Task RunRestartOnlyAsync()
{
if (_isOperationRunning)
{
return;
}
List<DeploymentTarget> selected = GetSelectedTargets();
PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected);
if (!preflight.IsValid)
{
MessageBox.Show(
this,
string.Join(Environment.NewLine, preflight.Issues.Select(i => i.Message)),
"Neustart blockiert",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
string names = string.Join(", ", selected.Select(t => t.ContainerName));
if (MessageBox.Show(
this,
$"Container wirklich neu starten?\n\n{names}",
"ESB neu starten",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning) != DialogResult.Yes)
{
return;
}
_runCts?.Dispose();
_runCts = new CancellationTokenSource();
SetOperationRunning(true);
SetStatus($"Neustart läuft ({selected.Count})…", false);
Progress<TargetProgressUpdate> progress = new(u =>
{
SetTargetRowStatus(u.TargetId, u.StatusText);
SetStatus(u.StatusText, u.SuccessHint == false);
});
try
{
DeploymentRunResult run = await _orchestrator.RestartOnlyAsync(selected, progress, _runCts.Token);
foreach (TargetStepResult r in run.TargetResults)
{
SetTargetRowStatus(r.TargetId, r.StatusText);
}
string details = string.Join(
Environment.NewLine + Environment.NewLine,
run.TargetResults.Select(r =>
$"{r.TargetName}: {r.StatusText}" +
(string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail)));
SetStatus(
run.OverallSuccess ? "Neustart erfolgreich." : "Neustart fehlgeschlagen.",
!run.OverallSuccess);
MessageBox.Show(
this,
details,
run.OverallSuccess ? "Neustart erfolgreich" : "Neustart fehlgeschlagen",
MessageBoxButtons.OK,
run.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
}
catch (OperationCanceledException)
{
SetStatus("Neustart abgebrochen.", true);
}
catch (Exception ex)
{
SetStatus(ex.Message, true);
MessageBox.Show(this, ex.Message, "Neustart", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetOperationRunning(false);
_runCts?.Dispose();
_runCts = null;
}
}
private void SetTargetRowStatus(int targetId, string status)
{
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is DeploymentTarget t && t.Id == targetId)
{
row.Cells["Status"].Value = status;
break;
}
}
}
private void SetOperationRunning(bool running)
{
_isOperationRunning = running;
btnRestartOnly.Enabled = !running;
btnCancelRun.Enabled = running;
dgvTargets.Enabled = !running;
}
private void SetStatus(string text, bool isError)
{
lblStatus.Text = text;
lblStatus.ForeColor = isError ? RedColor : MutedTextColor;
}
private void TrySelectCertificateFile()
{
using OpenFileDialog dialog = new()
{
Title = "Zertifikat wählen",
Filter = "Zertifikate (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|Alle Dateien (*.*)|*.*"
};
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
string path = dialog.FileName;
if (!IsSupportedCertificateFile(path))
{
MessageBox.Show(this, "Dateityp wird nicht unterstützt (.cer/.crt/.pem/.pfx).", "Zertifikat",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
CertificateInfo info = ReadCertificateWithOptionalPassword(path);
txtCertificatePath.Text = path;
_loadedCertificateInfo = info;
DisplayCertificateInfo(info);
SetStatus("Zertifikat erkannt.", false);
}
catch (Exception ex)
{
_loadedCertificateInfo = null;
SetStatus($"Zertifikat ungültig: {ex.Message}", true);
MessageBox.Show(this, ex.Message, "Zertifikat", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private CertificateInfo ReadCertificateWithOptionalPassword(string path)
{
string ext = Path.GetExtension(path);
if (!ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase))
{
return ReadCertificate(path, null);
}
try
{
return ReadCertificate(path, null);
}
catch (CryptographicException)
{
string? pwd = PromptPassword();
if (pwd is null)
{
throw new InvalidOperationException("PFX-Passwort abgebrochen.");
}
return ReadCertificate(path, pwd);
}
}
private string? PromptPassword()
{
using Form dialog = new()
{
Text = "PFX-Passwort",
FormBorderStyle = FormBorderStyle.FixedDialog,
StartPosition = FormStartPosition.CenterParent,
ClientSize = new Size(360, 140),
MaximizeBox = false,
MinimizeBox = false,
BackColor = CardColor,
ForeColor = TextColor
};
Label label = new() { Text = "Passwort:", Left = 16, Top = 20, AutoSize = true };
TextBox txt = new()
{
Left = 16,
Top = 48,
Width = 320,
UseSystemPasswordChar = true
};
Button ok = new() { Text = "OK", DialogResult = DialogResult.OK, Left = 160, Top = 90, Width = 80 };
Button cancel = new() { Text = "Abbrechen", DialogResult = DialogResult.Cancel, Left = 250, Top = 90, Width = 90 };
dialog.Controls.AddRange([label, txt, ok, cancel]);
dialog.AcceptButton = ok;
dialog.CancelButton = cancel;
return dialog.ShowDialog(this) == DialogResult.OK ? txt.Text : null;
}
private void DisplayCertificateInfo(CertificateInfo info)
{
lblCertificateSubject.Text = "Subject: " + info.Subject;
lblCertificateIssuer.Text = "Issuer: " + info.Issuer;
lblCertificateExpiry.Text = "Gültig bis: " + info.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm");
lblCertificateFingerprint.Text = "SHA-256: " + info.FingerprintSha256;
lblCertificateValidity.Text = info.IsCurrentlyValid ? "Status: GÜLTIG" : "Status: UNGÜLTIG / ABGELAUFEN";
lblCertificateValidity.ForeColor = info.IsCurrentlyValid ? GreenColor : RedColor;
}
private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword)
{
string extension = Path.GetExtension(certificatePath);
using X509Certificate2 certificate =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
? new X509Certificate2(certificatePath, pfxPassword, X509KeyStorageFlags.EphemeralKeySet)
: new X509Certificate2(certificatePath);
string subject = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false);
string issuer = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: true);
if (string.IsNullOrWhiteSpace(subject)) subject = certificate.Subject;
if (string.IsNullOrWhiteSpace(issuer)) issuer = certificate.Issuer;
string fingerprint = FormatFingerprint(certificate.GetCertHashString(HashAlgorithmName.SHA256));
DateTimeOffset validFrom = new(certificate.NotBefore);
DateTimeOffset validUntil = new(certificate.NotAfter);
DateTimeOffset now = DateTimeOffset.Now;
return new CertificateInfo
{
Subject = subject,
Issuer = issuer,
ValidFrom = validFrom,
ValidUntil = validUntil,
FingerprintSha256 = fingerprint,
IsCurrentlyValid = now >= validFrom && now <= validUntil
};
}
private static string FormatFingerprint(string fingerprint)
=> string.IsNullOrWhiteSpace(fingerprint)
? string.Empty
: string.Join(":", Enumerable.Range(0, fingerprint.Length / 2)
.Select(i => fingerprint.Substring(i * 2, 2)));
private static bool IsSupportedCertificateFile(string filePath)
{
if (!File.Exists(filePath))
{
return false;
}
string ext = Path.GetExtension(filePath);
return ext.Equals(".cer", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".crt", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".pem", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase);
}
private Panel MakeCard()
=> new()
{
Dock = DockStyle.Fill,
BackColor = CardColor,
Padding = new Padding(8),
Margin = new Padding(0, 0, 0, 10)
};
private Label SectionTitle(string text, int top)
=> new()
{
Text = text,
Left = 16,
Top = top,
AutoSize = true,
Font = new Font("Segoe UI Semibold", 11f),
ForeColor = GoldColor
};
private Label MetaLabel(int left, int top, string text)
=> new()
{
Text = text,
Left = left,
Top = top,
AutoSize = true,
ForeColor = MutedTextColor
};
private Button MakeButton(string text, Color back)
=> new()
{
Text = text,
Height = 32,
FlatStyle = FlatStyle.Flat,
BackColor = back,
ForeColor = TextColor,
FlatAppearance = { BorderSize = 0 },
Cursor = Cursors.Hand
};
}