using System.Net;
using System.Net.Sockets;
namespace w4c_workflows.Services.Security;
///
/// Authorises an outbound before a workflow node sends it.
/// Names are resolved up front and every returned address is checked, so a host
/// that maps to both a public and a private address is rejected rather than
/// accepted. The connect-time half of the defence is
/// : egress clients re-resolve and re-validate the
/// address in their ConnectCallback, so a name that answers differently on
/// the second lookup (DNS rebinding) cannot reach a blocked address.
/// Literal IPs skip DNS entirely.
///
public sealed class EgressGuard
{
private readonly EgressPolicy _policy;
private readonly IHostAddressResolver _resolver;
public EgressGuard(EgressPolicy policy, IHostAddressResolver resolver)
{
_policy = policy;
_resolver = resolver;
}
/// Ceiling the HTTP node applies to its own redirect setting.
public int MaxRedirects => _policy.MaxRedirects;
public async Task AuthorizeAsync(Uri uri, CancellationToken ct)
{
var hostDecision = _policy.CheckHost(uri);
if (hostDecision != null)
return hostDecision;
var host = uri.DnsSafeHost;
if (IPAddress.TryParse(host, out var literal))
return _policy.CheckAddresses(host, new[] { literal });
IReadOnlyList addresses;
try
{
addresses = await _resolver.ResolveAsync(host, ct);
}
catch (SocketException ex)
{
return PolicyDecision.Deny("dns_failure", $"could not resolve host '{host}': {ex.Message}");
}
catch (ArgumentException ex)
{
return PolicyDecision.Deny("dns_failure", $"could not resolve host '{host}': {ex.Message}");
}
return _policy.CheckAddresses(host, addresses);
}
}