using System.Diagnostics; using System.Text; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Führt PowerShell-Befehle via WinRM (Invoke-Command) auf dem Sonic-Server aus. /// /// Voraussetzungen auf dem Zielrechner: /// - WinRM muss aktiviert sein: Enable-PSRemoting -Force /// - Ausführungsrichtlinie: Set-ExecutionPolicy RemoteSigned /// /// Voraussetzungen auf dem App-Rechner (einmalig, als Admin): /// - Set-Item WSMan:\localhost\Client\TrustedHosts -Value "dekun-painwbdet" /// public sealed class WinRmExecutor { private readonly SonicConnection _connection; public WinRmExecutor(SonicConnection connection) { _connection = connection; } /// /// Prüft die WinRM-Konnektivität und ob der Sonic-Server per TCP erreichbar ist. /// public async Task<(bool Success, string? Error)> TestConnectionAsync( CancellationToken cancellationToken = default) { string host = ExtractHost(_connection.ConnectionUrl); // TCP-Ping auf WinRM-Port try { using System.Net.Sockets.TcpClient tcp = new(); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 5, 30))); await tcp.ConnectAsync(host, _connection.WinRmPort, cts.Token); } catch (Exception ex) when (ex is not OperationCanceledException) { return (false, $"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" + $"Auf dem Zielrechner ausführen: Enable-PSRemoting -Force"); } // Kurztest: Hostname zurückgeben (bool ok, string? output, string? error) = await RunScriptAsync( "$env:COMPUTERNAME", cancellationToken); return ok ? (true, null) : (false, $"WinRM-Verbindung fehlgeschlagen: {error}"); } /// /// Führt einen Scriptblock auf dem Remote-Rechner aus und gibt Stdout zurück. /// public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync( string scriptBlock, CancellationToken cancellationToken = default) { string host = ExtractHost(_connection.ConnectionUrl); // Passwort als SecureString – bleibt im PowerShell-Prozess, wird nicht als Argument übergeben // Stattdessen: Scriptblock über stdin senden string fullScript = BuildScript(host, scriptBlock); ProcessStartInfo psi = new() { FileName = "powershell.exe", Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -Command -", UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardInputEncoding = Encoding.UTF8 }; using Process process = new() { StartInfo = psi }; if (!process.Start()) { return (false, null, "PowerShell-Prozess konnte nicht gestartet werden."); } // Skript über stdin – Credentials gehen NICHT als sichtbares Argument durch await process.StandardInput.WriteAsync(fullScript); process.StandardInput.Close(); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600))); Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); Task stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); try { await process.WaitForExitAsync(timeoutCts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { try { process.Kill(entireProcessTree: true); } catch { /* ignore */ } return (false, null, $"WinRM-Ausführung Timeout nach {_connection.TimeoutSeconds}s."); } string stdout = (await stdoutTask).Trim(); string stderr = (await stderrTask).Trim(); if (process.ExitCode == 0) { return (true, stdout, stderr.Length > 0 ? stderr : null); } return (false, stdout.Length > 0 ? stdout : null, stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}"); } private string BuildScript(string host, string scriptBlock) { // Passwort über Variable, nicht als Argument – verhindert Sichtbarkeit in Prozessliste string escapedPwd = _connection.Password.Replace("'", "''"); string escapedUser = _connection.Username.Replace("'", "''"); string escapedHost = host.Replace("'", "''"); return "$ErrorActionPreference = 'Stop'\n" + $"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" + $"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" + $"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} -Credential $cred -ScriptBlock {{\n" + $" {scriptBlock}\n" + "} -ErrorAction Stop"; } /// /// Ersetzt Platzhalter in einem konfigurierten WinRM-Script. /// {container} → Container-Name (einfache Hochkommas werden verdoppelt) /// {domain} → Domain-Name /// {xapiPath} → Pfad zur XApi-Quelldatei /// public static string ApplyScriptTemplate(string template, string containerName, string domainName = "", string xapiPath = "") => template .Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) .Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) .Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase); private static string ExtractHost(string connectionUrl) { try { return new Uri(connectionUrl).Host; } catch { return connectionUrl; } } private static string Truncate(string s, int max = 600) => s.Length <= max ? s : s[..max] + "…"; }