Improve certificate path probing with demo UNC target and remove Sonic container scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 09:14:25 +02:00
co-authored by Cursor
parent f7e9bd43e2
commit f584fd68fe
16 changed files with 1026 additions and 999 deletions
@@ -268,12 +268,35 @@ BEGIN
1; 1;
END; END;
/* Demo-Pfad für Zertifikat-Austausch (UNC server.p12) immer sicherstellen */
DECLARE @DemoDirectory [nvarchar](500) =
N'\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs';
DECLARE @DemoFileName [nvarchar](260) = N'server.p12';
UPDATE [dbo].[CertificateTarget]
SET
[TargetDirectory] = @DemoDirectory,
[TargetFileName] = @DemoFileName,
[BackupEnabled] = 1,
[BackupDirectoryName] = N'Backup',
[IsActive] = 1,
[ModifiedDateTime] = SYSUTCDATETIME(),
[ModifiedBy] = SUSER_SNAME()
WHERE [SonicContainerId] = @SonicContainerId
AND
(
[TargetFileName] IN (N'server.crt', N'server.p12')
OR [TargetDirectory] LIKE N'C:\ESB\Certificates%'
);
IF NOT EXISTS IF NOT EXISTS
( (
SELECT 1 SELECT 1
FROM [dbo].[CertificateTarget] AS target FROM [dbo].[CertificateTarget] AS target
WHERE target.[SonicContainerId] = @SonicContainerId WHERE target.[SonicContainerId] = @SonicContainerId
AND target.[TargetFileName] = N'server.crt' AND target.[TargetDirectory] = @DemoDirectory
AND target.[TargetFileName] = @DemoFileName
AND target.[IsActive] = 1
) )
BEGIN BEGIN
INSERT INTO [dbo].[CertificateTarget] INSERT INTO [dbo].[CertificateTarget]
@@ -290,8 +313,8 @@ BEGIN
VALUES VALUES
( (
@SonicContainerId, @SonicContainerId,
N'C:\ESB\Certificates', @DemoDirectory,
N'server.crt', @DemoFileName,
1, 1,
N'Backup', N'Backup',
1, 1,
@@ -0,0 +1,106 @@
/*
COMMENT: Stellt den Demo-Zertifikatspfad für DE-Test / ct-ZADBService sicher.
Kann allein gegen eine bestehende EsbZertifikatManager-DB laufen
(ohne Drop/Create). EnvironmentCode muss TEST sein (appsettings).
*/
USE [EsbZertifikatManager];
GO
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @SonicContainerId [int];
DECLARE @TargetDirectory [nvarchar](500) =
N'\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs';
DECLARE @TargetFileName [nvarchar](260) = N'server.p12';
SELECT @SonicContainerId = container.[SonicContainerId]
FROM [dbo].[SonicContainer] AS container
INNER JOIN [dbo].[SonicConnection] AS connection
ON connection.[SonicConnectionId] = container.[SonicConnectionId]
INNER JOIN [dbo].[Environment] AS environment
ON environment.[EnvironmentId] = connection.[EnvironmentId]
WHERE environment.[EnvironmentCode] = N'TEST'
AND connection.[ConnectionName] = N'DE-Test'
AND container.[ContainerName] = N'ct-ZADBService';
IF @SonicContainerId IS NULL
BEGIN
THROW 50010,
N'Demo-Pfad: Container ct-ZADBService / DE-Test / TEST nicht gefunden. Zuerst 03_Seed.sql ausführen.',
1;
END;
/* Alte Platzhalter am gleichen Container auf Demo-Pfad umbiegen */
UPDATE [dbo].[CertificateTarget]
SET
[TargetDirectory] = @TargetDirectory,
[TargetFileName] = @TargetFileName,
[BackupEnabled] = 1,
[BackupDirectoryName] = N'Backup',
[IsActive] = 1,
[ModifiedDateTime] = SYSUTCDATETIME(),
[ModifiedBy] = SUSER_SNAME()
WHERE [SonicContainerId] = @SonicContainerId
AND
(
[TargetFileName] IN (N'server.crt', N'server.p12')
OR [TargetDirectory] LIKE N'C:\ESB\Certificates%'
);
IF NOT EXISTS
(
SELECT 1
FROM [dbo].[CertificateTarget] AS target
WHERE target.[SonicContainerId] = @SonicContainerId
AND target.[TargetDirectory] = @TargetDirectory
AND target.[TargetFileName] = @TargetFileName
AND target.[IsActive] = 1
)
BEGIN
INSERT INTO [dbo].[CertificateTarget]
(
[SonicContainerId],
[TargetDirectory],
[TargetFileName],
[BackupEnabled],
[BackupDirectoryName],
[IsActive],
[CreationDateTime],
[CreatedBy]
)
VALUES
(
@SonicContainerId,
@TargetDirectory,
@TargetFileName,
1,
N'Backup',
1,
SYSUTCDATETIME(),
SUSER_SNAME()
);
END;
SELECT
target.[CertificateTargetId],
company.[CompanyCode],
environment.[EnvironmentCode],
connection.[ConnectionName],
container.[ContainerName],
target.[TargetDirectory],
target.[TargetFileName],
target.[IsActive]
FROM [dbo].[CertificateTarget] AS target
INNER JOIN [dbo].[SonicContainer] AS container
ON container.[SonicContainerId] = target.[SonicContainerId]
INNER JOIN [dbo].[SonicConnection] AS connection
ON connection.[SonicConnectionId] = container.[SonicConnectionId]
INNER JOIN [dbo].[Environment] AS environment
ON environment.[EnvironmentId] = connection.[EnvironmentId]
INNER JOIN [dbo].[Company] AS company
ON company.[CompanyId] = container.[CompanyId]
WHERE container.[SonicContainerId] = @SonicContainerId;
PRINT N'Demo-Pfad ist aktiv: \\dekun-painwbdet\e$\...\certs\server.p12';
GO
@@ -32,9 +32,20 @@ cd Database\AlwaysEncrypted
| Environment | TEST2, TEST, PROD | | Environment | TEST2, TEST, PROD |
| SonicConnection | DE-Test → `tcp://dekun-painwbdet:13070`, Domain `proalpha-test`, Env TEST | | SonicConnection | DE-Test → `tcp://dekun-painwbdet:13070`, Domain `proalpha-test`, Env TEST |
| SonicContainer | ct-ZADBService (Company DE) | | SonicContainer | ct-ZADBService (Company DE) |
| CertificateTarget | `C:\ESB\Certificates\server.crt` (Platzhalter) | | CertificateTarget | Demo-UNC `\\dekun-painwbdet\e$\...\certs\server.p12` |
| SonicCredential | **nicht** geseedet (Setup-Wizard / AE) | | SonicCredential | **nicht** geseedet (Setup-Wizard / AE) |
## Demo-Pfad nachziehen (bestehende DB)
Wenn der alte Platzhalter noch in SQL liegt:
```text
04_EnsureDemoPath.sql
```
Zusätzlich: in `appsettings.json` ist `"Demo": { "Enabled": true, ... }`
die App zeigt den Demo-Pfad auch ohne SQL-Update in der Zielliste (Kopieren testen).
## Always Encrypted ## Always Encrypted
| Objekt | Name | | Objekt | Name |
+156 -23
View File
@@ -900,7 +900,7 @@ namespace ZA.CoreService.ESBCertificateManager
Label hint = new Label Label hint = new Label
{ {
Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx", Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx, .p12",
ForeColor = MutedTextColor, ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8.5f), Font = new Font("Segoe UI", 8.5f),
AutoSize = true, AutoSize = true,
@@ -920,11 +920,11 @@ namespace ZA.CoreService.ESBCertificateManager
{ {
Title = "Zertifikatsdatei auswählen", Title = "Zertifikatsdatei auswählen",
Filter = Filter =
"Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|" + "Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx;*.p12)|*.cer;*.crt;*.pem;*.pfx;*.p12|" +
"CER-Zertifikate (*.cer)|*.cer|" + "CER-Zertifikate (*.cer)|*.cer|" +
"CRT-Zertifikate (*.crt)|*.crt|" + "CRT-Zertifikate (*.crt)|*.crt|" +
"PEM-Zertifikate (*.pem)|*.pem|" + "PEM-Zertifikate (*.pem)|*.pem|" +
"PFX-Zertifikate (*.pfx)|*.pfx", "PFX/P12-Zertifikate (*.pfx;*.p12)|*.pfx;*.p12",
FilterIndex = 1, FilterIndex = 1,
Multiselect = false, Multiselect = false,
CheckFileExists = true, CheckFileExists = true,
@@ -947,7 +947,7 @@ namespace ZA.CoreService.ESBCertificateManager
MessageBox.Show( MessageBox.Show(
this, this,
"Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" + "Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" +
".cer, .crt, .pem oder .pfx", ".cer, .crt, .pem, .pfx oder .p12",
"Nicht unterstütztes Dateiformat", "Nicht unterstütztes Dateiformat",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Warning); MessageBoxIcon.Warning);
@@ -971,15 +971,17 @@ namespace ZA.CoreService.ESBCertificateManager
private CertificateInfo? TryReadSelectedCertificate(string selectedPath) private CertificateInfo? TryReadSelectedCertificate(string selectedPath)
{ {
bool isPfx = Path.GetExtension(selectedPath) string extension = Path.GetExtension(selectedPath);
.Equals(".pfx", StringComparison.OrdinalIgnoreCase); bool needsPassword =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
try try
{ {
// Zuerst ohne Passwort versuchen // Zuerst ohne Passwort versuchen
return ReadCertificate(selectedPath, null); return ReadCertificate(selectedPath, null);
} }
catch (CryptographicException) when (isPfx) catch (CryptographicException) when (needsPassword)
{ {
// Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist // Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist
string? pfxPassword = PromptForPfxPassword(); string? pfxPassword = PromptForPfxPassword();
@@ -1141,8 +1143,12 @@ namespace ZA.CoreService.ESBCertificateManager
string extension = Path.GetExtension(certificatePath); string extension = Path.GetExtension(certificatePath);
using X509Certificate2 certificate = bool passwordProtected =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
using X509Certificate2 certificate =
passwordProtected
? new X509Certificate2( ? new X509Certificate2(
certificatePath, certificatePath,
pfxPassword, pfxPassword,
@@ -1245,7 +1251,8 @@ namespace ZA.CoreService.ESBCertificateManager
".cer", ".cer",
".crt", ".crt",
".pem", ".pem",
".pfx" ".pfx",
".p12"
}; };
return supportedExtensions.Contains( return supportedExtensions.Contains(
@@ -1389,9 +1396,9 @@ namespace ZA.CoreService.ESBCertificateManager
}); });
dgvTargets.Columns.Add("Target", "Ziel"); dgvTargets.Columns.Add("Target", "Ziel");
dgvTargets.Columns.Add("Environment", "Umgebung"); dgvTargets.Columns.Add("Path", "Zertifikat-Pfad");
dgvTargets.Columns.Add("Container", "Container / Service"); dgvTargets.Columns.Add("Expiry", "Aktuelles Zertifikat");
dgvTargets.Columns.Add("Tls", "TLS-Prüfung"); dgvTargets.Columns.Add("Container", "Neustart-Container");
dgvTargets.Columns.Add("Status", "Status"); dgvTargets.Columns.Add("Status", "Status");
dgvTargets.CurrentCellDirtyStateChanged += (_, _) => dgvTargets.CurrentCellDirtyStateChanged += (_, _) =>
@@ -1527,10 +1534,12 @@ namespace ZA.CoreService.ESBCertificateManager
"Bereitstellungsziele werden aus SQL geladen ...", "Bereitstellungsziele werden aus SQL geladen ...",
isError: false); isError: false);
_loadedTargets = IReadOnlyList<DeploymentTarget> fromSql =
await _deploymentTargetRepository.GetActiveAsync( await _deploymentTargetRepository.GetActiveAsync(
_settings.EnvironmentCode); _settings.EnvironmentCode);
_loadedTargets = EnsureDemoTarget(fromSql);
BindTargetsToGrid(_loadedTargets); BindTargetsToGrid(_loadedTargets);
if (_loadedTargets.Count == 0) if (_loadedTargets.Count == 0)
@@ -1542,9 +1551,38 @@ namespace ZA.CoreService.ESBCertificateManager
return; return;
} }
// Demo: erstes Ziel vorauswählen
if (dgvTargets.Rows.Count > 0)
{
dgvTargets.Rows[0].Cells["Selected"].Value = true;
}
SetStatus( SetStatus(
$"{_loadedTargets.Count} Bereitstellungsziel(e) aus SQL geladen.", $"{_loadedTargets.Count} Bereitstellungsziel(e) geladen"
+ (_settings.Demo.Enabled
? $" · Demo: {Path.Combine(_settings.Demo.TargetDirectory, _settings.Demo.TargetFileName)}"
: string.Empty),
isError: false); isError: false);
await ProbeTargetCertificatesAsync();
}
catch (Exception ex) when (_settings.Demo.Enabled)
{
// Demo trotzdem möglich, wenn SQL kurz nicht erreichbar ist
_loadedTargets = EnsureDemoTarget([]);
BindTargetsToGrid(_loadedTargets);
if (dgvTargets.Rows.Count > 0)
{
dgvTargets.Rows[0].Cells["Selected"].Value = true;
}
SetStatus(
$"SQL-Ziele nicht geladen ({ex.Message}). "
+ "Demo-Pfad ist trotzdem verfügbar.",
isError: true);
await ProbeTargetCertificatesAsync();
} }
catch catch
{ {
@@ -1565,23 +1603,118 @@ namespace ZA.CoreService.ESBCertificateManager
foreach (DeploymentTarget target in targets) foreach (DeploymentTarget target in targets)
{ {
int tlsPort = target.TlsPort is > 0 ? target.TlsPort.Value : 443;
string tls = string.IsNullOrWhiteSpace(target.TlsHost)
? "—"
: $"{target.TlsHost}:{tlsPort}";
int rowIndex = dgvTargets.Rows.Add( int rowIndex = dgvTargets.Rows.Add(
false, false,
target.Name, target.Name,
target.Environment, target.FullTargetPath,
target.ContainerName, "wird geprüft …",
tls, string.IsNullOrWhiteSpace(target.ContainerName)
? "—"
: target.ContainerName,
"Bereit"); "Bereit");
dgvTargets.Rows[rowIndex].Tag = target; dgvTargets.Rows[rowIndex].Tag = target;
} }
} }
private IReadOnlyList<DeploymentTarget> EnsureDemoTarget(
IReadOnlyList<DeploymentTarget> fromSql)
{
if (!_settings.Demo.Enabled)
{
return fromSql;
}
string directory = _settings.Demo.TargetDirectory.Trim();
string fileName = _settings.Demo.TargetFileName.Trim();
if (string.IsNullOrWhiteSpace(directory)
|| string.IsNullOrWhiteSpace(fileName))
{
return fromSql;
}
bool alreadyPresent = fromSql.Any(target =>
string.Equals(
target.TargetDirectory.TrimEnd('\\', '/'),
directory.TrimEnd('\\', '/'),
StringComparison.OrdinalIgnoreCase)
&& string.Equals(
target.CertificateFileName,
fileName,
StringComparison.OrdinalIgnoreCase));
if (alreadyPresent)
{
return fromSql;
}
List<DeploymentTarget> merged = [.. fromSql];
merged.Insert(
0,
new DeploymentTarget
{
Id = -1,
Name = _settings.Demo.DisplayName,
Environment = _settings.EnvironmentCode,
IsActive = true,
TargetDirectory = directory,
CertificateFileName = fileName,
BackupEnabled = true,
BackupDirectoryName = "Backup",
ContainerName = _settings.Demo.ContainerName,
RestartType = RestartType.None,
SonicConnectionName =
_settings.Demo.SonicConnectionName,
RestartTimeoutSeconds = 180
});
return merged;
}
private async Task ProbeTargetCertificatesAsync()
{
CertificateProbeService probe = new();
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is not DeploymentTarget target)
{
continue;
}
CertificateProbeResult result = await Task.Run(
() => probe.Probe(target.FullTargetPath));
if (IsDisposed || !dgvTargets.Columns.Contains("Expiry"))
{
return;
}
if (result.Success && result.Certificate is not null)
{
string expiry = result.Certificate.ValidUntil
.ToLocalTime()
.ToString("dd.MM.yyyy");
row.Cells["Expiry"].Value = result.Certificate.IsCurrentlyValid
? $"gültig bis {expiry}"
: $"abgelaufen {expiry}";
continue;
}
if (result.FileExists)
{
row.Cells["Expiry"].Value = "Datei da · Kennwort nötig";
continue;
}
row.Cells["Expiry"].Value = result.Success
? "Datei fehlt"
: "nicht erreichbar";
}
}
private List<DeploymentTarget> GetSelectedTargets() private List<DeploymentTarget> GetSelectedTargets()
{ {
List<DeploymentTarget> selected = []; List<DeploymentTarget> selected = [];
@@ -11,6 +11,31 @@ public sealed class AppSettings
public RuntimeSettings Runtime { get; set; } = new(); public RuntimeSettings Runtime { get; set; } = new();
public AlwaysEncryptedSettings AlwaysEncrypted { get; set; } = new(); public AlwaysEncryptedSettings AlwaysEncrypted { get; set; } = new();
/// <summary>
/// Wenn aktiv, erscheint der Demo-UNC-Pfad auch ohne SQL-Update in der Zielliste.
/// </summary>
public DemoSettings Demo { get; set; } = new();
}
public sealed class DemoSettings
{
public bool Enabled { get; set; } = true;
public string TargetDirectory { get; set; } =
DemoCertificatePath.Directory;
public string TargetFileName { get; set; } =
DemoCertificatePath.FileName;
public string ContainerName { get; set; } =
"ct-ZADBService";
public string DisplayName { get; set; } =
"DE / Demo ZADBService";
public string SonicConnectionName { get; set; } =
"DE-Test";
} }
public sealed class DatabaseSettings public sealed class DatabaseSettings
@@ -0,0 +1,15 @@
namespace ZA.CoreService.ESBCertificateManager.Models;
/// <summary>
/// Fester Demo-Pfad für den ersten Zertifikat-Austausch-Test (TEST / DE).
/// </summary>
public static class DemoCertificatePath
{
public const string Directory =
@"\\dekun-painwbdet\e$\proalpha-inwb\test\Containers\proalpha-test.ct-ZADBService\certs";
public const string FileName = "server.p12";
public static string FullPath =>
Path.Combine(Directory, FileName);
}
@@ -9,6 +9,10 @@ public sealed class DeploymentTarget
public required string TargetDirectory { get; init; } public required string TargetDirectory { get; init; }
public required string CertificateFileName { get; init; } public required string CertificateFileName { get; init; }
/// <summary>Vollständiger Dateipfad (UNC oder lokal) zur Zieldatei.</summary>
public string FullTargetPath =>
Path.Combine(TargetDirectory, CertificateFileName);
public bool BackupEnabled { get; init; } = true; public bool BackupEnabled { get; init; } = true;
public string BackupDirectoryName { get; init; } = "Backup"; public string BackupDirectoryName { get; init; } = "Backup";
@@ -63,8 +63,11 @@ public sealed class CertificateTargetOption
public required string CompanyCode { get; init; } public required string CompanyCode { get; init; }
public string FullTargetPath =>
Path.Combine(TargetDirectory, TargetFileName);
public override string ToString() public override string ToString()
{ {
return $"{CompanyCode} / {ContainerName}: {TargetDirectory}\\{TargetFileName}"; return $"{CompanyCode} / {ContainerName}: {FullTargetPath}";
} }
} }
@@ -0,0 +1,302 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
public sealed class CertificateProbeService
{
public static readonly string[] SupportedExtensions =
[
".cer",
".crt",
".pem",
".pfx",
".p12"
];
public CertificateProbeResult Probe(
string certificatePath,
Func<string?>? passwordPrompt = null)
{
string? normalized = NormalizePath(certificatePath);
if (normalized is null)
{
return CertificateProbeResult.Failed(
"Kein gültiger Zertifikatspfad angegeben.");
}
if (!IsSupportedExtension(normalized))
{
return CertificateProbeResult.Failed(
"Nicht unterstütztes Format. Erlaubt: "
+ string.Join(", ", SupportedExtensions));
}
if (!File.Exists(normalized))
{
return CertificateProbeResult.Missing(
normalized,
"Am Zielpfad liegt noch keine Zertifikatsdatei.");
}
try
{
CertificateInfo info = ReadCertificate(normalized, password: null);
return CertificateProbeResult.Found(normalized, info);
}
catch (CryptographicException) when (IsPasswordProtected(normalized))
{
if (passwordPrompt is null)
{
return CertificateProbeResult.PasswordRequired(normalized);
}
string? password = passwordPrompt.Invoke();
if (password is null)
{
return CertificateProbeResult.Failed(
"Zum Lesen der PFX/P12-Datei wird ein Kennwort benötigt.");
}
try
{
CertificateInfo info = ReadCertificate(normalized, password);
return CertificateProbeResult.Found(normalized, info);
}
catch (CryptographicException)
{
return CertificateProbeResult.Failed(
"Das Kennwort ist falsch oder die Datei ist beschädigt.");
}
}
catch (Exception ex)
{
return CertificateProbeResult.Failed(
$"Zertifikat konnte nicht gelesen werden: {ex.Message}");
}
}
public static bool TrySplitTargetPath(
string fullPath,
out string directory,
out string fileName,
out string? error)
{
directory = string.Empty;
fileName = string.Empty;
error = null;
string? normalized = NormalizePath(fullPath);
if (normalized is null)
{
error = "Bitte einen vollständigen Pfad zur Zertifikatsdatei angeben.";
return false;
}
if (!IsSupportedExtension(normalized))
{
error =
"Dateiendung nicht unterstützt. Erlaubt: "
+ string.Join(", ", SupportedExtensions);
return false;
}
string? dir = Path.GetDirectoryName(normalized);
string file = Path.GetFileName(normalized);
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(file))
{
error =
"Pfad muss Verzeichnis und Dateiname enthalten, z. B.\n"
+ @"\\server\e$\...\certs\server.p12";
return false;
}
if (Path.GetFileName(file) != file)
{
error = "Dateiname darf keinen weiteren Verzeichnisanteil enthalten.";
return false;
}
directory = dir;
fileName = file;
return true;
}
public static string CombineTargetPath(
string directory,
string fileName)
{
return Path.Combine(
directory.Trim().TrimEnd('\\', '/'),
fileName.Trim());
}
public static bool IsSupportedExtension(string path)
{
string extension = Path.GetExtension(path);
return SupportedExtensions.Contains(
extension,
StringComparer.OrdinalIgnoreCase);
}
private static bool IsPasswordProtected(string path)
{
string extension = Path.GetExtension(path);
return extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
}
private static string? NormalizePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
string trimmed = path.Trim().Trim('"');
if (trimmed.Length == 0)
{
return null;
}
// UNC und absolute lokale Pfade akzeptieren
if (!Path.IsPathRooted(trimmed)
&& !trimmed.StartsWith(@"\\", StringComparison.Ordinal))
{
return null;
}
return trimmed;
}
private static CertificateInfo ReadCertificate(
string certificatePath,
string? password)
{
string extension = Path.GetExtension(certificatePath);
using X509Certificate2 certificate =
IsPasswordProtected(certificatePath)
? new X509Certificate2(
certificatePath,
password,
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 = certificate.GetCertHashString(
HashAlgorithmName.SHA256);
fingerprint = string.Join(
":",
Enumerable.Range(0, fingerprint.Length / 2)
.Select(index => fingerprint.Substring(index * 2, 2)));
DateTimeOffset validFrom = new(certificate.NotBefore);
DateTimeOffset validUntil = new(certificate.NotAfter);
DateTimeOffset now = DateTimeOffset.Now;
return new CertificateInfo
{
Subject = subject,
Issuer = issuer,
FingerprintSha256 = fingerprint,
ValidFrom = validFrom,
ValidUntil = validUntil,
IsCurrentlyValid = now >= validFrom && now <= validUntil
};
}
}
public sealed class CertificateProbeResult
{
public bool Success { get; init; }
public bool FileExists { get; init; }
public string Path { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
public CertificateInfo? Certificate { get; init; }
public static CertificateProbeResult Found(
string path,
CertificateInfo certificate)
{
string expiry = certificate.ValidUntil.ToLocalTime()
.ToString("dd.MM.yyyy HH:mm");
string validity = certificate.IsCurrentlyValid
? "gültig"
: "abgelaufen / ungültig";
return new CertificateProbeResult
{
Success = true,
FileExists = true,
Path = path,
Certificate = certificate,
Message =
$"Zertifikat gefunden · Subject: {certificate.Subject} · "
+ $"Ablauf: {expiry} ({validity})"
};
}
public static CertificateProbeResult Missing(
string path,
string message)
{
return new CertificateProbeResult
{
Success = true,
FileExists = false,
Path = path,
Message = message
};
}
public static CertificateProbeResult Failed(string message)
{
return new CertificateProbeResult
{
Success = false,
FileExists = false,
Message = message
};
}
public static CertificateProbeResult PasswordRequired(string path)
{
return new CertificateProbeResult
{
Success = false,
FileExists = true,
Path = path,
Message = "PFX/P12 vorhanden Kennwort nötig zum Lesen des Ablaufs"
};
}
}
@@ -8,7 +8,8 @@ public sealed class LocalCertificateDeployer
".cer", ".cer",
".crt", ".crt",
".pem", ".pem",
".pfx" ".pfx",
".p12"
}; };
public async Task<string> DeployAsync( public async Task<string> DeployAsync(
@@ -1,303 +0,0 @@
using System.Diagnostics;
using System.Text;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
public sealed class SonicContainerScanner
{
private readonly RuntimeSettings _runtimeSettings;
public SonicContainerScanner(RuntimeSettings runtimeSettings)
{
_runtimeSettings = runtimeSettings
?? throw new ArgumentNullException(nameof(runtimeSettings));
}
public async Task<ContainerScanResult> ScanAsync(
SonicSystemOption system,
string userName,
string secret,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(system);
if (string.IsNullOrWhiteSpace(userName))
{
return ContainerScanResult.Failed(
"Der Sonic-Benutzername fehlt.");
}
if (string.IsNullOrEmpty(secret))
{
return ContainerScanResult.Failed(
"Das Sonic-Kennwort fehlt.");
}
string javaPath =
PathResolver.ResolvePath(
_runtimeSettings.JavaExecutablePath);
string libraryPath =
PathResolver.ResolvePath(
_runtimeSettings.SonicClientLibraryPath);
string? toolsPath = ResolveToolsDirectory();
if (!File.Exists(javaPath))
{
return ContainerScanResult.Failed(
$"Java wurde nicht gefunden: {javaPath}");
}
if (!Directory.Exists(libraryPath))
{
return ContainerScanResult.Failed(
$"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}");
}
if (toolsPath is null)
{
return ContainerScanResult.Failed(
"SonicMfContainerTool.class wurde nicht gefunden.");
}
string[] jars =
Directory.GetFiles(
libraryPath,
"*.jar",
SearchOption.TopDirectoryOnly);
if (jars.Length == 0)
{
return ContainerScanResult.Failed(
"Im Sonic-Client-Ordner wurden keine JAR-Dateien gefunden.");
}
string classpath =
toolsPath
+ Path.PathSeparator
+ string.Join(
Path.PathSeparator,
jars.OrderBy(
Path.GetFileName,
StringComparer.OrdinalIgnoreCase));
ProcessStartInfo startInfo = new()
{
FileName = javaPath,
WorkingDirectory = toolsPath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true
};
startInfo.ArgumentList.Add("-cp");
startInfo.ArgumentList.Add(classpath);
startInfo.ArgumentList.Add("SonicMfContainerTool");
startInfo.ArgumentList.Add("list");
startInfo.ArgumentList.Add("--domain");
startInfo.ArgumentList.Add(system.DomainName);
startInfo.ArgumentList.Add("--url");
startInfo.ArgumentList.Add(system.ConnectionUrl);
startInfo.ArgumentList.Add("--user");
startInfo.ArgumentList.Add(userName.Trim());
startInfo.ArgumentList.Add("--timeout");
startInfo.ArgumentList.Add("45");
startInfo.Environment["ESB_SONIC_PASSWORD"] = secret;
using Process process = new()
{
StartInfo = startInfo
};
try
{
if (!process.Start())
{
return ContainerScanResult.Failed(
"Der Java-Prozess konnte nicht gestartet werden.");
}
}
catch (Exception ex)
{
return ContainerScanResult.Failed(
$"Java konnte nicht gestartet werden: {ex.Message}");
}
Task<string> standardOutputTask =
process.StandardOutput.ReadToEndAsync();
Task<string> standardErrorTask =
process.StandardError.ReadToEndAsync();
using CancellationTokenSource timeoutSource =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeoutSource.CancelAfter(TimeSpan.FromSeconds(50));
try
{
await process.WaitForExitAsync(timeoutSource.Token);
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
TryKill(process);
return ContainerScanResult.Failed(
"Der Container-Scan hat das Zeitlimit überschritten.");
}
catch (OperationCanceledException)
{
TryKill(process);
throw;
}
string standardOutput =
(await standardOutputTask).Trim();
string standardError =
(await standardErrorTask).Trim();
string combined =
string.Join(
Environment.NewLine,
new[] { standardOutput, standardError }
.Where(value => !string.IsNullOrWhiteSpace(value)));
bool success =
process.ExitCode == 0
&& combined.Contains(
"OK:ContainerListEnd",
StringComparison.OrdinalIgnoreCase)
&& !combined.Contains(
"ERROR:",
StringComparison.OrdinalIgnoreCase);
if (!success)
{
return ContainerScanResult.Failed(
ExtractError(combined)
?? $"Container-Scan fehlgeschlagen, ExitCode={process.ExitCode}.");
}
List<string> containers = [];
foreach (string line in standardOutput.Split(
['\r', '\n'],
StringSplitOptions.RemoveEmptyEntries))
{
const string prefix = "CONTAINER=";
if (!line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
string name = line[prefix.Length..].Trim();
if (name.Length > 0
&& !containers.Contains(name, StringComparer.OrdinalIgnoreCase))
{
containers.Add(name);
}
}
containers.Sort(StringComparer.OrdinalIgnoreCase);
return ContainerScanResult.Successful(containers);
}
private static string? ResolveToolsDirectory()
{
string baseDirectory = AppContext.BaseDirectory;
string[] candidates =
[
Path.Combine(baseDirectory, "Tools"),
Path.Combine(baseDirectory, "..", "..", "..", "Tools"),
Path.Combine(
Directory.GetCurrentDirectory(),
"Tools")
];
foreach (string candidate in candidates)
{
string fullPath = Path.GetFullPath(candidate);
string classFile = Path.Combine(
fullPath,
"SonicMfContainerTool.class");
if (File.Exists(classFile))
{
return fullPath;
}
}
return null;
}
private static string? ExtractError(string output)
{
foreach (string line in output.Split(
['\r', '\n'],
StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
{
return line["ERROR:".Length..].Trim();
}
}
return string.IsNullOrWhiteSpace(output) ? null : output;
}
private static void TryKill(Process process)
{
try
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
}
catch
{
}
}
}
public sealed class ContainerScanResult
{
public bool Success { get; init; }
public string Message { get; init; } = string.Empty;
public IReadOnlyList<string> Containers { get; init; } = [];
public static ContainerScanResult Successful(
IReadOnlyList<string> containers)
{
return new ContainerScanResult
{
Success = true,
Message = $"{containers.Count} Container gefunden.",
Containers = containers
};
}
public static ContainerScanResult Failed(string message)
{
return new ContainerScanResult
{
Success = false,
Message = message
};
}
}
@@ -1,16 +1,22 @@
using ZA.CoreService.ESBCertificateManager.Models; using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager.Setup; namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class AddTargetPathForm : Form public sealed class AddTargetPathForm : Form
{ {
private readonly SetupCoordinator _coordinator; private readonly SetupCoordinator _coordinator;
private readonly CertificateProbeService _probeService = new();
private readonly ComboBox _cmbContainer; private readonly ComboBox _cmbContainer;
private readonly TextBox _txtDirectory; private readonly TextBox _txtFullPath;
private readonly TextBox _txtFileName;
private readonly TextBox _txtBackupFolder; private readonly TextBox _txtBackupFolder;
private readonly CheckBox _chkBackup; private readonly CheckBox _chkBackup;
private readonly Label _lblProbeResult;
private readonly Button _btnProbe;
private readonly Button _btnSave; private readonly Button _btnSave;
private CertificateProbeResult? _lastProbe;
private bool _running; private bool _running;
public int? CreatedCertificateTargetId { get; private set; } public int? CreatedCertificateTargetId { get; private set; }
@@ -27,15 +33,15 @@ public sealed class AddTargetPathForm : Form
MaximizeBox = false; MaximizeBox = false;
MinimizeBox = false; MinimizeBox = false;
ShowInTaskbar = false; ShowInTaskbar = false;
ClientSize = new Size(560, 450); ClientSize = new Size(640, 560);
BackColor = SettingsUi.Background; BackColor = SettingsUi.Background;
ForeColor = SettingsUi.Text; ForeColor = SettingsUi.Text;
Font = new Font("Segoe UI", 10f); Font = new Font("Segoe UI", 10f);
Label title = new() Label title = new()
{ {
Text = "Neuer Zertifikat-Pfad", Text = "Zertifikat-Datei-Pfad",
Location = new Point(28, 22), Location = new Point(28, 18),
AutoSize = true, AutoSize = true,
Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold),
ForeColor = SettingsUi.Text ForeColor = SettingsUi.Text
@@ -44,23 +50,45 @@ public sealed class AddTargetPathForm : Form
Label subtitle = new() Label subtitle = new()
{ {
Text = Text =
"Zielverzeichnis und Dateiname für die Zertifikatsablage.", "UNC- oder lokaler Dateipfad zur .cer/.crt/.pem/.pfx/.p12-Datei. "
Location = new Point(30, 58), + "Der Container ist nur das Neustart-Ziel.",
Location = new Point(30, 54),
MaximumSize = new Size(580, 40),
AutoSize = true, AutoSize = true,
ForeColor = SettingsUi.Muted ForeColor = SettingsUi.Muted
}; };
Panel card = SettingsUi.CreateCard(); Panel card = SettingsUi.CreateCard();
card.Location = new Point(28, 95); card.Location = new Point(28, 100);
card.Size = new Size(504, 270); card.Size = new Size(584, 360);
card.Paint += SettingsUi.PaintCardBorder; card.Paint += SettingsUi.PaintCardBorder;
_txtFullPath = SettingsUi.CreateTextBox();
_txtFullPath.Text = DemoCertificatePath.FullPath;
_txtFullPath.PlaceholderText = DemoCertificatePath.FullPath;
Button browse = SettingsUi.CreateGhostButton("…");
browse.Size = new Size(40, 32);
browse.Click += (_, _) => BrowseForFile();
_btnProbe = SettingsUi.CreateGhostButton("Prüfen");
_btnProbe.Size = new Size(100, 32);
_btnProbe.Click += (_, _) => RunProbe();
_lblProbeResult = new Label
{
AutoSize = false,
Size = new Size(548, 52),
ForeColor = SettingsUi.Muted,
Font = new Font("Segoe UI", 9f),
Text =
"Noch nicht geprüft. Bitte Pfad prüfen bei vorhandener "
+ "Datei wird der Ablauf angezeigt."
};
_cmbContainer = SettingsUi.CreateComboBox(); _cmbContainer = SettingsUi.CreateComboBox();
_txtDirectory = SettingsUi.CreateTextBox();
_txtFileName = SettingsUi.CreateTextBox();
_txtBackupFolder = SettingsUi.CreateTextBox(); _txtBackupFolder = SettingsUi.CreateTextBox();
_txtBackupFolder.Text = "Backup"; _txtBackupFolder.Text = "Backup";
_txtFileName.Text = "server.crt";
_chkBackup = new CheckBox _chkBackup = new CheckBox
{ {
@@ -71,21 +99,54 @@ public sealed class AddTargetPathForm : Form
Font = new Font("Segoe UI", 9.5f) Font = new Font("Segoe UI", 9.5f)
}; };
AddField(card, "Container", _cmbContainer, 18, 18, 460); Label pathLabel = SettingsUi.CreateFieldLabel(
AddField(card, "Zielverzeichnis", _txtDirectory, 18, 78, 460); "Vollständiger Pfad zur Zertifikatsdatei");
AddField(card, "Dateiname", _txtFileName, 18, 138, 220); pathLabel.Location = new Point(18, 16);
AddField(card, "Backup-Ordner", _txtBackupFolder, 258, 138, 220); _txtFullPath.Location = new Point(18, 38);
_txtFullPath.Width = 370;
browse.Location = new Point(394, 38);
_btnProbe.Location = new Point(440, 38);
_chkBackup.Location = new Point(18, 210); _lblProbeResult.Location = new Point(18, 78);
Label containerLabel = SettingsUi.CreateFieldLabel(
"Neustart-Ziel (Sonic-Container) nicht der Dateipfad");
containerLabel.Location = new Point(18, 140);
_cmbContainer.Location = new Point(18, 162);
_cmbContainer.Width = 548;
Label hint = new()
{
Text =
"Nach dem Austausch wird dieser Container über MfApi "
+ "neu gestartet. Der Pfad oben ist unabhängig davon.",
Location = new Point(18, 202),
MaximumSize = new Size(548, 36),
AutoSize = true,
ForeColor = SettingsUi.Muted,
Font = new Font("Segoe UI", 8.5f)
};
AddField(card, "Backup-Ordner", _txtBackupFolder, 18, 250, 220);
_chkBackup.Location = new Point(258, 272);
card.Controls.Add(_chkBackup); card.Controls.Add(_chkBackup);
card.Controls.Add(pathLabel);
card.Controls.Add(_txtFullPath);
card.Controls.Add(browse);
card.Controls.Add(_btnProbe);
card.Controls.Add(_lblProbeResult);
card.Controls.Add(containerLabel);
card.Controls.Add(_cmbContainer);
card.Controls.Add(hint);
_btnSave = SettingsUi.CreatePrimaryButton("Pfad anlegen"); _btnSave = SettingsUi.CreatePrimaryButton("Pfad anlegen");
_btnSave.Location = new Point(312, 385); _btnSave.Location = new Point(392, 490);
_btnSave.Size = new Size(220, 40); _btnSave.Size = new Size(220, 40);
_btnSave.Click += async (_, _) => await SaveAsync(); _btnSave.Click += async (_, _) => await SaveAsync();
Button cancel = SettingsUi.CreateGhostButton("Abbrechen"); Button cancel = SettingsUi.CreateGhostButton("Abbrechen");
cancel.Location = new Point(190, 385); cancel.Location = new Point(270, 490);
cancel.Size = new Size(110, 40); cancel.Size = new Size(110, 40);
cancel.Click += (_, _) => cancel.Click += (_, _) =>
{ {
@@ -103,6 +164,64 @@ public sealed class AddTargetPathForm : Form
await LoadContainersAsync(preferredSonicContainerId); await LoadContainersAsync(preferredSonicContainerId);
} }
private void BrowseForFile()
{
using OpenFileDialog dialog = new()
{
Title = "Vorhandene Zertifikatsdatei wählen",
Filter =
"Zertifikate (*.cer;*.crt;*.pem;*.pfx;*.p12)|"
+ "*.cer;*.crt;*.pem;*.pfx;*.p12|"
+ "Alle Dateien (*.*)|*.*",
CheckFileExists = false,
Multiselect = false
};
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
_txtFullPath.Text = dialog.FileName;
RunProbe();
}
private void RunProbe()
{
_lastProbe = _probeService.Probe(
_txtFullPath.Text,
() => PromptForPassword(this));
if (!_lastProbe.Success)
{
_lblProbeResult.ForeColor = Color.FromArgb(220, 100, 100);
_lblProbeResult.Text = _lastProbe.Message;
return;
}
if (!_lastProbe.FileExists)
{
_lblProbeResult.ForeColor = Color.FromArgb(230, 180, 80);
_lblProbeResult.Text =
_lastProbe.Message
+ " Pfad kann trotzdem gespeichert werden "
+ "(erstes Deployment).";
return;
}
CertificateInfo cert = _lastProbe.Certificate!;
string expiry = cert.ValidUntil.ToLocalTime()
.ToString("dd.MM.yyyy HH:mm");
_lblProbeResult.ForeColor = cert.IsCurrentlyValid
? Color.FromArgb(90, 200, 140)
: Color.FromArgb(220, 100, 100);
_lblProbeResult.Text =
$"Datei vorhanden · Subject: {cert.Subject}\n"
+ $"Ablauf: {expiry} · Fingerprint: {cert.FingerprintSha256}";
}
private async Task LoadContainersAsync(int? preferredSonicContainerId) private async Task LoadContainersAsync(int? preferredSonicContainerId)
{ {
try try
@@ -151,13 +270,59 @@ public sealed class AddTargetPathForm : Form
{ {
MessageBox.Show( MessageBox.Show(
this, this,
"Bitte einen Container wählen.", "Bitte ein Neustart-Ziel (Sonic-Container) wählen.",
"Zertifikat-Pfad", "Zertifikat-Pfad",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Warning); MessageBoxIcon.Warning);
return; return;
} }
if (!CertificateProbeService.TrySplitTargetPath(
_txtFullPath.Text,
out string directory,
out string fileName,
out string? splitError))
{
MessageBox.Show(
this,
splitError ?? "Ungültiger Pfad.",
"Zertifikat-Pfad",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
// Vor dem Speichern immer prüfen
RunProbe();
if (_lastProbe is null || !_lastProbe.Success)
{
MessageBox.Show(
this,
_lastProbe?.Message
?? "Pfad konnte nicht geprüft werden.",
"Zertifikat-Pfad",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
if (!_lastProbe.FileExists)
{
DialogResult confirm = MessageBox.Show(
this,
"Am Ziel liegt noch keine Zertifikatsdatei.\n\n"
+ "Pfad trotzdem speichern?",
"Datei fehlt",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (confirm != DialogResult.Yes)
{
return;
}
}
_running = true; _running = true;
_btnSave.Enabled = false; _btnSave.Enabled = false;
Cursor = Cursors.WaitCursor; Cursor = Cursors.WaitCursor;
@@ -167,8 +332,8 @@ public sealed class AddTargetPathForm : Form
CreatedCertificateTargetId = CreatedCertificateTargetId =
await _coordinator.AddCertificateTargetAsync( await _coordinator.AddCertificateTargetAsync(
container.SonicContainerId, container.SonicContainerId,
_txtDirectory.Text, directory,
_txtFileName.Text, fileName,
_chkBackup.Checked, _chkBackup.Checked,
_txtBackupFolder.Text); _txtBackupFolder.Text);
@@ -192,6 +357,57 @@ public sealed class AddTargetPathForm : Form
} }
} }
private static string? PromptForPassword(IWin32Window owner)
{
using Form dialog = new()
{
Text = "PFX/P12-Kennwort",
FormBorderStyle = FormBorderStyle.FixedDialog,
StartPosition = FormStartPosition.CenterParent,
ClientSize = new Size(360, 140),
MaximizeBox = false,
MinimizeBox = false,
ShowInTaskbar = false,
BackColor = SettingsUi.Background,
ForeColor = SettingsUi.Text
};
Label label = new()
{
Text = "Kennwort für die Zertifikatsdatei:",
Location = new Point(16, 16),
AutoSize = true
};
TextBox txtPwd = new()
{
Location = new Point(16, 44),
Width = 320,
UseSystemPasswordChar = true
};
Button btnOk = SettingsUi.CreatePrimaryButton("OK");
btnOk.Location = new Point(196, 90);
btnOk.Size = new Size(140, 32);
btnOk.DialogResult = DialogResult.OK;
Button btnCancel = SettingsUi.CreateGhostButton("Abbrechen");
btnCancel.Location = new Point(100, 90);
btnCancel.Size = new Size(90, 32);
btnCancel.DialogResult = DialogResult.Cancel;
dialog.Controls.Add(label);
dialog.Controls.Add(txtPwd);
dialog.Controls.Add(btnOk);
dialog.Controls.Add(btnCancel);
dialog.AcceptButton = btnOk;
dialog.CancelButton = btnCancel;
return dialog.ShowDialog(owner) == DialogResult.OK
? txtPwd.Text
: null;
}
private static void AddField( private static void AddField(
Control parent, Control parent,
string label, string label,
@@ -1,432 +0,0 @@
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class SelectContainersForm : Form
{
private readonly SetupCoordinator _coordinator;
private readonly SonicSystemOption _system;
private readonly SonicContainerScanner _scanner;
private readonly ComboBox _cmbCompany;
private readonly CheckedListBox _lstContainers;
private readonly TextBox _txtManual;
private readonly TextBox _txtUser;
private readonly TextBox _txtPassword;
private readonly Label _lblStatus;
private readonly Button _btnScan;
private readonly Button _btnSave;
private bool _running;
public int SavedCount { get; private set; }
public SelectContainersForm(
SetupCoordinator coordinator,
SonicSystemOption system)
{
_coordinator = coordinator;
_system = system;
_scanner = new SonicContainerScanner(coordinator.Settings.Runtime);
Text = "Container auswählen";
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
ClientSize = new Size(640, 620);
BackColor = SettingsUi.Background;
ForeColor = SettingsUi.Text;
Font = new Font("Segoe UI", 10f);
Label title = new()
{
Text = "Container für System",
Location = new Point(28, 20),
AutoSize = true,
Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold),
ForeColor = SettingsUi.Text
};
Label subtitle = new()
{
Text =
$"{system.ConnectionName} · {system.ConnectionUrl}"
+ Environment.NewLine
+ "Scan von Sonic oder manuell eintragen, dann übernehmen.",
Location = new Point(30, 54),
Size = new Size(580, 40),
ForeColor = SettingsUi.Muted
};
Panel card = SettingsUi.CreateCard();
card.Location = new Point(28, 110);
card.Size = new Size(584, 430);
card.Paint += SettingsUi.PaintCardBorder;
_cmbCompany = SettingsUi.CreateComboBox();
_lstContainers = new CheckedListBox
{
BackColor = Color.FromArgb(12, 28, 48),
ForeColor = SettingsUi.Text,
BorderStyle = BorderStyle.FixedSingle,
CheckOnClick = true,
Font = new Font("Segoe UI", 10f)
};
_txtManual = SettingsUi.CreateTextBox();
_txtUser = SettingsUi.CreateTextBox();
_txtPassword = SettingsUi.CreateTextBox();
_txtPassword.UseSystemPasswordChar = true;
AddField(card, "Company", _cmbCompany, 18, 16, 540);
AddField(card, "Gefundene / gewählte Container", _lstContainers, 18, 76, 540, 150);
Label manualLabel = SettingsUi.CreateFieldLabel("Manuell hinzufügen");
manualLabel.Location = new Point(18, 240);
_txtManual.Location = new Point(18, 262);
_txtManual.Width = 400;
Button btnAddManual = SettingsUi.CreateGhostButton("");
btnAddManual.Location = new Point(430, 260);
btnAddManual.Size = new Size(50, 32);
btnAddManual.Click += (_, _) => AddManualContainer();
Button btnSuggest = SettingsUi.CreateGhostButton("ct-ZADBService");
btnSuggest.Location = new Point(490, 260);
btnSuggest.Size = new Size(68, 32);
btnSuggest.Font = new Font("Segoe UI", 7.5f, FontStyle.Bold);
btnSuggest.Click += (_, _) =>
{
_txtManual.Text = "ct-ZADBService";
AddManualContainer();
};
AddField(card, "Sonic-Benutzer (für Scan)", _txtUser, 18, 308, 250);
AddField(card, "Kennwort", _txtPassword, 288, 308, 270);
_btnScan = SettingsUi.CreatePrimaryButton("Von Sonic scannen");
_btnScan.Location = new Point(18, 375);
_btnScan.Size = new Size(200, 36);
_btnScan.Click += async (_, _) => await ScanAsync();
_lblStatus = new Label
{
Location = new Point(230, 380),
Size = new Size(330, 30),
ForeColor = SettingsUi.Muted
};
card.Controls.Add(manualLabel);
card.Controls.Add(_txtManual);
card.Controls.Add(btnAddManual);
card.Controls.Add(btnSuggest);
card.Controls.Add(_btnScan);
card.Controls.Add(_lblStatus);
_btnSave = SettingsUi.CreatePrimaryButton("Auswahl speichern");
_btnSave.Location = new Point(352, 555);
_btnSave.Size = new Size(260, 42);
_btnSave.Click += async (_, _) => await SaveAsync();
Button skip = SettingsUi.CreateGhostButton("Später");
skip.Location = new Point(230, 555);
skip.Size = new Size(110, 42);
skip.Click += (_, _) =>
{
DialogResult = DialogResult.Cancel;
Close();
};
Controls.Add(title);
Controls.Add(subtitle);
Controls.Add(card);
Controls.Add(skip);
Controls.Add(_btnSave);
Shown += async (_, _) => await InitializeAsync();
}
private async Task InitializeAsync()
{
try
{
IReadOnlyList<CompanyOption> companies =
await _coordinator.LoadCompaniesAsync();
_cmbCompany.Items.Clear();
foreach (CompanyOption company in companies)
{
_cmbCompany.Items.Add(company);
}
if (_cmbCompany.Items.Count > 0)
{
int deIndex = 0;
for (int index = 0; index < companies.Count; index++)
{
if (string.Equals(
companies[index].CompanyCode,
"DE",
StringComparison.OrdinalIgnoreCase))
{
deIndex = index;
break;
}
}
_cmbCompany.SelectedIndex = deIndex;
}
IReadOnlyList<SonicCredentialProfile> profiles =
await _coordinator.LoadCredentialsAsync(
_system.SonicConnectionId);
SonicCredentialProfile? profile =
profiles.FirstOrDefault(item => item.IsDefault)
?? profiles.FirstOrDefault();
if (profile is not null)
{
_txtUser.Text = profile.UserName;
_txtPassword.Text = profile.Secret;
_lblStatus.Text =
"Profil geladen — Scan möglich.";
_lblStatus.ForeColor = SettingsUi.Green;
}
else
{
_lblStatus.Text =
"Kein Profil — Benutzer/Kennwort für Scan eingeben oder manuell.";
_lblStatus.ForeColor = SettingsUi.Gold;
}
// Bereits vorhandene Container vorselektieren
IReadOnlyList<ContainerOption> existing =
await _coordinator.LoadContainersAsync(
_system.SonicConnectionId);
foreach (ContainerOption container in existing)
{
AddContainerName(container.ContainerName, check: true);
}
}
catch (Exception ex)
{
_lblStatus.Text = ex.Message;
_lblStatus.ForeColor = SettingsUi.Red;
}
}
private void AddManualContainer()
{
string name = _txtManual.Text.Trim();
if (name.Length == 0)
{
return;
}
AddContainerName(name, check: true);
_txtManual.Clear();
_txtManual.Focus();
}
private void AddContainerName(string name, bool check)
{
for (int index = 0; index < _lstContainers.Items.Count; index++)
{
if (string.Equals(
_lstContainers.Items[index]?.ToString(),
name,
StringComparison.OrdinalIgnoreCase))
{
_lstContainers.SetItemChecked(index, check);
return;
}
}
int added = _lstContainers.Items.Add(name);
_lstContainers.SetItemChecked(added, check);
}
private async Task ScanAsync()
{
if (_running)
{
return;
}
_running = true;
_btnScan.Enabled = false;
_btnSave.Enabled = false;
Cursor = Cursors.WaitCursor;
_lblStatus.Text = "Scanne Domain…";
_lblStatus.ForeColor = SettingsUi.Muted;
try
{
ContainerScanResult result =
await _scanner.ScanAsync(
_system,
_txtUser.Text,
_txtPassword.Text);
if (!result.Success)
{
_lblStatus.Text = result.Message;
_lblStatus.ForeColor = SettingsUi.Red;
return;
}
foreach (string container in result.Containers)
{
AddContainerName(container, check: true);
}
_lblStatus.Text =
result.Containers.Count == 0
? "Scan OK, aber keine Container gefunden — bitte manuell."
: $"{result.Containers.Count} Container gescannt und vorausgewählt.";
_lblStatus.ForeColor =
result.Containers.Count == 0
? SettingsUi.Gold
: SettingsUi.Green;
}
catch (Exception ex)
{
_lblStatus.Text = ex.Message;
_lblStatus.ForeColor = SettingsUi.Red;
}
finally
{
_running = false;
_btnScan.Enabled = true;
_btnSave.Enabled = true;
Cursor = Cursors.Default;
}
}
private async Task SaveAsync()
{
if (_running)
{
return;
}
if (_cmbCompany.SelectedItem is not CompanyOption company)
{
MessageBox.Show(
this,
"Bitte eine Company wählen.",
"Container",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
List<string> selected = _lstContainers.CheckedItems
.Cast<object>()
.Select(item => item.ToString() ?? string.Empty)
.Where(name => name.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (selected.Count == 0)
{
MessageBox.Show(
this,
"Bitte mindestens einen Container angehakt lassen "
+ "oder manuell hinzufügen.",
"Container",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
_running = true;
_btnSave.Enabled = false;
Cursor = Cursors.WaitCursor;
try
{
IReadOnlyList<ContainerOption> existing =
await _coordinator.LoadContainersAsync(
_system.SonicConnectionId);
HashSet<string> existingNames = existing
.Select(item => item.ContainerName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
int created = 0;
foreach (string containerName in selected)
{
if (existingNames.Contains(containerName))
{
continue;
}
await _coordinator.AddSonicContainerAsync(
company.CompanyId,
_system.SonicConnectionId,
containerName,
containerDisplayName: null,
restartTimeoutSeconds: 180);
created++;
}
SavedCount = created;
MessageBox.Show(
this,
created == 0
? "Alle gewählten Container waren bereits vorhanden."
: $"{created} Container gespeichert und geprüft.",
"Prüfung OK",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show(
this,
ex.Message,
"Container",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
_running = false;
_btnSave.Enabled = true;
Cursor = Cursors.Default;
}
}
private static void AddField(
Control parent,
string label,
Control input,
int x,
int y,
int width,
int height = 30)
{
Label fieldLabel = SettingsUi.CreateFieldLabel(label);
fieldLabel.Location = new Point(x, y);
input.Location = new Point(x, y + 22);
input.Width = width;
input.Height = height;
parent.Controls.Add(fieldLabel);
parent.Controls.Add(input);
}
}
@@ -1,4 +1,5 @@
using ZA.CoreService.ESBCertificateManager.Models; using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager.Setup; namespace ZA.CoreService.ESBCertificateManager.Setup;
@@ -381,9 +382,7 @@ public sealed class SetupWizardForm : Form
Panel toolbar = CreateToolbar( Panel toolbar = CreateToolbar(
"+ Container hinzufügen", "+ Container hinzufügen",
async () => await AddContainerAsync(), async () => await AddContainerAsync());
secondaryText: "Von Sonic scannen",
onSecondary: async () => await ScanContainersForSelectedSystemAsync());
_lvContainers = SettingsUi.CreateListView(); _lvContainers = SettingsUi.CreateListView();
_lvContainers.Dock = DockStyle.Fill; _lvContainers.Dock = DockStyle.Fill;
@@ -400,20 +399,22 @@ public sealed class SetupWizardForm : Form
{ {
_panelPaths = CreateContentPanel( _panelPaths = CreateContentPanel(
"Zertifikat-Pfade", "Zertifikat-Pfade",
"Zielverzeichnisse und Dateinamen für Deployments."); "Dateipfade (UNC/lokal) zum Austausch. Container = nur Neustart-Ziel.");
Panel toolbar = CreateToolbar( Panel toolbar = CreateToolbar(
"+ Pfad hinzufügen", "+ Pfad hinzufügen",
async () => await AddPathAsync()); async () => await AddPathAsync(),
"Pfad prüfen",
async () => await ProbeSelectedPathAsync());
_lvPaths = SettingsUi.CreateListView(); _lvPaths = SettingsUi.CreateListView();
_lvPaths.Dock = DockStyle.Fill; _lvPaths.Dock = DockStyle.Fill;
_lvPaths.Columns.Add("Company", 70); _lvPaths.Columns.Add("Company", 70);
_lvPaths.Columns.Add("Container", 140); _lvPaths.Columns.Add("Neustart-Container", 140);
_lvPaths.Columns.Add("Verzeichnis", 280); _lvPaths.Columns.Add("Zertifikat-Pfad", 360);
_lvPaths.Columns.Add("Datei", 120); _lvPaths.Columns.Add("Ablauf", 140);
_lvPaths.Columns.Add("Backup", 80); _lvPaths.Columns.Add("Backup", 80);
_lvPaths.Columns.Add("System", 140); _lvPaths.Columns.Add("System", 120);
FinishPanelLayout(_panelPaths, _lvPaths, toolbar); FinishPanelLayout(_panelPaths, _lvPaths, toolbar);
} }
@@ -644,13 +645,14 @@ public sealed class SetupWizardForm : Form
await _coordinator.LoadCertificateTargetsAsync(); await _coordinator.LoadCertificateTargetsAsync();
_lvPaths.Items.Clear(); _lvPaths.Items.Clear();
CertificateProbeService probe = new();
foreach (CertificateTargetOption target in targets) foreach (CertificateTargetOption target in targets)
{ {
ListViewItem item = new(target.CompanyCode); ListViewItem item = new(target.CompanyCode);
item.SubItems.Add(target.ContainerName); item.SubItems.Add(target.ContainerName);
item.SubItems.Add(target.TargetDirectory); item.SubItems.Add(target.FullTargetPath);
item.SubItems.Add(target.TargetFileName); item.SubItems.Add("…");
item.SubItems.Add( item.SubItems.Add(
target.BackupEnabled target.BackupEnabled
? target.BackupDirectoryName ? target.BackupDirectoryName
@@ -658,6 +660,11 @@ public sealed class SetupWizardForm : Form
item.SubItems.Add(target.ConnectionName); item.SubItems.Add(target.ConnectionName);
item.Tag = target; item.Tag = target;
_lvPaths.Items.Add(item); _lvPaths.Items.Add(item);
CertificateProbeResult result = await Task.Run(
() => probe.Probe(target.FullTargetPath));
item.SubItems[3].Text = FormatProbeExpiry(result);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -666,6 +673,110 @@ public sealed class SetupWizardForm : Form
} }
} }
private async Task ProbeSelectedPathAsync()
{
if (_lvPaths.SelectedItems.Count == 0
|| _lvPaths.SelectedItems[0].Tag is not CertificateTargetOption target)
{
MessageBox.Show(
this,
"Bitte zuerst einen Zertifikat-Pfad in der Liste wählen.",
"Pfad prüfen",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return;
}
CertificateProbeService probe = new();
CertificateProbeResult result = probe.Probe(
target.FullTargetPath,
() => PromptCertificatePassword(this));
_lvPaths.SelectedItems[0].SubItems[3].Text =
FormatProbeExpiry(result);
MessageBoxIcon icon = result.Success && result.Certificate is not null
? (result.Certificate.IsCurrentlyValid
? MessageBoxIcon.Information
: MessageBoxIcon.Warning)
: MessageBoxIcon.Warning;
MessageBox.Show(
this,
result.Message + "\n\n" + target.FullTargetPath,
"Pfad prüfen",
MessageBoxButtons.OK,
icon);
}
private static string FormatProbeExpiry(CertificateProbeResult result)
{
if (result.Success && result.Certificate is not null)
{
return result.Certificate.ValidUntil
.ToLocalTime()
.ToString("dd.MM.yyyy HH:mm");
}
if (result.FileExists)
{
return "Kennwort nötig";
}
return result.Success ? "fehlt" : "n/a";
}
private static string? PromptCertificatePassword(IWin32Window owner)
{
using Form dialog = new()
{
Text = "PFX/P12-Kennwort",
FormBorderStyle = FormBorderStyle.FixedDialog,
StartPosition = FormStartPosition.CenterParent,
ClientSize = new Size(360, 140),
MaximizeBox = false,
MinimizeBox = false,
ShowInTaskbar = false,
BackColor = SettingsUi.Background,
ForeColor = SettingsUi.Text
};
Label label = new()
{
Text = "Kennwort für die Zertifikatsdatei:",
Location = new Point(16, 16),
AutoSize = true
};
TextBox txtPwd = new()
{
Location = new Point(16, 44),
Width = 320,
UseSystemPasswordChar = true
};
Button btnOk = SettingsUi.CreatePrimaryButton("OK");
btnOk.Location = new Point(196, 90);
btnOk.Size = new Size(140, 32);
btnOk.DialogResult = DialogResult.OK;
Button btnCancel = SettingsUi.CreateGhostButton("Abbrechen");
btnCancel.Location = new Point(100, 90);
btnCancel.Size = new Size(90, 32);
btnCancel.DialogResult = DialogResult.Cancel;
dialog.Controls.Add(label);
dialog.Controls.Add(txtPwd);
dialog.Controls.Add(btnOk);
dialog.Controls.Add(btnCancel);
dialog.AcceptButton = btnOk;
dialog.CancelButton = btnCancel;
return dialog.ShowDialog(owner) == DialogResult.OK
? txtPwd.Text
: null;
}
private async Task AddSystemAsync() private async Task AddSystemAsync()
{ {
using AddSonicSystemForm dialog = new(_coordinator); using AddSonicSystemForm dialog = new(_coordinator);
@@ -706,78 +817,6 @@ public sealed class SetupWizardForm : Form
"Prüfung OK", "Prüfung OK",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Information); MessageBoxIcon.Information);
SonicSystemOption? createdSystem = _systems
.FirstOrDefault(system =>
system.SonicConnectionId == createdId);
if (createdSystem is null
&& _lvSystems.SelectedItems.Count > 0)
{
createdSystem =
_lvSystems.SelectedItems[0].Tag as SonicSystemOption;
}
if (createdSystem is null)
{
return;
}
DialogResult next = MessageBox.Show(
this,
"Container jetzt per Sonic-Scan oder manuell zuordnen?",
"Container",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (next != DialogResult.Yes)
{
return;
}
using SelectContainersForm selectForm =
new(_coordinator, createdSystem);
if (selectForm.ShowDialog(this) == DialogResult.OK)
{
await LoadContainersListAsync();
await RefreshAllAsync();
}
}
private async Task ScanContainersForSelectedSystemAsync()
{
SonicSystemOption? system = null;
if (_lvSystems.SelectedItems.Count > 0)
{
system = _lvSystems.SelectedItems[0].Tag as SonicSystemOption;
}
system ??= _systems.FirstOrDefault();
if (system is null)
{
// Systeme-Liste ggf. nachladen
await LoadSystemsListAsync();
system = _systems.FirstOrDefault();
}
if (system is null)
{
ShowWarning(
"Bitte zuerst ein Sonic-System anlegen oder auswählen.");
return;
}
using SelectContainersForm selectForm =
new(_coordinator, system);
if (selectForm.ShowDialog(this) == DialogResult.OK)
{
await LoadContainersListAsync();
await RefreshAllAsync();
}
} }
private async Task AddContainerAsync() private async Task AddContainerAsync()
@@ -1,7 +1,5 @@
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import javax.management.ObjectName; import javax.management.ObjectName;
@@ -11,14 +9,13 @@ import javax.management.ObjectName;
* Befehle: * Befehle:
* validate - Anmeldung + lesender Container-Zugriff * validate - Anmeldung + lesender Container-Zugriff
* restart - Container neu starten (stop/restart) * restart - Container neu starten (stop/restart)
* list - Container der Domain auflisten
* *
* javac -source 8 -target 8 SonicMfContainerTool.java * javac -source 8 -target 8 SonicMfContainerTool.java
*/ */
public final class SonicMfContainerTool public final class SonicMfContainerTool
{ {
public static final String TOOL_VERSION = public static final String TOOL_VERSION =
"2026-07-29-validate-restart-list"; "2026-07-29-validate-restart";
public static void main(String[] args) public static void main(String[] args)
{ {
@@ -29,16 +26,15 @@ public final class SonicMfContainerTool
Args parsedArguments = Args.parse(args); Args parsedArguments = Args.parse(args);
if (!"validate".equals(parsedArguments.command) if (!"validate".equals(parsedArguments.command)
&& !"restart".equals(parsedArguments.command) && !"restart".equals(parsedArguments.command))
&& !"list".equals(parsedArguments.command))
{ {
fail( fail(
"Usage: SonicMfContainerTool " "Usage: SonicMfContainerTool "
+ "<validate|restart|list> " + "<validate|restart> "
+ "--domain D " + "--domain D "
+ "--url U " + "--url U "
+ "--user U " + "--user U "
+ "[--container C] " + "--container C "
+ "[--timeout SEC]"); + "[--timeout SEC]");
return; return;
} }
@@ -51,12 +47,7 @@ public final class SonicMfContainerTool
return; return;
} }
boolean needsContainer = if (isBlank(parsedArguments.container))
"validate".equals(parsedArguments.command)
|| "restart".equals(parsedArguments.command);
if (needsContainer
&& isBlank(parsedArguments.container))
{ {
fail("container ist Pflicht fuer validate/restart."); fail("container ist Pflicht fuer validate/restart.");
return; return;
@@ -82,13 +73,7 @@ public final class SonicMfContainerTool
try try
{ {
if ("list".equals(parsedArguments.command)) if ("validate".equals(
{
listContainers(
connector,
parsedArguments.domain);
}
else if ("validate".equals(
parsedArguments.command)) parsedArguments.command))
{ {
validateConnection( validateConnection(
@@ -126,115 +111,6 @@ public final class SonicMfContainerTool
} }
} }
private static void listContainers(
Object connector,
String domain)
throws Exception
{
Method queryNamesMethod =
findMethod(
connector.getClass(),
"queryNames",
new Class[]
{
ObjectName.class,
javax.management.QueryExp.class
});
if (queryNamesMethod == null)
{
throw new IllegalStateException(
"queryNames(ObjectName, QueryExp) wurde nicht gefunden.");
}
ObjectName pattern =
new ObjectName("*:ID=AGENT");
Object rawResult = queryNamesMethod.invoke(
connector,
new Object[]
{
pattern,
null
});
if (!(rawResult instanceof Set))
{
throw new IllegalStateException(
"queryNames lieferte kein Set.");
}
Set names = (Set) rawResult;
String domainPrefix = domain + ".";
int count = 0;
System.out.println("OK:ContainerListBegin");
Iterator iterator = names.iterator();
while (iterator.hasNext())
{
Object entry = iterator.next();
if (!(entry instanceof ObjectName))
{
continue;
}
ObjectName objectName = (ObjectName) entry;
String canonical = objectName.getCanonicalName();
if (canonical == null)
{
continue;
}
// Erwartet: domain.containerName:ID=AGENT
if (!canonical.regionMatches(
true,
0,
domainPrefix,
0,
domainPrefix.length()))
{
continue;
}
int colon = canonical.indexOf(':');
if (colon <= domainPrefix.length())
{
continue;
}
String containerName =
canonical.substring(
domainPrefix.length(),
colon);
if (isBlank(containerName))
{
continue;
}
System.out.println(
"CONTAINER="
+ containerName.trim());
count++;
}
System.out.println(
"OK:ContainerListEnd count="
+ count
+ " domain="
+ domain
+ " tool="
+ TOOL_VERSION);
System.out.flush();
}
private static Object connect( private static Object connect(
String url, String url,
String user, String user,
@@ -14,5 +14,13 @@
"CertificateThumbprint": "95E906CCC9C5F227969877CF3BD46D2AD7168853", "CertificateThumbprint": "95E906CCC9C5F227969877CF3BD46D2AD7168853",
"StoreName": "My", "StoreName": "My",
"StoreLocation": "CurrentUser" "StoreLocation": "CurrentUser"
},
"Demo": {
"Enabled": true,
"TargetDirectory": "\\\\dekun-painwbdet\\e$\\proalpha-inwb\\test\\Containers\\proalpha-test.ct-ZADBService\\certs",
"TargetFileName": "server.p12",
"ContainerName": "ct-ZADBService",
"DisplayName": "DE / Demo ZADBService",
"SonicConnectionName": "DE-Test"
} }
} }