diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index fd5f9367d1..5773d78aa9 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -36,6 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2AC ProjectSection(SolutionItems) = preProject test\shared\DummyApplication.cs = test\shared\DummyApplication.cs test\shared\HttpClientSlim.cs = test\shared\HttpClientSlim.cs + test\shared\KestrelTestLoggerFactory.cs = test\shared\KestrelTestLoggerFactory.cs test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs test\shared\MockConnection.cs = test\shared\MockConnection.cs test\shared\MockFrameControl.cs = test\shared\MockFrameControl.cs diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs index 1ba9d55393..c2256120bc 100644 --- a/samples/SampleApp/Startup.cs +++ b/samples/SampleApp/Startup.cs @@ -34,33 +34,34 @@ namespace SampleApp public static void Main(string[] args) { - var hostBuilder = new WebHostBuilder().UseKestrel(options => - { - options.Listen(IPAddress.Loopback, 5000, listenOptions => + var host = new WebHostBuilder() + .UseKestrel(options => { - // Uncomment the following to enable Nagle's algorithm for this endpoint. - //listenOptions.NoDelay = false; + options.Listen(IPAddress.Loopback, 5000, listenOptions => + { + // Uncomment the following to enable Nagle's algorithm for this endpoint. + //listenOptions.NoDelay = false; - listenOptions.UseConnectionLogging(); - }); - options.Listen(IPAddress.Loopback, 5001, listenOptions => - { - listenOptions.UseHttps("testCert.pfx", "testPassword"); - listenOptions.UseConnectionLogging(); - }); + listenOptions.UseConnectionLogging(); + }); + options.Listen(IPAddress.Loopback, 5001, listenOptions => + { + listenOptions.UseHttps("testCert.pfx", "testPassword"); + listenOptions.UseConnectionLogging(); + }); - options.UseSystemd(); + options.UseSystemd(); - // The following section should be used to demo sockets - //options.ListenUnixSocket("/tmp/kestrel-test.sock"); + // The following section should be used to demo sockets + //options.ListenUnixSocket("/tmp/kestrel-test.sock"); - // Uncomment the following line to change the default number of libuv threads for all endpoints. - //options.ThreadCount = 4; - }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup(); + // Uncomment the following line to change the default number of libuv threads for all endpoints. + //options.ThreadCount = 4; + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .Build(); - var host = hostBuilder.Build(); host.Run(); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs index 46d0bdac5a..e0f592154b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs @@ -1,12 +1,12 @@ // 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 System.Runtime.InteropServices; -using System.Text; namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { - internal class Constants + internal static class Constants { public const int ListenBacklog = 128; @@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure public static readonly int? ECONNRESET = GetECONNRESET(); public static readonly int? EADDRINUSE = GetEADDRINUSE(); + /// + /// The IPEndPoint Kestrel will bind to if nothing else is specified. + /// + public static readonly IPEndPoint DefaultIPEndPoint = new IPEndPoint(IPAddress.Loopback, 5000); + /// /// Prefix of host name used to specify Unix sockets in the configuration. /// diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs index a8a9c5af66..24803a963a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs @@ -112,22 +112,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel } engine.Start(threadCount); - var atLeastOneListener = false; var listenOptions = Options.ListenOptions; + var hasListenOptions = listenOptions.Any(); + var hasServerAddresses = _serverAddresses.Addresses.Any(); - if (listenOptions.Any()) + if (hasListenOptions && hasServerAddresses) { - var addresses = _serverAddresses.Addresses; - if (addresses.SingleOrDefault() != "http://localhost:5000") - { - var joined = string.Join(", ", addresses); - throw new NotSupportedException($"Specifying address(es) '{joined}' is incompatible with also configuring endpoint(s) in UseKestrel."); - } + var joined = string.Join(", ", _serverAddresses.Addresses); + _logger.LogWarning($"Overriding address(es) '{joined}'. Binding to endpoints defined in {nameof(WebHostBuilderKestrelExtensions.UseKestrel)}() instead."); _serverAddresses.Addresses.Clear(); } - else + else if (!hasListenOptions && !hasServerAddresses) + { + _logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultIPEndPoint} by default."); + listenOptions.Add(new ListenOptions(Constants.DefaultIPEndPoint)); + } + else if (!hasListenOptions) { // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature. var copiedAddresses = _serverAddresses.Addresses.ToArray(); @@ -155,7 +157,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel // If StartLocalhost doesn't throw, there is at least one listener. // The port cannot change for "localhost". _serverAddresses.Addresses.Add(parsedAddress.ToString()); - atLeastOneListener = true; } else { @@ -172,8 +173,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel foreach (var endPoint in listenOptions) { - atLeastOneListener = true; - try { _disposables.Push(engine.CreateServer(endPoint)); @@ -191,11 +190,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel // If requested port was "0", replace with assigned dynamic port. _serverAddresses.Addresses.Add(endPoint.ToString()); } - - if (!atLeastOneListener) - { - throw new InvalidOperationException("No recognized listening addresses were configured."); - } } catch (Exception ex) { diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs index 58044e3692..0357f6ab4f 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs @@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; @@ -126,6 +128,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + [ConditionalFact(Skip = "Waiting on https://github.com/aspnet/Hosting/issues/917")] + [PortSupportedCondition(5000)] + public async Task DefaultsToPort5000() + { + var testLogger = new TestApplicationErrorLogger(); + + var hostBuilder = new WebHostBuilder() + .UseKestrel() + .ConfigureServices(services => + { + services.AddSingleton(new KestrelTestLoggerFactory(testLogger)); + }) + .Configure(ConfigureEchoAddress); + + using (var host = hostBuilder.Build()) + { + host.Start(); + + var debugLog = testLogger.Messages.Single(log => log.LogLevel == LogLevel.Debug); + Assert.True(debugLog.Message.Contains("default")); + + foreach (var testUrl in new[] { "http://127.0.0.1:5000", "http://localhost:5000" }) + { + var response = await HttpClientSlim.GetStringAsync(testUrl); + Assert.Equal(new Uri(testUrl).ToString(), response); + } + } + } + [Fact] public void ThrowsWhenBindingToIPv4AddressInUse() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerTests.cs index 773d5180e9..bb82eac55f 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerTests.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; +using System.Net; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Server.Kestrel; @@ -43,16 +45,26 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(1, testLogger.CriticalErrorsLogged); } - [Fact] - public void StartWithEmptyAddressesThrows() + [Theory] + [InlineData("http://localhost:5000")] + [InlineData("The value of the string shouldn't matter.")] + [InlineData(null)] + public void StartWarnsWhenIgnoringIServerAddressesFeature(string ignoredAddress) { var testLogger = new TestApplicationErrorLogger(); - var server = CreateServer(new KestrelServerOptions(), testLogger); + var kestrelOptions = new KestrelServerOptions(); - var exception = Assert.Throws(() => StartDummyApplication(server)); + // Directly configuring an endpoint using Listen causes the IServerAddressesFeature to be ignored. + kestrelOptions.Listen(IPAddress.Loopback, 0); - Assert.Equal("No recognized listening addresses were configured.", exception.Message); - Assert.Equal(1, testLogger.CriticalErrorsLogged); + using (var server = CreateServer(kestrelOptions, testLogger)) + { + server.Features.Get().Addresses.Add(ignoredAddress); + StartDummyApplication(server); + + var warning = testLogger.Messages.Single(log => log.LogLevel == LogLevel.Warning); + Assert.True(warning.Message.Contains("Overriding")); + } } [Theory] @@ -87,37 +99,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var lifetime = new LifetimeNotImplemented(); - return new KestrelServer(Options.Create(options), lifetime, new TestLoggerFactory(testLogger)); + return new KestrelServer(Options.Create(options), lifetime, new KestrelTestLoggerFactory(testLogger)); } private static void StartDummyApplication(IServer server) { server.Start(new DummyApplication(context => TaskCache.CompletedTask)); } - - private class TestLoggerFactory : ILoggerFactory - { - private readonly ILogger _testLogger; - - public TestLoggerFactory(ILogger testLogger) - { - _testLogger = testLogger; - } - - public ILogger CreateLogger(string categoryName) - { - return _testLogger; - } - - public void AddProvider(ILoggerProvider provider) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - throw new NotImplementedException(); - } - } } } diff --git a/test/shared/KestrelTestLoggerFactory.cs b/test/shared/KestrelTestLoggerFactory.cs new file mode 100644 index 0000000000..e8dbbd3408 --- /dev/null +++ b/test/shared/KestrelTestLoggerFactory.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Testing +{ + public class KestrelTestLoggerFactory : ILoggerFactory + { + private readonly ILogger _testLogger; + + public KestrelTestLoggerFactory(ILogger testLogger) + { + _testLogger = testLogger; + } + + public ILogger CreateLogger(string categoryName) + { + return _testLogger; + } + + public void AddProvider(ILoggerProvider provider) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/shared/TestApplicationErrorLogger.cs b/test/shared/TestApplicationErrorLogger.cs index 80c12a23bd..057e8435fb 100644 --- a/test/shared/TestApplicationErrorLogger.cs +++ b/test/shared/TestApplicationErrorLogger.cs @@ -38,7 +38,13 @@ namespace Microsoft.AspNetCore.Testing Console.WriteLine($"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception?.Message}"); #endif - Messages.Add(new LogMessage { LogLevel = logLevel, EventId = eventId, Exception = exception }); + Messages.Add(new LogMessage + { + LogLevel = logLevel, + EventId = eventId, + Exception = exception, + Message = formatter(state, exception) + }); } public class LogMessage @@ -46,6 +52,7 @@ namespace Microsoft.AspNetCore.Testing public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } public Exception Exception { get; set; } + public string Message { get; set; } } } }