// 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)) { context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(parsedAddress.Port)); // 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); } } } } }