// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public class AddressRegistrationTests { [Theory, MemberData(nameof(AddressRegistrationDataIPv4))] public async Task RegisterAddresses_IPv4_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); } [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv4Port80))] [PortSupportedCondition(80)] public async Task RegisterAddresses_IPv4Port80_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); } [ConditionalTheory, MemberData(nameof(IPEndPointRegistrationDataRandomPort))] [IPv6SupportedCondition] public async Task RegisterIPEndPoint_RandomPort_Success(IPEndPoint endPoint, Func testUrl) { await RegisterIPEndPoint_Success(endPoint, testUrl); } [ConditionalTheory, MemberData(nameof(IPEndPointRegistrationDataPort443))] [IPv6SupportedCondition] [PortSupportedCondition(443)] public async Task RegisterIPEndPoint_Port443_Success(IPEndPoint endpoint, Func testUrl) { await RegisterIPEndPoint_Success(endpoint, testUrl); } [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6))] [IPv6SupportedCondition] public async Task RegisterAddresses_IPv6_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); } [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6Port80))] [IPv6SupportedCondition] [PortSupportedCondition(80)] public async Task RegisterAddresses_IPv6Port80_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); } [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6ScopeId))] [IPv6SupportedCondition] public async Task RegisterAddresses_IPv6ScopeId_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); } private async Task RegisterAddresses_Success(string addressInput, Func testUrls) { var hostBuilder = new WebHostBuilder() .UseKestrel() .UseUrls(addressInput) .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { host.Start(); foreach (var testUrl in testUrls(host.ServerFeatures.Get())) { var response = await HttpClientSlim.GetStringAsync(testUrl, validateCertificate: false); // Compare the response with Uri.ToString(), rather than testUrl directly. // Required to handle IPv6 addresses with zone index, like "fe80::3%1" Assert.Equal(new Uri(testUrl).ToString(), response); } } } private async Task RegisterIPEndPoint_Success(IPEndPoint endPoint, Func testUrl) { var hostBuilder = new WebHostBuilder() .UseKestrel(options => { options.Listen(endPoint, listenOptions => { if (testUrl(listenOptions.IPEndPoint).StartsWith("https")) { listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); } }); }) .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { host.Start(); var options = ((IOptions)host.Services.GetService(typeof(IOptions))).Value; Assert.Single(options.ListenOptions); var listenOptions = options.ListenOptions[0]; var response = await HttpClientSlim.GetStringAsync(testUrl(listenOptions.IPEndPoint), validateCertificate: false); // Compare the response with Uri.ToString(), rather than testUrl directly. // Required to handle IPv6 addresses with zone index, like "fe80::3%1" Assert.Equal(new Uri(testUrl(listenOptions.IPEndPoint)).ToString(), response); } } [Fact] public void ThrowsWhenBindingToIPv4AddressInUse() { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { var port = GetNextPort(); socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); var hostBuilder = new WebHostBuilder() .UseKestrel() .UseUrls($"http://127.0.0.1:{port}") .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { var exception = Assert.Throws(() => host.Start()); Assert.Equal($"Failed to bind to address http://127.0.0.1:{port}: address already in use.", exception.Message); } } } [ConditionalFact] [IPv6SupportedCondition] public void ThrowsWhenBindingToIPv6AddressInUse() { using (var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp)) { var port = GetNextPort(); socket.Bind(new IPEndPoint(IPAddress.IPv6Loopback, port)); var hostBuilder = new WebHostBuilder() .UseKestrel() .UseUrls($"http://[::1]:{port}") .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { var exception = Assert.Throws(() => host.Start()); Assert.Equal($"Failed to bind to address http://[::1]:{port}: address already in use.", exception.Message); } } } [Fact] public void ThrowsWhenBindingLocalhostToIPv4AddressInUse() { ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily.InterNetwork, IPAddress.Loopback); } [ConditionalFact] [IPv6SupportedCondition] public void ThrowsWhenBindingLocalhostToIPv6AddressInUse() { ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily.InterNetworkV6, IPAddress.IPv6Loopback); } [Fact] public void ThrowsWhenBindingLocalhostToDynamicPort() { var hostBuilder = new WebHostBuilder() .UseKestrel() .UseUrls("http://localhost:0") .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { Assert.Throws(() => host.Start()); } } private void ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily addressFamily, IPAddress address) { using (var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)) { var port = GetNextPort(); socket.Bind(new IPEndPoint(address, port)); var hostBuilder = new WebHostBuilder() .UseKestrel() .UseUrls($"http://localhost:{port}") .Configure(ConfigureEchoAddress); using (var host = hostBuilder.Build()) { var exception = Assert.Throws(() => host.Start()); Assert.Equal( $"Failed to bind to address http://localhost:{port} on the {(addressFamily == AddressFamily.InterNetwork ? "IPv4" : "IPv6")} loopback interface: port already in use.", exception.Message); } } } public static TheoryData> AddressRegistrationDataIPv4 { get { var dataset = new TheoryData>(); // Default host and port dataset.Add(null, _ => new[] { "http://127.0.0.1:5000/" }); dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" }); // Static ports var port = GetNextPort(); // Loopback dataset.Add($"http://127.0.0.1:{port}", _ => new[] { $"http://127.0.0.1:{port}/" }); // Localhost dataset.Add($"http://localhost:{port}", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/" }); // Any dataset.Add($"http://*:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" }); dataset.Add($"http://+:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" }); // Path after port dataset.Add($"http://127.0.0.1:{port}/base/path", _ => new[] { $"http://127.0.0.1:{port}/base/path" }); // Dynamic port and non-loopback addresses dataset.Add("http://127.0.0.1:0/", GetTestUrls); dataset.Add($"http://{Dns.GetHostName()}:0/", GetTestUrls); var ipv4Addresses = GetIPAddresses() .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); foreach (var ip in ipv4Addresses) { dataset.Add($"http://{ip}:0/", GetTestUrls); } return dataset; } } public static TheoryData> IPEndPointRegistrationDataRandomPort { get { var dataset = new TheoryData>(); // Static port var port = GetNextPort(); // Loopback dataset.Add(new IPEndPoint(IPAddress.Loopback, port), _ => $"http://127.0.0.1:{port}/"); dataset.Add(new IPEndPoint(IPAddress.Loopback, port), _ => $"https://127.0.0.1:{port}/"); // IPv6 loopback dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, port), _ => FixTestUrl($"http://[::1]:{port}/")); dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, port), _ => FixTestUrl($"https://[::1]:{port}/")); // Any dataset.Add(new IPEndPoint(IPAddress.Any, port), _ => $"http://127.0.0.1:{port}/"); dataset.Add(new IPEndPoint(IPAddress.Any, port), _ => $"https://127.0.0.1:{port}/"); // IPv6 Any dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => $"http://127.0.0.1:{port}/"); dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => FixTestUrl($"http://[::1]:{port}/")); dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => $"https://127.0.0.1:{port}/"); dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => FixTestUrl($"https://[::1]:{port}/")); // Dynamic port dataset.Add(new IPEndPoint(IPAddress.Loopback, 0), endPoint => $"http://127.0.0.1:{endPoint.Port}/"); dataset.Add(new IPEndPoint(IPAddress.Loopback, 0), endPoint => $"https://127.0.0.1:{endPoint.Port}/"); var ipv4Addresses = GetIPAddresses() .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); foreach (var ip in ipv4Addresses) { dataset.Add(new IPEndPoint(ip, 0), endPoint => FixTestUrl($"http://{endPoint}/")); dataset.Add(new IPEndPoint(ip, 0), endPoint => FixTestUrl($"https://{endPoint}/")); } return dataset; } } public static TheoryData> AddressRegistrationDataIPv4Port80 { get { var dataset = new TheoryData>(); // Default port for HTTP (80) dataset.Add("http://127.0.0.1", _ => new[] { "http://127.0.0.1/" }); dataset.Add("http://localhost", _ => new[] { "http://127.0.0.1/" }); dataset.Add("http://*", _ => new[] { "http://127.0.0.1/" }); return dataset; } } public static TheoryData> IPEndPointRegistrationDataPort443 { get { var dataset = new TheoryData>(); dataset.Add(new IPEndPoint(IPAddress.Loopback, 443), _ => "https://127.0.0.1/"); dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, 443), _ => FixTestUrl("https://[::1]/")); dataset.Add(new IPEndPoint(IPAddress.Any, 443), _ => "https://127.0.0.1/"); dataset.Add(new IPEndPoint(IPAddress.IPv6Any, 443), _ => FixTestUrl("https://[::1]/")); return dataset; } } public static TheoryData> AddressRegistrationDataIPv6 { get { var dataset = new TheoryData>(); // Default host and port dataset.Add(null, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" }); dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" }); // Static ports var port = GetNextPort(); // Loopback dataset.Add($"http://[::1]:{port}/", _ => new[] { $"http://[::1]:{port}/" }); // Localhost dataset.Add($"http://localhost:{port}", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Any dataset.Add($"http://*:{port}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); dataset.Add($"http://+:{port}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Explicit IPv4 and IPv6 on same port dataset.Add($"http://127.0.0.1:{port}/;http://[::1]:{port}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Path after port dataset.Add($"http://[::1]:{port}/base/path", _ => new[] { $"http://[::1]:{port}/base/path" }); // Dynamic port and non-loopback addresses var ipv6Addresses = GetIPAddresses() .Where(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) .Where(ip => ip.ScopeId == 0); foreach (var ip in ipv6Addresses) { dataset.Add($"http://[{ip}]:0/", GetTestUrls); } return dataset; } } public static TheoryData> AddressRegistrationDataIPv6Port80 { get { var dataset = new TheoryData>(); // Default port for HTTP (80) dataset.Add("http://[::1]", _ => new[] { "http://[::1]/" }); dataset.Add("http://localhost", _ => new[] { "http://127.0.0.1/", "http://[::1]/" }); dataset.Add("http://*", _ => new[] { "http://[::1]/" }); return dataset; } } public static TheoryData> AddressRegistrationDataIPv6ScopeId { get { var dataset = new TheoryData>(); // Dynamic port var ipv6Addresses = GetIPAddresses() .Where(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) .Where(ip => ip.ScopeId != 0); foreach (var ip in ipv6Addresses) { dataset.Add($"http://[{ip}]:0/", GetTestUrls); } return dataset; } } private static IEnumerable GetIPAddresses() { return NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.OperationalStatus == OperationalStatus.Up) .SelectMany(i => i.GetIPProperties().UnicastAddresses) .Select(a => a.Address); } private static string[] GetTestUrls(IServerAddressesFeature addressesFeature) { return addressesFeature.Addresses .Select(FixTestUrl) .ToArray(); } private static string FixTestUrl(string url) { var fixedUrl = url.Replace("://+", "://localhost") .Replace("0.0.0.0", Dns.GetHostName()) .Replace("[::]", Dns.GetHostName()); if (!fixedUrl.EndsWith("/")) { fixedUrl = fixedUrl + "/"; } return fixedUrl; } private void ConfigureEchoAddress(IApplicationBuilder app) { app.Run(context => { return context.Response.WriteAsync(context.Request.GetDisplayUrl()); }); } private static int _nextPort = 8001; private static object _portLock = new object(); private static int GetNextPort() { lock (_portLock) { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { while (true) { try { var port = _nextPort++; socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); return port; } catch (SocketException) { // Retry unless exhausted if (_nextPort == 65536) { throw; } } } } } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] private class PortSupportedConditionAttribute : Attribute, ITestCondition { private readonly int _port; private readonly Lazy _portSupported; public PortSupportedConditionAttribute(int port) { _port = port; _portSupported = new Lazy(CanBindToPort); } public bool IsMet { get { return _portSupported.Value; } } public string SkipReason { get { return $"Cannot bind to port {_port} on the host."; } } private bool CanBindToPort() { try { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { socket.Bind(new IPEndPoint(IPAddress.Loopback, _port)); return true; } } catch (SocketException) { return false; } } } } }