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)
This commit is contained in:
parent
42d82a507d
commit
7a3a731686
|
|
@ -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> listenOptions,
|
||||
ILogger logger,
|
||||
Func<ListenOptions, Task> 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<string> Addresses { get; set; }
|
||||
public List<ListenOptions> ListenOptions { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public Func<ListenOptions, Task> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
|
||||
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
|
||||
/// </summary>
|
||||
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<Exception>();
|
||||
|
||||
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<string> 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<ListenOptions> 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<ListenOptions> _endpoints;
|
||||
|
||||
public EndpointsStrategy(IReadOnlyCollection<ListenOptions> 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<string> _addresses;
|
||||
|
||||
public AddressesStrategy(IReadOnlyCollection<string> addresses)
|
||||
{
|
||||
_addresses = addresses;
|
||||
}
|
||||
|
||||
public virtual async Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
foreach (var address in _addresses)
|
||||
{
|
||||
await BindAddressAsync(address, context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TContext>(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<TContext>(List<ListenOptions> listenOptions, ServiceContext serviceContext, IHttpApplication<TContext> 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<TContext>(List<ListenOptions> listenOptions, ServiceContext serviceContext, IHttpApplication<TContext> application)
|
||||
{
|
||||
foreach (var endPoint in listenOptions)
|
||||
{
|
||||
var connectionHandler = new ConnectionHandler<TContext>(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<TContext>(ServerAddress parsedAddress, ServiceContext serviceContext, IHttpApplication<TContext> 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<Exception>();
|
||||
|
||||
try
|
||||
{
|
||||
var ipv4ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, parsedAddress.Port));
|
||||
|
||||
var connectionHandler = new ConnectionHandler<TContext>(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<TContext>(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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
|
||||
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
|
||||
/// </summary>
|
||||
internal static IPEndPoint CreateIPEndPoint(ServerAddress address)
|
||||
{
|
||||
IPAddress ip;
|
||||
|
||||
if (!IPAddress.TryParse(address.Host, out ip))
|
||||
{
|
||||
ip = IPAddress.IPv6Any;
|
||||
}
|
||||
|
||||
return new IPEndPoint(ip, address.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ListenOptions>();
|
||||
|
||||
var tcs = new TaskCompletionSource<ListenOptions>();
|
||||
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<ListenOptions>();
|
||||
|
||||
await Assert.ThrowsAsync<IOException>(() =>
|
||||
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<ListenOptions>();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
{
|
||||
var exception = Assert.Throws<IOException>(() => 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue