using System.Net;
using System.Net.Sockets;
namespace WebhookServer.Core.Auth;
///
/// Compiled allow-list of IPs and CIDR ranges. Empty list = allow all.
///
public sealed class IpAllowList
{
private readonly List _networks;
public bool IsEmpty => _networks.Count == 0;
private IpAllowList(List networks) => _networks = networks;
public bool Contains(IPAddress address)
{
if (IsEmpty) return true;
var normalized = Normalize(address);
foreach (var net in _networks)
{
if (net.BaseAddress.AddressFamily != normalized.AddressFamily) continue;
if (net.Contains(normalized)) return true;
}
return false;
}
///
/// Parse a list of allowlist entries. Each entry may be a single IP or a CIDR.
/// Throws on the first invalid entry.
///
public static IpAllowList Parse(IEnumerable entries)
{
var nets = new List();
foreach (var raw in entries)
{
var entry = raw?.Trim();
if (string.IsNullOrEmpty(entry)) continue;
nets.Add(ParseEntry(entry));
}
return new IpAllowList(nets);
}
public static bool TryParse(IEnumerable entries, out IpAllowList list, out string? error)
{
var nets = new List();
foreach (var raw in entries)
{
var entry = raw?.Trim();
if (string.IsNullOrEmpty(entry)) continue;
try
{
nets.Add(ParseEntry(entry));
}
catch (FormatException ex)
{
list = new IpAllowList(new List());
error = $"invalid entry '{raw}': {ex.Message}";
return false;
}
}
list = new IpAllowList(nets);
error = null;
return true;
}
private static IPNetwork ParseEntry(string entry)
{
if (entry.Contains('/'))
return IPNetwork.Parse(entry);
if (!IPAddress.TryParse(entry, out var addr))
throw new FormatException($"'{entry}' is not a valid IP address or CIDR");
var prefix = addr.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
return new IPNetwork(Normalize(addr), prefix);
}
private static IPAddress Normalize(IPAddress address)
{
if (address.AddressFamily == AddressFamily.InterNetworkV6 && address.IsIPv4MappedToIPv6)
return address.MapToIPv4();
return address;
}
}