Configurable bind addresses + display host in Server Settings
ServerConfig grows two fields: - BindAddresses: list of IPs Kestrel binds to (empty = all interfaces, current behavior). Listening only on a subset is useful when the host has multiple NICs and the webhook should not be reachable on all of them. - DisplayHost: the hostname/IP the GUI splices into the URL column and Copy URL button. Cosmetic; doesn't affect what the server accepts. Server Settings dialog gains a "Network" section: a checkbox for "all interfaces" plus per-NIC checkboxes auto-detected via NetworkInterface. GetAllNetworkInterfaces, and an editable ComboBox for the display host pre-populated with detected IPs and the machine name. Listener restart fires on BindAddresses change but not on DisplayHost change (cosmetic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,8 +57,9 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
|
||||
if (status is not null)
|
||||
{
|
||||
HttpBaseUrl = $"http://localhost:{status.HttpPort}";
|
||||
HttpsBaseUrl = status.HttpsPort.HasValue ? $"https://localhost:{status.HttpsPort.Value}" : null;
|
||||
var host = string.IsNullOrEmpty(status.DisplayHost) ? "localhost" : status.DisplayHost;
|
||||
HttpBaseUrl = $"http://{host}:{status.HttpPort}";
|
||||
HttpsBaseUrl = status.HttpsPort.HasValue ? $"https://{host}:{status.HttpsPort.Value}" : null;
|
||||
}
|
||||
|
||||
Endpoints.Clear();
|
||||
@@ -202,6 +203,8 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
ServerConfig.HttpPort = vm.HttpPort;
|
||||
ServerConfig.TrustedProxies = vm.TrustedProxiesList;
|
||||
ServerConfig.HttpsBinding = vm.BuildBinding();
|
||||
ServerConfig.BindAddresses = vm.BindAddressesList;
|
||||
ServerConfig.DisplayHost = vm.DisplayHostValue;
|
||||
await _client.InvokeAsync(AdminOps.UpdateConfig, ServerConfig).ConfigureAwait(false);
|
||||
await RefreshAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Versioning;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -16,6 +20,14 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private string _pfxPassword = "";
|
||||
[ObservableProperty] private string _thumbprint = "";
|
||||
[ObservableProperty] private string _trustedProxiesText = "";
|
||||
[ObservableProperty] private bool _listenAllInterfaces = true;
|
||||
[ObservableProperty] private string _displayHost = "localhost";
|
||||
|
||||
/// <summary>One row per detected local IPv4/IPv6 address. Bound for "listen on" checkboxes.</summary>
|
||||
public ObservableCollection<NetworkAddressRow> Addresses { get; } = new();
|
||||
|
||||
/// <summary>Suggestions for the Display URL host dropdown (detected IPs + localhost + machine name).</summary>
|
||||
public ObservableCollection<string> DisplayHostChoices { get; } = new();
|
||||
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
@@ -31,11 +43,48 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
PfxPath = b?.PfxPath ?? "";
|
||||
PfxPassword = b?.PfxPassword?.Plaintext ?? "";
|
||||
Thumbprint = b?.Thumbprint ?? "";
|
||||
|
||||
var detected = DetectLocalAddresses();
|
||||
var alreadyBound = new HashSet<string>(config.BindAddresses, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ListenAllInterfaces = config.BindAddresses.Count == 0;
|
||||
foreach (var (addr, label) in detected)
|
||||
{
|
||||
Addresses.Add(new NetworkAddressRow
|
||||
{
|
||||
Address = addr,
|
||||
Label = label,
|
||||
IsBound = !ListenAllInterfaces && alreadyBound.Contains(addr),
|
||||
});
|
||||
}
|
||||
// Surface any persisted address that isn't currently detected (e.g. a NIC unplugged
|
||||
// since save) so the user can keep or remove it explicitly.
|
||||
foreach (var entry in config.BindAddresses)
|
||||
{
|
||||
if (Addresses.Any(a => string.Equals(a.Address, entry, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
Addresses.Add(new NetworkAddressRow { Address = entry, Label = "(not currently present)", IsBound = true });
|
||||
}
|
||||
|
||||
DisplayHostChoices.Add("localhost");
|
||||
DisplayHostChoices.Add(Environment.MachineName);
|
||||
foreach (var (addr, _) in detected)
|
||||
if (!DisplayHostChoices.Contains(addr))
|
||||
DisplayHostChoices.Add(addr);
|
||||
|
||||
DisplayHost = string.IsNullOrEmpty(config.DisplayHost) ? "localhost" : config.DisplayHost;
|
||||
}
|
||||
|
||||
public List<string> TrustedProxiesList =>
|
||||
(TrustedProxiesText ?? "").Split(new[] { '\r', '\n', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
|
||||
public List<string> BindAddressesList =>
|
||||
ListenAllInterfaces
|
||||
? new List<string>()
|
||||
: Addresses.Where(a => a.IsBound).Select(a => a.Address).ToList();
|
||||
|
||||
public string? DisplayHostValue =>
|
||||
string.IsNullOrEmpty(DisplayHost) || DisplayHost == "localhost" ? null : DisplayHost.Trim();
|
||||
|
||||
public HttpsBinding? BuildBinding()
|
||||
{
|
||||
if (!HttpsEnabled) return null;
|
||||
@@ -58,4 +107,29 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
|
||||
[RelayCommand]
|
||||
private void Save() => Accepted = true;
|
||||
|
||||
private static IEnumerable<(string Address, string Label)> DetectLocalAddresses()
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
if (ni.OperationalStatus != OperationalStatus.Up) continue;
|
||||
if (ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel) continue;
|
||||
foreach (var ua in ni.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
if (ua.Address.AddressFamily != AddressFamily.InterNetwork &&
|
||||
ua.Address.AddressFamily != AddressFamily.InterNetworkV6) continue;
|
||||
var key = ua.Address.ToString();
|
||||
if (!seen.Add(key)) continue;
|
||||
yield return (key, $"{ni.Name} ({ni.NetworkInterfaceType})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class NetworkAddressRow : ObservableObject
|
||||
{
|
||||
public required string Address { get; init; }
|
||||
public required string Label { get; init; }
|
||||
[ObservableProperty] private bool _isBound;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user