Add live TLS certificate probing and improve restart error handling.
Configure CertificateCheckUrl per container for curl-like TLS checks, classify Sonic permission errors, and extend setup wizard for container management. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Liest das präsentierte TLS-Zertifikat eines Endpoints
|
||||
/// (entspricht grob <c>curl -v https://host:port</c>).
|
||||
/// </summary>
|
||||
public sealed class TlsEndpointProbeService
|
||||
{
|
||||
private static readonly Regex HostPortRegex = new(
|
||||
@"^(?<host>[^:/]+)(:(?<port>\d{1,5}))?$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public CertificateProbeResult Probe(
|
||||
string? checkUrl,
|
||||
int timeoutMilliseconds = 8000)
|
||||
{
|
||||
if (!TryParseEndpoint(
|
||||
checkUrl,
|
||||
out string host,
|
||||
out int port,
|
||||
out string serverName,
|
||||
out string? parseError))
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
parseError ?? "Ungültige Prüf-URL.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource cts = new(timeoutMilliseconds);
|
||||
using TcpClient client = new();
|
||||
|
||||
using (cts.Token.Register(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Timeout beendet die Verbindung.
|
||||
}
|
||||
}))
|
||||
{
|
||||
client.Connect(host, port);
|
||||
}
|
||||
|
||||
using SslStream ssl = new(
|
||||
client.GetStream(),
|
||||
leaveInnerStreamOpen: false,
|
||||
userCertificateValidationCallback:
|
||||
static (_, _, _, _) => true);
|
||||
|
||||
ssl.AuthenticateAsClient(serverName);
|
||||
|
||||
if (ssl.RemoteCertificate is null)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
$"Kein Zertifikat von {host}:{port} erhalten.");
|
||||
}
|
||||
|
||||
using X509Certificate2 certificate = new(ssl.RemoteCertificate);
|
||||
CertificateInfo info = ToCertificateInfo(certificate);
|
||||
|
||||
return CertificateProbeResult.Found(
|
||||
$"{host}:{port}",
|
||||
info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
$"TLS-Prüfung {host}:{port} fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseEndpoint(
|
||||
string? value,
|
||||
out string host,
|
||||
out int port,
|
||||
out string serverName,
|
||||
out string? error)
|
||||
{
|
||||
host = string.Empty;
|
||||
port = 443;
|
||||
serverName = string.Empty;
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
error = "Keine Prüf-URL hinterlegt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string trimmed = value.Trim();
|
||||
|
||||
if (Uri.TryCreate(trimmed, UriKind.Absolute, out Uri? uri)
|
||||
&& (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("ssl", StringComparison.OrdinalIgnoreCase)
|
||||
|| uri.Scheme.Equals("tcp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri.Host))
|
||||
{
|
||||
error = "Host in der Prüf-URL fehlt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = uri.Host;
|
||||
serverName = uri.Host;
|
||||
port = uri.IsDefaultPort
|
||||
? (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
|
||||
? 80
|
||||
: 443)
|
||||
: uri.Port;
|
||||
|
||||
if (port is < 1 or > 65535)
|
||||
{
|
||||
error = "Port muss zwischen 1 und 65535 liegen.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Match match = HostPortRegex.Match(trimmed);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error =
|
||||
"Prüf-URL ungültig. Beispiele:\n"
|
||||
+ "https://server:8443\n"
|
||||
+ "server:8443\n"
|
||||
+ "server";
|
||||
return false;
|
||||
}
|
||||
|
||||
host = match.Groups["host"].Value;
|
||||
serverName = host;
|
||||
|
||||
if (match.Groups["port"].Success)
|
||||
{
|
||||
if (!int.TryParse(match.Groups["port"].Value, out port)
|
||||
|| port is < 1 or > 65535)
|
||||
{
|
||||
error = "Port muss zwischen 1 und 65535 liegen.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(host);
|
||||
}
|
||||
|
||||
public static string? NormalizeCheckUrl(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string trimmed = value.Trim();
|
||||
|
||||
if (!TryParseEndpoint(
|
||||
trimmed,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out string? error))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
error ?? "Ungültige Prüf-URL.",
|
||||
nameof(value));
|
||||
}
|
||||
|
||||
if (trimmed.Length > 500)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Die Prüf-URL darf maximal 500 Zeichen enthalten.",
|
||||
nameof(value));
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static CertificateInfo ToCertificateInfo(
|
||||
X509Certificate2 certificate)
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user