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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
@ -111,50 +107,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
ServerOptions = Options
|
ServerOptions = Options
|
||||||
};
|
};
|
||||||
|
|
||||||
var listenOptions = Options.ListenOptions;
|
async Task OnBind(ListenOptions endpoint)
|
||||||
var hasListenOptions = listenOptions.Any();
|
|
||||||
var hasServerAddresses = _serverAddresses.Addresses.Any();
|
|
||||||
|
|
||||||
if (_serverAddresses.PreferHostingUrls && hasServerAddresses)
|
|
||||||
{
|
{
|
||||||
if (hasListenOptions)
|
var connectionHandler = new ConnectionHandler<TContext>(endpoint, serviceContext, application);
|
||||||
{
|
var transport = _transportFactory.Create(endpoint, connectionHandler);
|
||||||
var joined = string.Join(", ", _serverAddresses.Addresses);
|
_transports.Add(transport);
|
||||||
_logger.LogInformation($"Overriding endpoints defined in UseKestrel() since {nameof(IServerAddressesFeature.PreferHostingUrls)} is set to true. Binding to address(es) '{joined}' instead.");
|
|
||||||
|
|
||||||
listenOptions.Clear();
|
await transport.BindAsync().ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
await BindToServerAddresses(listenOptions, serviceContext, application, cancellationToken).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 AddressBinder.BindAsync(_serverAddresses, Options.ListenOptions, _logger, OnBind).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
// Graceful shutdown if possible
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
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}).");
|
$"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());
|
var exception = Assert.Throws<IOException>(() => host.Start());
|
||||||
Assert.Equal(
|
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);
|
exception.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue