using System.Net; using System.Net.Http; using ClawdDotNet.Tools.WebFetch; using Shouldly; namespace ClawdDotNet.Tools.Tests.WebFetch; /// /// #580: Checking the URL only sees the name. localtest.me is on no blocklist /// and resolves to 127.0.0.1 - in the test run WebFetch thus opened a connection /// to loopback. That's why the resolved address is checked, without a real network. /// public sealed class UrlGuardResolutionTests { private static Func> Resolution(params string[] addresses) => (_, _) => Task.FromResult(addresses.Select(IPAddress.Parse).ToArray()); [Theory] [InlineData("127.0.0.1")] [InlineData("192.168.40.48")] [InlineData("169.254.169.254")] [InlineData("::1")] [InlineData("::ffff:10.0.0.5")] public async Task A_name_resolving_into_the_local_network_is_blocked(string address) { var ex = await Should.ThrowAsync( () => UrlGuard.ResolveAllowedAsync("localtest.me", Resolution(address), default)); ex.Message.ShouldContain("localtest.me"); ex.Message.ShouldContain("blocked"); } [Fact] public async Task A_name_with_a_public_and_an_internal_address_is_blocked() { // Exactly the pattern with which you outwit a sampling check. await Should.ThrowAsync( () => UrlGuard.ResolveAllowedAsync("gemischt.example", Resolution("93.184.215.14", "127.0.0.1"), default)); } [Fact] public async Task A_name_with_public_addresses_is_allowed() { var addresses = await UrlGuard.ResolveAllowedAsync( "example.com", Resolution("93.184.215.14", "2606:2800:21f:cb07:6820:80da:af6b:8b2c"), default); addresses.Length.ShouldBe(2); } [Fact] public async Task An_IP_literal_is_checked_without_resolution() { var called = false; await Should.ThrowAsync(() => UrlGuard.ResolveAllowedAsync( "[::1]", (_, _) => { called = true; return Task.FromResult(Array.Empty()); }, default)); called.ShouldBeFalse(); } [Fact] public async Task A_name_without_address_is_not_connected() => await Should.ThrowAsync( () => UrlGuard.ResolveAllowedAsync("nirgends.example", Resolution(), default)); }