From 7a3a731686e6868dc91af847431ba11c94f604b4 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 19 Apr 2017 14:59:49 -0700 Subject: [PATCH] Refactor address binding and handle EAFNOSUPPORT - Simplify KestrelServer by refactoring address binding into a separate class - Use strategy pattern to implement address binding for different sceanrios - Add fallback from binding 0.0.0.0 if binding to [::] fails (can happen if UvException with EAFNOSUPPORT is thrown) --- .../Internal/AddressBinder.cs | 307 ++++++++++++++++++ .../KestrelServer.cs | 185 +---------- .../AddressBinderTests.cs | 121 +++++++ .../CreateIPEndpointTests.cs | 25 -- .../AddressRegistrationTests.cs | 2 +- 5 files changed, 435 insertions(+), 205 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/AddressBinder.cs create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/AddressBinderTests.cs delete mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/CreateIPEndpointTests.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/AddressBinder.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/AddressBinder.cs new file mode 100644 index 0000000000..e30418727b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/AddressBinder.cs @@ -0,0 +1,307 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class AddressBinder + { + public static async Task BindAsync(IServerAddressesFeature addresses, + List listenOptions, + ILogger logger, + Func createBinding) + { + var strategy = CreateStrategy( + listenOptions.ToArray(), + addresses.Addresses.ToArray(), + addresses.PreferHostingUrls); + + var context = new AddressBindContext + { + Addresses = addresses.Addresses, + ListenOptions = listenOptions, + Logger = logger, + CreateBinding = createBinding + }; + + // reset options. The actual used options and addresses will be populated + // by the address binding feature + listenOptions.Clear(); + addresses.Addresses.Clear(); + + await strategy.BindAsync(context).ConfigureAwait(false); + } + + private class AddressBindContext + { + public ICollection Addresses { get; set; } + public List ListenOptions { get; set; } + public ILogger Logger { get; set; } + + public Func CreateBinding { get; set; } + } + + private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses) + { + var hasListenOptions = listenOptions.Length > 0; + var hasAddresses = addresses.Length > 0; + + if (preferAddresses && hasAddresses) + { + if (hasListenOptions) + { + return new OverrideWithAddressesStrategy(addresses); + } + + return new AddressesStrategy(addresses); + } + else if (hasListenOptions) + { + if (hasAddresses) + { + return new OverrideWithEndpointsStrategy(listenOptions, addresses); + } + + return new EndpointsStrategy(listenOptions); + } + else if (hasAddresses) + { + // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature. + return new AddressesStrategy(addresses); + } + else + { + // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. + return new DefaultAddressStrategy(); + } + } + + /// + /// Returns an for the given host an port. + /// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any. + /// + protected internal static bool TryCreateIPEndPoint(ServerAddress address, out IPEndPoint endpoint) + { + if (!IPAddress.TryParse(address.Host, out var ip)) + { + endpoint = null; + return false; + } + + endpoint = new IPEndPoint(ip, address.Port); + return true; + } + + private static Task BindEndpointAsync(IPEndPoint endpoint, AddressBindContext context) + => BindEndpointAsync(new ListenOptions(endpoint), context); + + private static async Task BindEndpointAsync(ListenOptions endpoint, AddressBindContext context) + { + try + { + await context.CreateBinding(endpoint).ConfigureAwait(false); + } + catch (AddressInUseException ex) + { + throw new IOException($"Failed to bind to address {endpoint}: address already in use.", ex); + } + + context.ListenOptions.Add(endpoint); + } + + private static async Task BindLocalhostAsync(ServerAddress address, AddressBindContext context) + { + if (address.Port == 0) + { + throw new InvalidOperationException("Dynamic port binding is not supported when binding to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both."); + } + + var exceptions = new List(); + + try + { + await BindEndpointAsync(new IPEndPoint(IPAddress.Loopback, address.Port), context).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is IOException)) + { + context.Logger.LogWarning(0, $"Unable to bind to {address} on the IPv4 loopback interface: ({ex.Message})"); + exceptions.Add(ex); + } + + try + { + await BindEndpointAsync(new IPEndPoint(IPAddress.IPv6Loopback, address.Port), context).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is IOException)) + { + context.Logger.LogWarning(0, $"Unable to bind to {address} on the IPv6 loopback interface: ({ex.Message})"); + exceptions.Add(ex); + } + + if (exceptions.Count == 2) + { + throw new IOException($"Failed to bind to address {address}.", new AggregateException(exceptions)); + } + + // If StartLocalhost doesn't throw, there is at least one listener. + // The port cannot change for "localhost". + context.Addresses.Add(address.ToString()); + } + + private static async Task BindAddressAsync(string address, AddressBindContext context) + { + var parsedAddress = ServerAddress.FromUrl(address); + + if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"HTTPS endpoints can only be configured using {nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}()."); + } + else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Unrecognized scheme in server address '{address}'. Only 'http://' is supported."); + } + + if (!string.IsNullOrEmpty(parsedAddress.PathBase)) + { + throw new InvalidOperationException($"A path base can only be configured using {nameof(IApplicationBuilder)}.UsePathBase()."); + } + + if (parsedAddress.IsUnixPipe) + { + var endPoint = new ListenOptions(parsedAddress.UnixPipePath); + await BindEndpointAsync(endPoint, context).ConfigureAwait(false); + context.Addresses.Add(endPoint.GetDisplayName()); + } + else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. + await BindLocalhostAsync(parsedAddress, context).ConfigureAwait(false); + } + else + { + ListenOptions options; + if (TryCreateIPEndPoint(parsedAddress, out var endpoint)) + { + options = new ListenOptions(endpoint); + await BindEndpointAsync(options, context).ConfigureAwait(false); + } + else + { + // when address is 'http://hostname:port', 'http://*:port', or 'http://+:port' + try + { + options = new ListenOptions(new IPEndPoint(IPAddress.IPv6Any, parsedAddress.Port)); + await BindEndpointAsync(options, context).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is IOException)) + { + // for machines that do not support IPv6 + options = new ListenOptions(new IPEndPoint(IPAddress.Any, parsedAddress.Port)); + await BindEndpointAsync(options, context).ConfigureAwait(false); + } + } + + context.Addresses.Add(options.GetDisplayName()); + } + } + + private interface IStrategy + { + Task BindAsync(AddressBindContext context); + } + + private class DefaultAddressStrategy : IStrategy + { + public async Task BindAsync(AddressBindContext context) + { + context.Logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default."); + + await BindLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), context).ConfigureAwait(false); + } + } + + private class OverrideWithAddressesStrategy : AddressesStrategy + { + public OverrideWithAddressesStrategy(IReadOnlyCollection addresses) + : base(addresses) + { + } + + public override Task BindAsync(AddressBindContext context) + { + var joined = string.Join(", ", _addresses); + context.Logger.LogInformation($"Overriding endpoints defined in UseKestrel() since {nameof(IServerAddressesFeature.PreferHostingUrls)} is set to true. Binding to address(es) '{joined}' instead."); + + return base.BindAsync(context); + } + } + + private class OverrideWithEndpointsStrategy : EndpointsStrategy + { + private readonly string[] _originalAddresses; + + public OverrideWithEndpointsStrategy(IReadOnlyCollection endpoints, string[] originalAddresses) + : base(endpoints) + { + _originalAddresses = originalAddresses; + } + + public override Task BindAsync(AddressBindContext context) + { + var joined = string.Join(", ", _originalAddresses); + context.Logger.LogWarning($"Overriding address(es) {joined}. Binding to endpoints defined in UseKestrel() instead."); + + return base.BindAsync(context); + } + } + + private class EndpointsStrategy : IStrategy + { + private readonly IReadOnlyCollection _endpoints; + + public EndpointsStrategy(IReadOnlyCollection endpoints) + { + _endpoints = endpoints; + } + + public virtual async Task BindAsync(AddressBindContext context) + { + foreach (var endpoint in _endpoints) + { + await BindEndpointAsync(endpoint, context).ConfigureAwait(false); + + context.Addresses.Add(endpoint.GetDisplayName()); + } + } + } + + private class AddressesStrategy : IStrategy + { + protected readonly IReadOnlyCollection _addresses; + + public AddressesStrategy(IReadOnlyCollection addresses) + { + _addresses = addresses; + } + + public virtual async Task BindAsync(AddressBindContext context) + { + foreach (var address in _addresses) + { + await BindAddressAsync(address, context).ConfigureAwait(false); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServer.cs index 08cd079913..98504a4f58 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServer.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; @@ -111,50 +107,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core ServerOptions = Options }; - var listenOptions = Options.ListenOptions; - var hasListenOptions = listenOptions.Any(); - var hasServerAddresses = _serverAddresses.Addresses.Any(); - - if (_serverAddresses.PreferHostingUrls && hasServerAddresses) + async Task OnBind(ListenOptions endpoint) { - if (hasListenOptions) - { - var joined = string.Join(", ", _serverAddresses.Addresses); - _logger.LogInformation($"Overriding endpoints defined in UseKestrel() since {nameof(IServerAddressesFeature.PreferHostingUrls)} is set to true. Binding to address(es) '{joined}' instead."); + var connectionHandler = new ConnectionHandler(endpoint, serviceContext, application); + var transport = _transportFactory.Create(endpoint, connectionHandler); + _transports.Add(transport); - listenOptions.Clear(); - } - - await BindToServerAddresses(listenOptions, serviceContext, application, cancellationToken).ConfigureAwait(false); + await transport.BindAsync().ConfigureAwait(false); } - else if (hasListenOptions) - { - if (hasServerAddresses) - { - var joined = string.Join(", ", _serverAddresses.Addresses); - _logger.LogWarning($"Overriding address(es) '{joined}'. Binding to endpoints defined in UseKestrel() instead."); - _serverAddresses.Addresses.Clear(); - } - - await BindToEndpoints(listenOptions, serviceContext, application).ConfigureAwait(false); - } - else if (hasServerAddresses) - { - // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature. - await BindToServerAddresses(listenOptions, serviceContext, application, cancellationToken).ConfigureAwait(false); - } - else - { - _logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default."); - - // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. - await StartLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), serviceContext, application, cancellationToken).ConfigureAwait(false); - - // If StartLocalhost doesn't throw, there is at least one listener. - // The port cannot change for "localhost". - _serverAddresses.Addresses.Add(Constants.DefaultServerAddress); - } + await AddressBinder.BindAsync(_serverAddresses, Options.ListenOptions, _logger, OnBind).ConfigureAwait(false); } catch (Exception ex) { @@ -164,72 +126,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } } - private async Task BindToServerAddresses(List listenOptions, ServiceContext serviceContext, IHttpApplication application, CancellationToken cancellationToken) - { - var copiedAddresses = _serverAddresses.Addresses.ToArray(); - _serverAddresses.Addresses.Clear(); - foreach (var address in copiedAddresses) - { - var parsedAddress = ServerAddress.FromUrl(address); - - if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"HTTPS endpoints can only be configured using {nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}()."); - } - else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Unrecognized scheme in server address '{address}'. Only 'http://' is supported."); - } - - if (!string.IsNullOrEmpty(parsedAddress.PathBase)) - { - throw new InvalidOperationException($"A path base can only be configured using {nameof(IApplicationBuilder)}.UsePathBase()."); - } - - if (parsedAddress.IsUnixPipe) - { - listenOptions.Add(new ListenOptions(parsedAddress.UnixPipePath)); - } - else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) - { - // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. - await StartLocalhostAsync(parsedAddress, serviceContext, application, cancellationToken).ConfigureAwait(false); - // If StartLocalhost doesn't throw, there is at least one listener. - // The port cannot change for "localhost". - _serverAddresses.Addresses.Add(parsedAddress.ToString()); - } - else - { - // These endPoints will be added later to _serverAddresses.Addresses - listenOptions.Add(new ListenOptions(CreateIPEndPoint(parsedAddress))); - } - } - - await BindToEndpoints(listenOptions, serviceContext, application).ConfigureAwait(false); - } - - private async Task BindToEndpoints(List listenOptions, ServiceContext serviceContext, IHttpApplication application) - { - foreach (var endPoint in listenOptions) - { - var connectionHandler = new ConnectionHandler(endPoint, serviceContext, application); - var transport = _transportFactory.Create(endPoint, connectionHandler); - _transports.Add(transport); - - try - { - await transport.BindAsync().ConfigureAwait(false); - } - catch (AddressInUseException ex) - { - throw new IOException($"Failed to bind to address {endPoint}: address already in use.", ex); - } - - // If requested port was "0", replace with assigned dynamic port. - _serverAddresses.Addresses.Add(endPoint.GetDisplayName()); - } - } - // Graceful shutdown if possible public async Task StopAsync(CancellationToken cancellationToken) { @@ -282,74 +178,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core $"Maximum request buffer size ({Options.Limits.MaxRequestBufferSize.Value}) must be greater than or equal to maximum request headers size ({Options.Limits.MaxRequestHeadersTotalSize})."); } } - - private async Task StartLocalhostAsync(ServerAddress parsedAddress, ServiceContext serviceContext, IHttpApplication application, CancellationToken cancellationToken) - { - if (parsedAddress.Port == 0) - { - throw new InvalidOperationException("Dynamic port binding is not supported when binding to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both."); - } - - var exceptions = new List(); - - try - { - var ipv4ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, parsedAddress.Port)); - - var connectionHandler = new ConnectionHandler(ipv4ListenOptions, serviceContext, application); - var transport = _transportFactory.Create(ipv4ListenOptions, connectionHandler); - _transports.Add(transport); - await transport.BindAsync().ConfigureAwait(false); - } - catch (AddressInUseException ex) - { - throw new IOException($"Failed to bind to address {parsedAddress} on the IPv4 loopback interface: port already in use.", ex); - } - catch (Exception ex) - { - _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv4 loopback interface: ({ex.Message})"); - exceptions.Add(ex); - } - - try - { - var ipv6ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.IPv6Loopback, parsedAddress.Port)); - - var connectionHandler = new ConnectionHandler(ipv6ListenOptions, serviceContext, application); - var transport = _transportFactory.Create(ipv6ListenOptions, connectionHandler); - _transports.Add(transport); - await transport.BindAsync().ConfigureAwait(false); - } - catch (AddressInUseException ex) - { - throw new IOException($"Failed to bind to address {parsedAddress} on the IPv6 loopback interface: port already in use.", ex); - } - catch (Exception ex) - { - _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv6 loopback interface: ({ex.Message})"); - exceptions.Add(ex); - } - - if (exceptions.Count == 2) - { - throw new IOException($"Failed to bind to address {parsedAddress}.", new AggregateException(exceptions)); - } - } - - /// - /// Returns an for the given host an port. - /// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any. - /// - internal static IPEndPoint CreateIPEndPoint(ServerAddress address) - { - IPAddress ip; - - if (!IPAddress.TryParse(address.Host, out ip)) - { - ip = IPAddress.IPv6Any; - } - - return new IPEndPoint(ip, address.Port); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/AddressBinderTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/AddressBinderTests.cs new file mode 100644 index 0000000000..762e3191f1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/AddressBinderTests.cs @@ -0,0 +1,121 @@ +// 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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class AddressBinderTests + { + [Theory] + [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000)] + [InlineData("http://[::1]:5000", "::1", 5000)] + [InlineData("http://[::1]", "::1", 80)] + [InlineData("http://127.0.0.1", "127.0.0.1", 80)] + [InlineData("https://127.0.0.1", "127.0.0.1", 443)] + public void CorrectIPEndpointsAreCreated(string address, string expectedAddress, int expectedPort) + { + Assert.True(AddressBinder.TryCreateIPEndPoint( + ServerAddress.FromUrl(address), out var endpoint)); + Assert.NotNull(endpoint); + Assert.Equal(IPAddress.Parse(expectedAddress), endpoint.Address); + Assert.Equal(expectedPort, endpoint.Port); + } + + [Theory] + [InlineData("http://*")] + [InlineData("http://*:5000")] + [InlineData("http://+:80")] + [InlineData("http://+")] + [InlineData("http://randomhost:6000")] + [InlineData("http://randomhost")] + [InlineData("https://randomhost")] + public void DoesNotCreateIPEndPointOnInvalidIPAddress(string address) + { + Assert.False(AddressBinder.TryCreateIPEndPoint( + ServerAddress.FromUrl(address), out var endpoint)); + } + + [Theory] + [InlineData("*")] + [InlineData("randomhost")] + [InlineData("+")] + [InlineData("contoso.com")] + public async Task DefaultsToIPv6AnyOnInvalidIPAddress(string host) + { + var addresses = new ServerAddressesFeature(); + addresses.Addresses.Add($"http://{host}"); + var options = new List(); + + var tcs = new TaskCompletionSource(); + await AddressBinder.BindAsync(addresses, + options, + NullLogger.Instance, + endpoint => + { + tcs.TrySetResult(endpoint); + return Task.CompletedTask; + }); + var result = await tcs.Task; + Assert.Equal(IPAddress.IPv6Any, result.IPEndPoint.Address); + } + + [Fact] + public async Task WrapsAddressInUseExceptionAsIOException() + { + var addresses = new ServerAddressesFeature(); + addresses.Addresses.Add("http://localhost:5000"); + var options = new List(); + + await Assert.ThrowsAsync(() => + AddressBinder.BindAsync(addresses, + options, + NullLogger.Instance, + endpoint => throw new AddressInUseException("already in use"))); + } + + [Theory] + [InlineData("http://*:80")] + [InlineData("http://+:80")] + [InlineData("http://contoso.com:80")] + public async Task FallbackToIPv4WhenIPv6AnyBindFails(string address) + { + var addresses = new ServerAddressesFeature(); + addresses.Addresses.Add(address); + var options = new List(); + + var ipV6Attempt = false; + var ipV4Attempt = false; + + await AddressBinder.BindAsync(addresses, + options, + NullLogger.Instance, + endpoint => + { + if (endpoint.IPEndPoint.Address == IPAddress.IPv6Any) + { + ipV6Attempt = true; + throw new InvalidOperationException("EAFNOSUPPORT"); + } + + if (endpoint.IPEndPoint.Address == IPAddress.Any) + { + ipV4Attempt = true; + } + + return Task.CompletedTask; + }); + + Assert.True(ipV4Attempt, "Should have attempted to bind to IPAddress.Any"); + Assert.True(ipV6Attempt, "Should have attempted to bind to IPAddress.IPv6Any"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/CreateIPEndpointTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/CreateIPEndpointTests.cs deleted file mode 100644 index bf120febd4..0000000000 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/CreateIPEndpointTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.Net; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class CreateIPEndpointTests - { - [Theory] - [InlineData("10.10.10.10", "10.10.10.10")] - [InlineData("[::1]", "::1")] - [InlineData("randomhost", "::")] // "::" is IPAddress.IPv6Any - [InlineData("*", "::")] // "::" is IPAddress.IPv6Any - public void CorrectIPEndpointsAreCreated(string host, string expectedAddress) - { - var endpoint = KestrelServer.CreateIPEndPoint(ServerAddress.FromUrl($"http://{host}:5000/")); - Assert.NotNull(endpoint); - Assert.Equal(IPAddress.Parse(expectedAddress), endpoint.Address); - Assert.Equal(5000, endpoint.Port); - } - } -} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs index f7a24e1131..f89a055115 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { 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.", + $"Failed to bind to address http://{(addressFamily == AddressFamily.InterNetwork ? "127.0.0.1" : "[::1]")}:{port}: address already in use.", exception.Message); } }