using HanaToolbox.Config; using HanaToolbox.Logging; using HanaToolbox.Services; using HanaToolbox.Services.Interfaces; using Spectre.Console; namespace HanaToolbox.Tui; /// /// Interactive Firewall TUI using Spectre.Console. /// Mirrors the firewalld.sh interactive flow. /// public sealed class FirewallTui( FirewallService firewallService, AppLogger _logger) { public async Task RunAsync( FirewallConfig current, CancellationToken ct = default) { AnsiConsole.Clear(); AnsiConsole.Write(new Rule("[cyan]SAP B1 Firewall Configurator[/]").RuleStyle("cyan")); AnsiConsole.WriteLine(); // Flush question var flush = AnsiConsole.Confirm("Flush (remove) all current firewall rules before applying? [creates clean slate]", defaultValue: current.FlushBeforeApply); AnsiConsole.WriteLine(); // Per-service configuration var entries = new List(); foreach (var svc in current.Services) { AnsiConsole.Write(new Rule($"[green]{svc.Name}[/] [grey]({string.Join(", ", svc.Ports)})[/]").RuleStyle("grey")); var choice = AnsiConsole.Prompt( new SelectionPrompt() .Title("Access rule:") .AddChoices("Allow from ANYWHERE (Public)", "Restrict to SPECIFIC IPs", "Skip / Block") .HighlightStyle("cyan")); var decision = choice switch { "Allow from ANYWHERE (Public)" => FirewallDecision.All, "Restrict to SPECIFIC IPs" => FirewallDecision.Ip, _ => FirewallDecision.Skip }; var ips = new List(); if (decision == FirewallDecision.Ip) { var existing = string.Join(", ", svc.AllowedIps); var ipInput = AnsiConsole.Prompt( new TextPrompt("Enter IPs or subnets (comma-separated):") .DefaultValue(existing.Length > 0 ? existing : "192.168.1.1") .AllowEmpty()); ips = ipInput .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .Where(i => i.Length > 0) .ToList(); } entries.Add(new FirewallServiceEntry { Name = svc.Name, Ports = svc.Ports, Decision = decision, AllowedIps = ips }); AnsiConsole.WriteLine(); } // Summary table AnsiConsole.Write(new Rule("[yellow]Summary[/]").RuleStyle("yellow")); var table = new Table() .AddColumn("Service") .AddColumn("Action") .AddColumn("Details"); foreach (var e in entries) { var (action, details) = e.Decision switch { FirewallDecision.All => ("[red]Open Public[/]", "0.0.0.0/0"), FirewallDecision.Ip => ("[green]Restricted[/]", string.Join(", ", e.AllowedIps)), _ => ("[grey]Blocked/Skip[/]", "-") }; table.AddRow(e.Name.Length > 35 ? e.Name[..35] : e.Name, action, details); } AnsiConsole.Write(table); AnsiConsole.WriteLine(); if (!AnsiConsole.Confirm("Apply and save these rules?", defaultValue: true)) { AnsiConsole.MarkupLine("[yellow]Aborted.[/]"); return null; } var updated = new FirewallConfig { FlushBeforeApply = flush, Services = entries, Enabled = current.Enabled, ScheduleHour = current.ScheduleHour, ScheduleMinute = current.ScheduleMinute }; // Apply rules await firewallService.ApplyAsync(updated, ct); // Safety revert window AnsiConsole.MarkupLine("[yellow]Rules applied. You have 15 seconds to confirm your connection still works.[/]"); AnsiConsole.MarkupLine("Press [cyan]ENTER[/] to keep changes permanently, or wait to auto-revert."); var confirmed = await WaitForConfirmAsync(15, ct); if (!confirmed) { AnsiConsole.MarkupLine("[red]Timeout — reverting firewall to permanent config...[/]"); await AnsiConsole.Status().StartAsync("Reverting...", async _ => { await Task.Run(() => System.Diagnostics.Process.Start("/bin/bash", "-c firewall-cmd --reload")?.WaitForExit(), ct); }); return null; } AnsiConsole.MarkupLine("[green]Changes confirmed and saved permanently.[/]"); return updated; } private static async Task WaitForConfirmAsync(int seconds, CancellationToken ct) { var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(seconds)); try { await Task.Run(() => Console.ReadLine(), cts.Token); return true; } catch (OperationCanceledException) { return false; } } }