150 lines
5.2 KiB
C#
150 lines
5.2 KiB
C#
using HanaToolbox.Config;
|
|
using HanaToolbox.Logging;
|
|
using HanaToolbox.Services;
|
|
using HanaToolbox.Services.Interfaces;
|
|
using Spectre.Console;
|
|
|
|
namespace HanaToolbox.Tui;
|
|
|
|
/// <summary>
|
|
/// Interactive Firewall TUI using Spectre.Console.
|
|
/// Mirrors the firewalld.sh interactive flow.
|
|
/// </summary>
|
|
public sealed class FirewallTui(
|
|
FirewallService firewallService,
|
|
AppLogger _logger)
|
|
{
|
|
public async Task<FirewallConfig?> 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<FirewallServiceEntry>();
|
|
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<string>()
|
|
.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<string>();
|
|
if (decision == FirewallDecision.Ip)
|
|
{
|
|
var existing = string.Join(", ", svc.AllowedIps);
|
|
var ipInput = AnsiConsole.Prompt(
|
|
new TextPrompt<string>("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<bool> 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;
|
|
}
|
|
}
|
|
}
|