diff --git a/src/EmbedIO/Net/EndPointManager.cs b/src/EmbedIO/Net/EndPointManager.cs index 21033129..df18a1e8 100644 --- a/src/EmbedIO/Net/EndPointManager.cs +++ b/src/EmbedIO/Net/EndPointManager.cs @@ -79,28 +79,36 @@ internal static void AddPrefix(string p, HttpListener listener) } // listens on all the interfaces if host name cannot be parsed by IPAddress. - var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure); - epl.AddPrefix(lp, listener); + var endPointListeners = GetEpListeners(lp.Host, lp.Port, listener, lp.Secure); + foreach (var epl in endPointListeners) { + epl.AddPrefix(lp, listener); + } } - private static EndPointListener GetEpListener(string host, int port, HttpListener listener, bool secure = false) + private static IEnumerable GetEpListeners(string host, int port, HttpListener listener, bool secure = false) { - var address = ResolveAddress(host); + var addresses = ResolveAddresses(host); + var endPointListeners = new List(); + + foreach (var address in addresses) + { + var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary()); + endPointListeners.Add(p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure))); + } - var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary()); - return p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure)); + return endPointListeners; } - private static IPAddress ResolveAddress(string host) + private static IEnumerable ResolveAddresses(string host) { if (host == "*" || host == "+" || host == "0.0.0.0") { - return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any; + return new[] { UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any }; } if (IPAddress.TryParse(host, out var address)) { - return address; + return new[] { address }; } try @@ -110,11 +118,10 @@ private static IPAddress ResolveAddress(string host) AddressList = Dns.GetHostAddresses(host), }; - return hostEntry.AddressList[0]; + return hostEntry.AddressList; } - catch - { - return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any; + catch { + return new[] {UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any}; } } @@ -129,8 +136,10 @@ private static void RemovePrefix(string prefix, HttpListener listener) return; } - var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure); - epl.RemovePrefix(lp); + var epls = GetEpListeners(lp.Host, lp.Port, listener, lp.Secure); + foreach (var epl in epls) { + epl.RemovePrefix(lp); + } } catch (SocketException) { diff --git a/test/EmbedIO.Tests/Issues/Issue576_LocalhostDualStack.cs b/test/EmbedIO.Tests/Issues/Issue576_LocalhostDualStack.cs new file mode 100644 index 00000000..52f66243 --- /dev/null +++ b/test/EmbedIO.Tests/Issues/Issue576_LocalhostDualStack.cs @@ -0,0 +1,82 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using NUnit.Framework; +using Swan; + +namespace EmbedIO.Tests.Issues; + +[TestFixture] +public class Issue576_LocalhostDualStack { + [TestCase("127.0.0.1")] + [TestCase("::1")] + public async Task LocalhostAcceptsDualStack(string address) { + if (SwanRuntime.OS != Swan.OperatingSystem.Windows) + Assert.Ignore("Only Windows"); + + using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877"); + instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now)); + + _ = instance.RunAsync(); + + using var handler = BuildFakeResolver(IPAddress.Parse(address)); + using var client = new HttpClient(handler); + + var uri = new Uri("http://localhost:8877"); + Assert.IsNotEmpty(await client.GetStringAsync(uri).ConfigureAwait(false)); + } + + [TestCase("http://localhost:8877")] + [TestCase("http://127.0.0.1:8877")] + public async Task LocalhostV4AcceptsIpAndHost(string uri) { + if (SwanRuntime.OS != Swan.OperatingSystem.Windows) + Assert.Ignore("Only Windows"); + + using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877", "http://127.0.0.1:8877"); + instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now)); + + _ = instance.RunAsync(); + + using var handler = BuildFakeResolver(IPAddress.Loopback); // force ipv4 for this test + using var client = new HttpClient(handler); + + Assert.IsNotEmpty(await client.GetStringAsync(new Uri(uri)).ConfigureAwait(false)); + } + + [TestCase("http://localhost:8877")] + [TestCase("http://[::1]:8877")] + public async Task LocalhostV6AcceptsIpAndHost(string uri) { + if (SwanRuntime.OS != Swan.OperatingSystem.Windows) + Assert.Ignore("Only Windows"); + + using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877", "http://[::1]:8877"); + instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now)); + + _ = instance.RunAsync(); + + using var handler = BuildFakeResolver(IPAddress.IPv6Loopback); // force ipv6 for this test + using var client = new HttpClient(handler); + + Assert.IsNotEmpty(await client.GetStringAsync(new Uri(uri)).ConfigureAwait(false)); + } + + private static SocketsHttpHandler BuildFakeResolver(IPAddress target) { + // Borrowed from https://www.meziantou.net/forcing-httpclient-to-use-ipv4-or-ipv6-addresses.htm and adapted + return new SocketsHttpHandler { + ConnectCallback = async (context, cancellationToken) => { + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; + + try { + await socket.ConnectAsync(target, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + } catch { + socket.Dispose(); + throw; + } + }, + }; + } +} \ No newline at end of file