diff --git a/.travis.yml b/.travis.yml index a0be886892..03c4d59416 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: csharp sudo: required dist: trusty +services: + - docker addons: apt: packages: @@ -30,3 +32,4 @@ before_install: - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi script: - ./build.sh --quiet verify + - if test "$TRAVIS_OS_NAME" != "osx"; then bash test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh; fi diff --git a/samples/LargeResponseApp/Startup.cs b/samples/LargeResponseApp/Startup.cs index bc02662832..47ecd8a08b 100644 --- a/samples/LargeResponseApp/Startup.cs +++ b/samples/LargeResponseApp/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -40,8 +41,10 @@ namespace LargeResponseApp public static void Main(string[] args) { var host = new WebHostBuilder() - .UseKestrel() - .UseUrls("http://localhost:5001/") + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 5001); + }) .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup() .Build(); diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs index 84bb918a99..1ba9d55393 100644 --- a/samples/SampleApp/Startup.cs +++ b/samples/SampleApp/Startup.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -33,24 +34,33 @@ namespace SampleApp public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseKestrel(options => + var hostBuilder = new WebHostBuilder().UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 5000, listenOptions => { - // options.ThreadCount = 4; - options.NoDelay = true; - options.UseHttps("testCert.pfx", "testPassword"); - options.UseConnectionLogging(); - }) - .UseUrls("http://localhost:5000", "https://localhost:5001") - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() - .Build(); + // Uncomment the following to enable Nagle's algorithm for this endpoint. + //listenOptions.NoDelay = false; - // The following section should be used to demo sockets - //var addresses = application.GetAddresses(); - //addresses.Clear(); - //addresses.Add("http://unix:/tmp/kestrel-test.sock"); + listenOptions.UseConnectionLogging(); + }); + options.Listen(IPAddress.Loopback, 5001, listenOptions => + { + listenOptions.UseHttps("testCert.pfx", "testPassword"); + listenOptions.UseConnectionLogging(); + }); + options.UseSystemd(); + + // 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(); + + var host = hostBuilder.Build(); host.Run(); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapter.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapter.cs new file mode 100644 index 0000000000..d56d440512 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapter.cs @@ -0,0 +1,160 @@ +// 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.IO; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Https +{ + public class HttpsConnectionAdapter : IConnectionAdapter + { + private static readonly ClosedAdaptedConnection _closedAdaptedConnection = new ClosedAdaptedConnection(); + + private readonly HttpsConnectionAdapterOptions _options; + private readonly ILogger _logger; + + public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options) + : this(options, loggerFactory: null) + { + } + + public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options, ILoggerFactory loggerFactory) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (options.ServerCertificate == null) + { + throw new ArgumentException("The server certificate parameter is required."); + } + + _options = options; + _logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionAdapter)); + } + + public async Task OnConnectionAsync(ConnectionAdapterContext context) + { + SslStream sslStream; + bool certificateRequired; + + if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) + { + sslStream = new SslStream(context.ConnectionStream); + certificateRequired = false; + } + else + { + sslStream = new SslStream(context.ConnectionStream, leaveInnerStreamOpen: false, + userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => + { + if (certificate == null) + { + return _options.ClientCertificateMode != ClientCertificateMode.RequireCertificate; + } + + if (_options.ClientCertificateValidation == null) + { + if (sslPolicyErrors != SslPolicyErrors.None) + { + return false; + } + } + + var certificate2 = ConvertToX509Certificate2(certificate); + if (certificate2 == null) + { + return false; + } + + if (_options.ClientCertificateValidation != null) + { + if (!_options.ClientCertificateValidation(certificate2, chain, sslPolicyErrors)) + { + return false; + } + } + + return true; + }); + + certificateRequired = true; + } + + try + { + await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, certificateRequired, + _options.SslProtocols, _options.CheckCertificateRevocation); + } + catch (IOException ex) + { + _logger?.LogInformation(1, ex, "Failed to authenticate HTTPS connection."); + sslStream.Dispose(); + return _closedAdaptedConnection; + } + + return new HttpsAdaptedConnection(sslStream); + } + + private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certificate) + { + if (certificate == null) + { + return null; + } + + X509Certificate2 certificate2 = certificate as X509Certificate2; + if (certificate2 != null) + { + return certificate2; + } + +#if NETSTANDARD1_3 + // conversion X509Certificate to X509Certificate2 not supported + // https://github.com/dotnet/corefx/issues/4510 + return null; +#else + return new X509Certificate2(certificate); +#endif + } + + private class HttpsAdaptedConnection : IAdaptedConnection + { + private readonly SslStream _sslStream; + + public HttpsAdaptedConnection(SslStream sslStream) + { + _sslStream = sslStream; + } + + public Stream ConnectionStream => _sslStream; + + public void PrepareRequest(IFeatureCollection requestFeatures) + { + var clientCertificate = ConvertToX509Certificate2(_sslStream.RemoteCertificate); + if (clientCertificate != null) + { + requestFeatures.Set(new TlsConnectionFeature { ClientCertificate = clientCertificate }); + } + + requestFeatures.Get().Scheme = "https"; + } + } + + private class ClosedAdaptedConnection : IAdaptedConnection + { + public Stream ConnectionStream { get; } = new ClosedStream(); + + public void PrepareRequest(IFeatureCollection requestFeatures) + { + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilterOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapterOptions.cs similarity index 90% rename from src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilterOptions.cs rename to src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapterOptions.cs index 93eab7158d..728c842c65 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilterOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionAdapterOptions.cs @@ -8,9 +8,9 @@ using System.Security.Cryptography.X509Certificates; namespace Microsoft.AspNetCore.Server.Kestrel.Https { - public class HttpsConnectionFilterOptions + public class HttpsConnectionAdapterOptions { - public HttpsConnectionFilterOptions() + public HttpsConnectionAdapterOptions() { ClientCertificateMode = ClientCertificateMode.NoCertificate; SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilter.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilter.cs deleted file mode 100644 index 30a34cfb91..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Https/HttpsConnectionFilter.cs +++ /dev/null @@ -1,156 +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; -using System.IO; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Filter; -using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Https -{ - public class HttpsConnectionFilter : IConnectionFilter - { - private static readonly ClosedStream _closedStream = new ClosedStream(); - - private readonly HttpsConnectionFilterOptions _options; - private readonly IConnectionFilter _previous; - private readonly ILogger _logger; - - public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous) - : this(options, previous, loggerFactory: null) - { - } - - public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous, ILoggerFactory loggerFactory) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - if (previous == null) - { - throw new ArgumentNullException(nameof(previous)); - } - if (options.ServerCertificate == null) - { - throw new ArgumentException("The server certificate parameter is required."); - } - - _options = options; - _previous = previous; - _logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionFilter)); - } - - public async Task OnConnectionAsync(ConnectionFilterContext context) - { - await _previous.OnConnectionAsync(context); - - if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase)) - { - SslStream sslStream; - bool certificateRequired; - - if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) - { - sslStream = new SslStream(context.Connection); - certificateRequired = false; - } - else - { - sslStream = new SslStream(context.Connection, leaveInnerStreamOpen: false, - userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => - { - if (certificate == null) - { - return _options.ClientCertificateMode != ClientCertificateMode.RequireCertificate; - } - - if (_options.ClientCertificateValidation == null) - { - if (sslPolicyErrors != SslPolicyErrors.None) - { - return false; - } - } - - var certificate2 = ConvertToX509Certificate2(certificate); - if (certificate2 == null) - { - return false; - } - - if (_options.ClientCertificateValidation != null) - { - if (!_options.ClientCertificateValidation(certificate2, chain, sslPolicyErrors)) - { - return false; - } - } - - return true; - }); - - certificateRequired = true; - } - - try - { - await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, certificateRequired, - _options.SslProtocols, _options.CheckCertificateRevocation); - } - catch (IOException ex) - { - _logger?.LogInformation(1, ex, "Failed to authenticate HTTPS connection."); - - sslStream.Dispose(); - context.Connection = _closedStream; - - return; - } - - var previousPrepareRequest = context.PrepareRequest; - context.PrepareRequest = features => - { - previousPrepareRequest?.Invoke(features); - - var clientCertificate = ConvertToX509Certificate2(sslStream.RemoteCertificate); - if (clientCertificate != null) - { - features.Set(new TlsConnectionFeature { ClientCertificate = clientCertificate }); - } - - features.Get().Scheme = "https"; - }; - - context.Connection = sslStream; - } - } - - private X509Certificate2 ConvertToX509Certificate2(X509Certificate certificate) - { - if (certificate == null) - { - return null; - } - - X509Certificate2 certificate2 = certificate as X509Certificate2; - if (certificate2 != null) - { - return certificate2; - } - -#if NETSTANDARD1_3 - // conversion X509Certificate to X509Certificate2 not supported - // https://github.com/dotnet/corefx/issues/4510 - return null; -#else - return new X509Certificate2(certificate); -#endif - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/KestrelServerOptionsHttpsExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Https/KestrelServerOptionsHttpsExtensions.cs deleted file mode 100644 index d4036985f5..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Https/KestrelServerOptionsHttpsExtensions.cs +++ /dev/null @@ -1,92 +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.IO; -using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Server.Kestrel; -using Microsoft.AspNetCore.Server.Kestrel.Filter; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Hosting -{ - public static class KestrelServerOptionsHttpsExtensions - { - /// - /// Configure Kestrel to use HTTPS. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure. - /// - /// - /// The name of a certificate file, relative to the directory that contains the application content files. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseHttps(this KestrelServerOptions options, string fileName) - { - var env = options.ApplicationServices.GetRequiredService(); - return options.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName))); - } - - /// - /// Configure Kestrel to use HTTPS. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure. - /// - /// - /// The name of a certificate file, relative to the directory that contains the application content files. - /// - /// - /// The password required to access the X.509 certificate data. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseHttps(this KestrelServerOptions options, string fileName, string password) - { - var env = options.ApplicationServices.GetRequiredService(); - return options.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password)); - } - - /// - /// Configure Kestrel to use HTTPS. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure. - /// - /// - /// The X.509 certificate. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseHttps(this KestrelServerOptions options, X509Certificate2 serverCertificate) - { - return options.UseHttps(new HttpsConnectionFilterOptions { ServerCertificate = serverCertificate }); - } - - /// - /// Configure Kestrel to use HTTPS. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure. - /// - /// - /// Options to configure HTTPS. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseHttps(this KestrelServerOptions options, HttpsConnectionFilterOptions httpsOptions) - { - var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter(); - var loggerFactory = options.ApplicationServices.GetRequiredService(); - options.ConnectionFilter = new HttpsConnectionFilter(httpsOptions, prevFilter, loggerFactory); - return options; - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/ListenOptionsHttpsExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Https/ListenOptionsHttpsExtensions.cs new file mode 100644 index 0000000000..148da6deb1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Https/ListenOptionsHttpsExtensions.cs @@ -0,0 +1,94 @@ +// 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.IO; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Server.Kestrel; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting +{ + /// + /// Extension methods fro that configure Kestrel to use HTTPS for a given endpoint. + /// + public static class ListenOptionsHttpsExtensions + { + /// + /// Configure Kestrel to use HTTPS. + /// + /// + /// The to configure. + /// + /// + /// The name of a certificate file, relative to the directory that contains the application content files. + /// + /// + /// The . + /// + public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName) + { + var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName))); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// + /// The to configure. + /// + /// + /// The name of a certificate file, relative to the directory that contains the application content files. + /// + /// + /// The password required to access the X.509 certificate data. + /// + /// + /// The . + /// + public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password) + { + var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password)); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// + /// The to configure. + /// + /// + /// The X.509 certificate. + /// + /// + /// The . + /// + public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate) + { + return listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = serverCertificate }); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// + /// The to configure. + /// + /// + /// Options to configure HTTPS. + /// + /// + /// The . + /// + public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions) + { + var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + listenOptions.ConnectionAdapters.Add(new HttpsConnectionAdapter(httpsOptions, loggerFactory)); + return listenOptions; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.net45.json b/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.net45.json new file mode 100644 index 0000000000..c063085fff --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.net45.json @@ -0,0 +1,14 @@ +[ + { + "OldTypeId": "public static class Microsoft.AspNetCore.Hosting.KestrelServerOptionsHttpsExtensions", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionFilterOptions", + "Kind": "Removal" + } +] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.netcore.json b/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.netcore.json new file mode 100644 index 0000000000..c063085fff --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Https/exceptions.netcore.json @@ -0,0 +1,14 @@ +[ + { + "OldTypeId": "public static class Microsoft.AspNetCore.Hosting.KestrelServerOptionsHttpsExtensions", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionFilterOptions", + "Kind": "Removal" + } +] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ConnectionAdapterContext.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ConnectionAdapterContext.cs new file mode 100644 index 0000000000..15a87c9075 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ConnectionAdapterContext.cs @@ -0,0 +1,19 @@ +// 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.IO; + +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter +{ + // Even though this only includes the non-adapted ConnectionStream currently, this is a context in case + // we want to add more connection metadata later. + public class ConnectionAdapterContext + { + internal ConnectionAdapterContext(Stream connectionStream) + { + ConnectionStream = connectionStream; + } + + public Stream ConnectionStream { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IAdaptedConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IAdaptedConnection.cs new file mode 100644 index 0000000000..aa8c2e820e --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IAdaptedConnection.cs @@ -0,0 +1,15 @@ +// 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.IO; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter +{ + public interface IAdaptedConnection + { + Stream ConnectionStream { get; } + + void PrepareRequest(IFeatureCollection requestFeatures); + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IConnectionAdapter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IConnectionAdapter.cs new file mode 100644 index 0000000000..cbdb12b3b3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/IConnectionAdapter.cs @@ -0,0 +1,13 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter +{ + public interface IConnectionAdapter + { + Task OnConnectionAsync(ConnectionAdapterContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/AdaptedPipeline.cs similarity index 91% rename from src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs rename to src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/AdaptedPipeline.cs index 4f4a0f9405..28c8fd66e1 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/AdaptedPipeline.cs @@ -7,13 +7,13 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal { - public class FilteredStreamAdapter : IDisposable + public class AdaptedPipeline : IDisposable { private readonly Stream _filteredStream; - public FilteredStreamAdapter( + public AdaptedPipeline( string connectionId, Stream filteredStream, MemoryPool memory, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LoggingStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/LoggingStream.cs similarity index 98% rename from src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LoggingStream.cs rename to src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/LoggingStream.cs index 14890c0e3a..184839163c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LoggingStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/LoggingStream.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal { internal class LoggingStream : Stream { diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LibuvStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/RawStream.cs similarity index 97% rename from src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LibuvStream.cs rename to src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/RawStream.cs index b8bc766f94..2c7fb8accf 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/LibuvStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/RawStream.cs @@ -8,16 +8,16 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal { - public class LibuvStream : Stream + public class RawStream : Stream { private readonly SocketInput _input; private readonly ISocketOutput _output; private Task _cachedTask = TaskCache.DefaultCompletedTask; - public LibuvStream(SocketInput input, ISocketOutput output) + public RawStream(SocketInput input, ISocketOutput output) { _input = input; _output = output; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/StreamSocketOutput.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/StreamSocketOutput.cs similarity index 98% rename from src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/StreamSocketOutput.cs rename to src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/StreamSocketOutput.cs index bce699f693..e2cce039aa 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/StreamSocketOutput.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/Internal/StreamSocketOutput.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal { public class StreamSocketOutput : ISocketOutput { diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ListenOptionsConnectionLoggingExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ListenOptionsConnectionLoggingExtensions.cs new file mode 100644 index 0000000000..9d5affe345 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/ListenOptionsConnectionLoggingExtensions.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.AspNetCore.Server.Kestrel; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class ListenOptionsConnectionLoggingExtensions + { + /// + /// Emits verbose logs for bytes read from and written to the connection. + /// + /// + /// The . + /// + public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions) + { + return listenOptions.UseConnectionLogging(nameof(LoggingConnectionAdapter)); + } + + /// + /// Emits verbose logs for bytes read from and written to the connection. + /// + /// + /// The . + /// + public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions, string loggerName) + { + var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionAdapter)); + listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(logger)); + return listenOptions; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/LoggingConnectionAdapter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/LoggingConnectionAdapter.cs new file mode 100644 index 0000000000..6c25f4ba2f --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Adapter/LoggingConnectionAdapter.cs @@ -0,0 +1,47 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Adapter +{ + public class LoggingConnectionAdapter : IConnectionAdapter + { + private readonly ILogger _logger; + + public LoggingConnectionAdapter(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + public Task OnConnectionAsync(ConnectionAdapterContext context) + { + return Task.FromResult( + new LoggingAdaptedConnection(context.ConnectionStream, _logger)); + } + + private class LoggingAdaptedConnection : IAdaptedConnection + { + public LoggingAdaptedConnection(Stream rawStream, ILogger logger) + { + ConnectionStream = new LoggingStream(rawStream, logger); + } + + public Stream ConnectionStream { get; } + + public void PrepareRequest(IFeatureCollection requestFeatures) + { + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/ConnectionFilterContext.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/ConnectionFilterContext.cs deleted file mode 100644 index 386753342f..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/ConnectionFilterContext.cs +++ /dev/null @@ -1,16 +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; -using System.IO; -using Microsoft.AspNetCore.Http.Features; - -namespace Microsoft.AspNetCore.Server.Kestrel.Filter -{ - public class ConnectionFilterContext - { - public ServerAddress Address { get; set; } - public Stream Connection { get; set; } - public Action PrepareRequest { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/IConnectionFilter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/IConnectionFilter.cs deleted file mode 100644 index 0447f8b07a..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/IConnectionFilter.cs +++ /dev/null @@ -1,12 +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.Threading.Tasks; - -namespace Microsoft.AspNetCore.Server.Kestrel.Filter -{ - public interface IConnectionFilter - { - Task OnConnectionAsync(ConnectionFilterContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/KestrelServerOptionsConnectionLoggingExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/KestrelServerOptionsConnectionLoggingExtensions.cs deleted file mode 100644 index a46d0646eb..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/KestrelServerOptionsConnectionLoggingExtensions.cs +++ /dev/null @@ -1,39 +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 Microsoft.AspNetCore.Server.Kestrel; -using Microsoft.AspNetCore.Server.Kestrel.Filter; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Hosting -{ - public static class KestrelServerOptionsConnectionLoggingExtensions - { - /// - /// Emits verbose logs for bytes read from and written to the connection. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseConnectionLogging(this KestrelServerOptions options) - { - return options.UseConnectionLogging(nameof(LoggingConnectionFilter)); - } - - /// - /// Emits verbose logs for bytes read from and written to the connection. - /// - /// - /// The Microsoft.AspNetCore.Server.KestrelServerOptions. - /// - public static KestrelServerOptions UseConnectionLogging(this KestrelServerOptions options, string loggerName) - { - var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter(); - var loggerFactory = options.ApplicationServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionFilter)); - options.ConnectionFilter = new LoggingConnectionFilter(logger, prevFilter); - return options; - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/LoggingConnectionFilter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/LoggingConnectionFilter.cs deleted file mode 100644 index fd77be1295..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/LoggingConnectionFilter.cs +++ /dev/null @@ -1,38 +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; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Filter.Internal; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Filter -{ - public class LoggingConnectionFilter : IConnectionFilter - { - private readonly ILogger _logger; - private readonly IConnectionFilter _previous; - - public LoggingConnectionFilter(ILogger logger, IConnectionFilter previous) - { - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - if (previous == null) - { - throw new ArgumentNullException(nameof(previous)); - } - - _logger = logger; - _previous = previous; - } - - public async Task OnConnectionAsync(ConnectionFilterContext context) - { - await _previous.OnConnectionAsync(context); - - context.Connection = new LoggingStream(context.Connection, _logger); - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/NoOpConnectionFilter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/NoOpConnectionFilter.cs deleted file mode 100644 index a41bd9313e..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/NoOpConnectionFilter.cs +++ /dev/null @@ -1,16 +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.Threading.Tasks; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Server.Kestrel.Filter -{ - public class NoOpConnectionFilter : IConnectionFilter - { - public Task OnConnectionAsync(ConnectionFilterContext context) - { - return TaskCache.CompletedTask; - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 2b2366ee4e..9d1c537814 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -2,12 +2,13 @@ // 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.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Filter; -using Microsoft.AspNetCore.Server.Kestrel.Filter.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; +using Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; using Microsoft.Extensions.Internal; @@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private static readonly Action _readCallback = (handle, status, state) => ReadCallback(handle, status, state); + private static readonly Func _allocCallback = (handle, suggestedsize, state) => AllocCallback(handle, suggestedsize, state); @@ -32,9 +34,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private readonly UvStreamHandle _socket; private readonly Frame _frame; - private ConnectionFilterContext _filterContext; - private LibuvStream _libuvStream; - private FilteredStreamAdapter _filteredStreamAdapter; + private readonly List _connectionAdapters; + private AdaptedPipeline _adaptedPipeline; + private Stream _filteredStream; private Task _readInputTask; private TaskCompletionSource _socketClosedTcs = new TaskCompletionSource(); @@ -47,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public Connection(ListenerContext context, UvStreamHandle socket) : base(context) { _socket = socket; + _connectionAdapters = context.ListenOptions.ConnectionAdapters; socket.Connection = this; ConnectionControl = this; @@ -80,57 +83,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private Func FrameFactory => ListenerContext.ServiceContext.FrameFactory; private IKestrelTrace Log => ListenerContext.ServiceContext.Log; private IThreadPool ThreadPool => ListenerContext.ServiceContext.ThreadPool; - private ServerAddress ServerAddress => ListenerContext.ServerAddress; private KestrelThread Thread => ListenerContext.Thread; public void Start() { Log.ConnectionStart(ConnectionId); - // Start socket prior to applying the ConnectionFilter + // Start socket prior to applying the ConnectionAdapter _socket.ReadStart(_allocCallback, _readCallback, this); - if (ServerOptions.ConnectionFilter == null) + if (_connectionAdapters.Count == 0) { _frame.Start(); } else { - _libuvStream = new LibuvStream(Input, Output); - - _filterContext = new ConnectionFilterContext - { - Connection = _libuvStream, - Address = ServerAddress - }; - - try - { - ServerOptions.ConnectionFilter.OnConnectionAsync(_filterContext).ContinueWith((task, state) => - { - var connection = (Connection)state; - - if (task.IsFaulted) - { - connection.Log.LogError(0, task.Exception, "ConnectionFilter.OnConnection"); - connection.ConnectionControl.End(ProduceEndType.SocketDisconnect); - } - else if (task.IsCanceled) - { - connection.Log.LogError("ConnectionFilter.OnConnection Canceled"); - connection.ConnectionControl.End(ProduceEndType.SocketDisconnect); - } - else - { - connection.ApplyConnectionFilter(); - } - }, this); - } - catch (Exception ex) - { - Log.LogError(0, ex, "ConnectionFilter.OnConnection"); - ConnectionControl.End(ProduceEndType.SocketDisconnect); - } + // ApplyConnectionAdaptersAsync should never throw. If it succeeds, it will call _frame.Start(). + // Otherwise, it will close the connection. + var ignore = ApplyConnectionAdaptersAsync(); } } @@ -161,13 +131,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { var connection = (Connection)state; - if (_filteredStreamAdapter != null) + if (connection._adaptedPipeline != null) { - Task.WhenAll(_readInputTask, _frame.StopAsync()).ContinueWith((task2, state2) => + Task.WhenAll(connection._readInputTask, connection._frame.StopAsync()).ContinueWith((task2, state2) => { var connection2 = (Connection)state2; - connection2._filterContext.Connection.Dispose(); - connection2._filteredStreamAdapter.Dispose(); + connection2._filteredStream.Dispose(); + connection2._adaptedPipeline.Dispose(); }, connection); } }, this); @@ -187,31 +157,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { _frame.SetBadRequestState(RequestRejectionReason.RequestTimeout); } - + StopAsync(); } Interlocked.Exchange(ref _lastTimestamp, timestamp); } - private void ApplyConnectionFilter() + private async Task ApplyConnectionAdaptersAsync() { - if (_filterContext.Connection != _libuvStream) + try { - _filteredStreamAdapter = new FilteredStreamAdapter(ConnectionId, _filterContext.Connection, Thread.Memory, Log, ThreadPool, _bufferSizeControl); + var rawStream = new RawStream(Input, Output); + var adapterContext = new ConnectionAdapterContext(rawStream); + var adaptedConnections = new IAdaptedConnection[_connectionAdapters.Count]; - _frame.Input = _filteredStreamAdapter.SocketInput; - _frame.Output = _filteredStreamAdapter.SocketOutput; + for (var i = 0; i < _connectionAdapters.Count; i++) + { + var adaptedConnection = await _connectionAdapters[i].OnConnectionAsync(adapterContext); + adaptedConnections[i] = adaptedConnection; + adapterContext = new ConnectionAdapterContext(adaptedConnection.ConnectionStream); + } - // Don't attempt to read input if connection has already closed. - // This can happen if a client opens a connection and immediately closes it. - _readInputTask = _socketClosedTcs.Task.Status == TaskStatus.WaitingForActivation ? - _filteredStreamAdapter.ReadInputAsync() : - TaskCache.CompletedTask; + if (adapterContext.ConnectionStream != rawStream) + { + _filteredStream = adapterContext.ConnectionStream; + _adaptedPipeline = new AdaptedPipeline(ConnectionId, adapterContext.ConnectionStream, + Thread.Memory, Log, ThreadPool, _bufferSizeControl); + + _frame.Input = _adaptedPipeline.SocketInput; + _frame.Output = _adaptedPipeline.SocketOutput; + + // Don't attempt to read input if connection has already closed. + // This can happen if a client opens a connection and immediately closes it. + _readInputTask = _socketClosedTcs.Task.Status == TaskStatus.WaitingForActivation + ? _adaptedPipeline.ReadInputAsync() + : TaskCache.CompletedTask; + } + + _frame.AdaptedConnections = adaptedConnections; + _frame.Start(); + } + catch (Exception ex) + { + Log.LogError(0, ex, $"Uncaught exception from the {nameof(IConnectionAdapter.OnConnectionAsync)} method of an {nameof(IConnectionAdapter)}."); + ConnectionControl.End(ProduceEndType.SocketDisconnect); } - - _frame.PrepareRequest = _filterContext.PrepareRequest; - _frame.Start(); } private static Libuv.uv_buf_t AllocCallback(UvStreamHandle handle, int suggestedSize, object state) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionContext.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionContext.cs index 72344c73b1..decc5dbf8c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionContext.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionContext.cs @@ -31,7 +31,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public IPEndPoint LocalEndPoint { get; set; } public string ConnectionId { get; set; } - - public Action PrepareRequest { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 2cb16d13bb..dfc49447b9 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http ServerOptions = context.ListenerContext.ServiceContext.ServerOptions; - _pathBase = ServerAddress.PathBase; + _pathBase = context.ListenerContext.ListenOptions.PathBase; FrameControl = this; _keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds; @@ -97,23 +98,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public ConnectionContext ConnectionContext { get; } public SocketInput Input { get; set; } public ISocketOutput Output { get; set; } - public Action PrepareRequest - { - get - { - return ConnectionContext.PrepareRequest; - } - set - { - ConnectionContext.PrepareRequest = value; - } - } + public IEnumerable AdaptedConnections { get; set; } protected IConnectionControl ConnectionControl => ConnectionContext.ConnectionControl; protected IKestrelTrace Log => ConnectionContext.ListenerContext.ServiceContext.Log; private DateHeaderValueManager DateHeaderValueManager => ConnectionContext.ListenerContext.ServiceContext.DateHeaderValueManager; - private ServerAddress ServerAddress => ConnectionContext.ListenerContext.ServerAddress; // Hold direct reference to ServerOptions since this is used very often in the request processing path private KestrelServerOptions ServerOptions { get; } private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint; @@ -367,7 +357,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http LocalPort = LocalEndPoint?.Port ?? 0; ConnectionIdFeature = ConnectionId; - PrepareRequest?.Invoke(this); + if (AdaptedConnections != null) + { + try + { + foreach (var adaptedConnection in AdaptedConnections) + { + adaptedConnection.PrepareRequest(this); + } + } + catch (Exception ex) + { + Log.LogError(0, ex, $"Uncaught exception from the {nameof(IAdaptedConnection.PrepareRequest)} method of an {nameof(IAdaptedConnection)}."); + } + } _manuallySetRequestAbortToken = null; _abortedCts = null; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Listener.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Listener.cs index f66485a5ed..3bb269f6c8 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Listener.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Listener.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http /// /// Base class for listeners in Kestrel. Listens for incoming connections /// - public abstract class Listener : ListenerContext, IAsyncDisposable + public class Listener : ListenerContext, IAsyncDisposable { private bool _closed; - protected Listener(ServiceContext serviceContext) + public Listener(ServiceContext serviceContext) : base(serviceContext) { } @@ -26,20 +26,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public IKestrelTrace Log => ServiceContext.Log; public Task StartAsync( - ServerAddress address, + ListenOptions listenOptions, KestrelThread thread) { - ServerAddress = address; + ListenOptions = listenOptions; Thread = thread; var tcs = new TaskCompletionSource(this); Thread.Post(state => { - var tcs2 = (TaskCompletionSource)state; + var tcs2 = (TaskCompletionSource) state; try { - var listener = ((Listener)tcs2.Task.AsyncState); + var listener = ((Listener) tcs2.Task.AsyncState); listener.ListenSocket = listener.CreateListenSocket(); ListenSocket.Listen(Constants.ListenBacklog, ConnectionCallback, this); tcs2.SetResult(0); @@ -56,11 +56,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http /// /// Creates the socket used to listen for incoming connections /// - protected abstract UvStreamHandle CreateListenSocket(); + private UvStreamHandle CreateListenSocket() + { + switch (ListenOptions.Type) + { + case ListenType.IPEndPoint: + case ListenType.FileHandle: + var socket = new UvTcpHandle(Log); + + try + { + socket.Init(Thread.Loop, Thread.QueueCloseHandle); + socket.NoDelay(ListenOptions.NoDelay); + + if (ListenOptions.Type == ListenType.IPEndPoint) + { + socket.Bind(ListenOptions.IPEndPoint); + + // If requested port was "0", replace with assigned dynamic port. + ListenOptions.IPEndPoint = socket.GetSockIPEndPoint(); + } + else + { + socket.Open((IntPtr)ListenOptions.FileHandle); + } + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + case ListenType.SocketPath: + var pipe = new UvPipeHandle(Log); + + try + { + pipe.Init(Thread.Loop, Thread.QueueCloseHandle, false); + pipe.Bind(ListenOptions.SocketPath); + } + catch + { + pipe.Dispose(); + throw; + } + + return pipe; + default: + throw new NotSupportedException(); + } + } private static void ConnectionCallback(UvStreamHandle stream, int status, Exception error, object state) { - var listener = (Listener)state; + var listener = (Listener) state; if (error != null) { @@ -77,7 +127,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http /// /// Socket being used to listen on /// Connection status - protected abstract void OnConnection(UvStreamHandle listenSocket, int status); + private void OnConnection(UvStreamHandle listenSocket, int status) + { + UvStreamHandle acceptSocket = null; + + try + { + acceptSocket = CreateAcceptSocket(); + listenSocket.Accept(acceptSocket); + DispatchConnection(acceptSocket); + } + catch (UvException ex) + { + Log.LogError(0, ex, "Listener.OnConnection"); + acceptSocket?.Dispose(); + } + } protected virtual void DispatchConnection(UvStreamHandle socket) { diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerContext.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerContext.cs index 8ecee653f0..083f1a2148 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerContext.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerContext.cs @@ -1,6 +1,9 @@ // 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.AspNetCore.Server.Kestrel.Internal.Networking; + namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { public class ListenerContext @@ -12,10 +15,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public ServiceContext ServiceContext { get; set; } - public ServerAddress ServerAddress { get; set; } + public ListenOptions ListenOptions { get; set; } public KestrelThread Thread { get; set; } - public KestrelServerOptions ServerOptions => ServiceContext.ServerOptions; + /// + /// Creates a socket which can be used to accept an incoming connection. + /// + protected UvStreamHandle CreateAcceptSocket() + { + switch (ListenOptions.Type) + { + case ListenType.IPEndPoint: + case ListenType.FileHandle: + var tcpHandle = new UvTcpHandle(ServiceContext.Log); + tcpHandle.Init(Thread.Loop, Thread.QueueCloseHandle); + tcpHandle.NoDelay(ListenOptions.NoDelay); + return tcpHandle; + case ListenType.SocketPath: + var pipeHandle = new UvPipeHandle(ServiceContext.Log); + pipeHandle.Init(Thread.Loop, Thread.QueueCloseHandle); + return pipeHandle; + default: + throw new InvalidOperationException(); + } + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerPrimary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerPrimary.cs index 4b192c505d..ebf6267630 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerPrimary.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerPrimary.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http /// A primary listener waits for incoming connections on a specified socket. Incoming /// connections may be passed to a secondary listener to handle. /// - public abstract class ListenerPrimary : Listener + public class ListenerPrimary : Listener { private readonly List _dispatchPipes = new List(); private int _dispatchIndex; @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http // but it has no other functional significance private readonly ArraySegment> _dummyMessage = new ArraySegment>(new[] { new ArraySegment(new byte[] { 1, 2, 3, 4 }) }); - protected ListenerPrimary(ServiceContext serviceContext) : base(serviceContext) + public ListenerPrimary(ServiceContext serviceContext) : base(serviceContext) { } @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public async Task StartAsync( string pipeName, byte[] pipeMessage, - ServerAddress address, + ListenOptions listenOptions, KestrelThread thread) { _pipeName = pipeName; @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false); } - await StartAsync(address, thread).ConfigureAwait(false); + await StartAsync(listenOptions, thread).ConfigureAwait(false); await Thread.PostAsync(state => ((ListenerPrimary)state).PostCallback(), this).ConfigureAwait(false); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerSecondary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerSecondary.cs index 078f9c6e03..9ce1478b24 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerSecondary.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ListenerSecondary.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http /// A secondary listener is delegated requests from a primary listener via a named pipe or /// UNIX domain socket. /// - public abstract class ListenerSecondary : ListenerContext, IAsyncDisposable + public class ListenerSecondary : ListenerContext, IAsyncDisposable { private string _pipeName; private byte[] _pipeMessage; @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private Libuv.uv_buf_t _buf; private bool _closed; - protected ListenerSecondary(ServiceContext serviceContext) : base(serviceContext) + public ListenerSecondary(ServiceContext serviceContext) : base(serviceContext) { _ptr = Marshal.AllocHGlobal(4); } @@ -35,14 +35,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public Task StartAsync( string pipeName, byte[] pipeMessage, - ServerAddress address, + ListenOptions listenOptions, KestrelThread thread) { _pipeName = pipeName; _pipeMessage = pipeMessage; _buf = thread.Loop.Libuv.buf_init(_ptr, 4); - ServerAddress = address; + ListenOptions = listenOptions; Thread = thread; DispatchPipe = new UvPipeHandle(Log); @@ -173,11 +173,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } } - /// - /// Creates a socket which can be used to accept an incoming connection - /// - protected abstract UvStreamHandle CreateAcceptSocket(); - private void FreeBuffer() { var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListener.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListener.cs deleted file mode 100644 index 8ef9e38683..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListener.cs +++ /dev/null @@ -1,61 +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 Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// Implementation of that uses UNIX domain sockets as its transport. - /// - public class PipeListener : Listener - { - public PipeListener(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates the socket used to listen for incoming connections - /// - protected override UvStreamHandle CreateListenSocket() - { - var socket = new UvPipeHandle(Log); - - try - { - socket.Init(Thread.Loop, Thread.QueueCloseHandle, false); - socket.Bind(ServerAddress.UnixPipePath); - } - catch - { - socket.Dispose(); - throw; - } - - return socket; - } - - /// - /// Handles an incoming connection - /// - /// Socket being used to listen on - /// Connection status - protected override void OnConnection(UvStreamHandle listenSocket, int status) - { - var acceptSocket = new UvPipeHandle(Log); - - try - { - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle, false); - listenSocket.Accept(acceptSocket); - DispatchConnection(acceptSocket); - } - catch (UvException ex) - { - Log.LogError(0, ex, "PipeListener.OnConnection"); - acceptSocket.Dispose(); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerPrimary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerPrimary.cs deleted file mode 100644 index ebc59cfadb..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerPrimary.cs +++ /dev/null @@ -1,62 +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 Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// An implementation of using UNIX sockets. - /// - public class PipeListenerPrimary : ListenerPrimary - { - public PipeListenerPrimary(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates the socket used to listen for incoming connections - /// - protected override UvStreamHandle CreateListenSocket() - { - var socket = new UvPipeHandle(Log); - - try - { - socket.Init(Thread.Loop, Thread.QueueCloseHandle, false); - socket.Bind(ServerAddress.UnixPipePath); - } - catch - { - socket.Dispose(); - throw; - } - - return socket; - } - - /// - /// Handles an incoming connection - /// - /// Socket being used to listen on - /// Connection status - protected override void OnConnection(UvStreamHandle listenSocket, int status) - { - var acceptSocket = new UvPipeHandle(Log); - - try - { - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle, false); - listenSocket.Accept(acceptSocket); - DispatchConnection(acceptSocket); - } - catch (UvException ex) - { - Log.LogError(0, ex, "ListenerPrimary.OnConnection"); - acceptSocket.Dispose(); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerSecondary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerSecondary.cs deleted file mode 100644 index be9879f13b..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/PipeListenerSecondary.cs +++ /dev/null @@ -1,27 +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 Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// An implementation of using UNIX sockets. - /// - public class PipeListenerSecondary : ListenerSecondary - { - public PipeListenerSecondary(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates a socket which can be used to accept an incoming connection - /// - protected override UvStreamHandle CreateAcceptSocket() - { - var acceptSocket = new UvPipeHandle(Log); - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle, false); - return acceptSocket; - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListener.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListener.cs deleted file mode 100644 index fd69130fb8..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListener.cs +++ /dev/null @@ -1,68 +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; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// Implementation of that uses TCP sockets as its transport. - /// - public class TcpListener : Listener - { - public TcpListener(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates the socket used to listen for incoming connections - /// - protected override UvStreamHandle CreateListenSocket() - { - var socket = new UvTcpHandle(Log); - - try - { - socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(ServerOptions.NoDelay); - socket.Bind(ServerAddress); - - // If requested port was "0", replace with assigned dynamic port. - ServerAddress.Port = socket.GetSockIPEndPoint().Port; - } - catch - { - socket.Dispose(); - throw; - } - - return socket; - } - - /// - /// Handle an incoming connection - /// - /// Socket being used to listen on - /// Connection status - protected override void OnConnection(UvStreamHandle listenSocket, int status) - { - var acceptSocket = new UvTcpHandle(Log); - - try - { - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle); - acceptSocket.NoDelay(ServerOptions.NoDelay); - listenSocket.Accept(acceptSocket); - DispatchConnection(acceptSocket); - } - catch (UvException ex) - { - Log.LogError(0, ex, "TcpListener.OnConnection"); - acceptSocket.Dispose(); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerPrimary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerPrimary.cs deleted file mode 100644 index 09b856438a..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerPrimary.cs +++ /dev/null @@ -1,68 +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; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// An implementation of using TCP sockets. - /// - public class TcpListenerPrimary : ListenerPrimary - { - public TcpListenerPrimary(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates the socket used to listen for incoming connections - /// - protected override UvStreamHandle CreateListenSocket() - { - var socket = new UvTcpHandle(Log); - - try - { - socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(ServerOptions.NoDelay); - socket.Bind(ServerAddress); - - // If requested port was "0", replace with assigned dynamic port. - ServerAddress.Port = socket.GetSockIPEndPoint().Port; - } - catch - { - socket.Dispose(); - throw; - } - - return socket; - } - - /// - /// Handles an incoming connection - /// - /// Socket being used to listen on - /// Connection status - protected override void OnConnection(UvStreamHandle listenSocket, int status) - { - var acceptSocket = new UvTcpHandle(Log); - - try - { - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle); - acceptSocket.NoDelay(ServerOptions.NoDelay); - listenSocket.Accept(acceptSocket); - DispatchConnection(acceptSocket); - } - catch (UvException ex) - { - Log.LogError(0, ex, "TcpListenerPrimary.OnConnection"); - acceptSocket.Dispose(); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerSecondary.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerSecondary.cs deleted file mode 100644 index 0a34eb7202..0000000000 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TcpListenerSecondary.cs +++ /dev/null @@ -1,28 +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 Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http -{ - /// - /// An implementation of using TCP sockets. - /// - public class TcpListenerSecondary : ListenerSecondary - { - public TcpListenerSecondary(ServiceContext serviceContext) : base(serviceContext) - { - } - - /// - /// Creates a socket which can be used to accept an incoming connection - /// - protected override UvStreamHandle CreateAcceptSocket() - { - var acceptSocket = new UvTcpHandle(Log); - acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle); - acceptSocket.NoDelay(ServerOptions.NoDelay); - return acceptSocket; - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs index 3acb659218..46d0bdac5a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/Constants.cs @@ -19,6 +19,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure /// public const string UnixPipeHostPrefix = "unix:/"; + /// + /// Prefix of host name used to specify pipe file descriptor in the configuration. + /// + public const string PipeDescriptorPrefix = "pipefd:"; + + /// + /// Prefix of host name used to specify socket descriptor in the configuration. + /// + public const string SocketDescriptorPrefix = "sockfd:"; + public const string ServerName = "Kestrel"; private static int? GetECONNRESET() diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/KestrelEngine.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/KestrelEngine.cs index 3b3b46a4c9..b7a7b5a63e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/KestrelEngine.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/KestrelEngine.cs @@ -61,49 +61,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal #endif } - public IDisposable CreateServer(ServerAddress address) + public IDisposable CreateServer(ListenOptions listenOptions) { var listeners = new List(); - var usingPipes = address.IsUnixPipe; - try { - var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); - var pipeMessage = Guid.NewGuid().ToByteArray(); - - var single = Threads.Count == 1; - var first = true; - - foreach (var thread in Threads) + if (Threads.Count == 1) { - if (single) - { - var listener = usingPipes ? - (Listener) new PipeListener(ServiceContext) : - new TcpListener(ServiceContext); - listeners.Add(listener); - listener.StartAsync(address, thread).Wait(); - } - else if (first) - { - var listener = usingPipes - ? (ListenerPrimary) new PipeListenerPrimary(ServiceContext) - : new TcpListenerPrimary(ServiceContext); + var listener = new Listener(ServiceContext); + listeners.Add(listener); + listener.StartAsync(listenOptions, Threads[0]).Wait(); + } + else + { + var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); - listeners.Add(listener); - listener.StartAsync(pipeName, pipeMessage, address, thread).Wait(); - } - else - { - var listener = usingPipes - ? (ListenerSecondary) new PipeListenerSecondary(ServiceContext) - : new TcpListenerSecondary(ServiceContext); - listeners.Add(listener); - listener.StartAsync(pipeName, pipeMessage, address, thread).Wait(); - } + var listenerPrimary = new ListenerPrimary(ServiceContext); + listeners.Add(listenerPrimary); + listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, Threads[0]).Wait(); - first = false; + foreach (var thread in Threads.Skip(1)) + { + var listenerSecondary = new ListenerSecondary(ServiceContext); + listeners.Add(listenerSecondary); + listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, thread).Wait(); + } } return new Disposable(() => @@ -114,7 +98,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal catch { DisposeListeners(listeners); - throw; } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/SockAddr.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/SockAddr.cs index ae4196153f..71e7c59b32 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/SockAddr.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/SockAddr.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking // 0000 0000 0b99 0017 => The third and fourth bytes 990B is the actual port // 9103 e000 9848 0120 => IPv6 address is represented in the 128bit field1 and field2. // 54a3 3e9d 2411 efb9 Read these two 64-bit long from right to left byte by byte. - // 0000 0000 0000 0000 + // 0000 0000 0000 0010 => Scope ID 0x10 (eg [::1%16]) the first 4 bytes of field3 in host byte order. // // Example 2: 10.135.34.141:39178 when adopt dual-stack sockets, IPv4 is mapped to IPv6 // @@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking // Reference: // - Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms740506(v=vs.85).aspx // - Linux: https://github.com/torvalds/linux/blob/6a13feb9c82803e2b815eca72fa7a9f5561d7861/include/linux/socket.h + // - Linux (sin6_scope_id): https://github.com/torvalds/linux/blob/5924bbecd0267d87c24110cbe2041b5075173a25/net/sunrpc/addr.c#L82 // - Apple: http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/socket.h // Quick calculate the port by mask the field and locate the byte 3 and byte 4 @@ -92,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking *((long*)(b + 8)) = _field2; } - return new IPEndPoint(new IPAddress(bytes), port); + return new IPEndPoint(new IPAddress(bytes, scopeid: _field3 & 0xFFFFFFFF), port); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTcpHandle.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTcpHandle.cs index 3684f4f412..402f2f37cd 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTcpHandle.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTcpHandle.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Net; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; @@ -24,20 +25,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking _uv.tcp_init(loop, this); } - public void Bind(ServerAddress address) + public void Open(IntPtr fileDescriptor) { - var endpoint = CreateIPEndpoint(address); + _uv.tcp_open(this, fileDescriptor); + } + public void Bind(IPEndPoint endPoint) + { SockAddr addr; - var addressText = endpoint.Address.ToString(); + var addressText = endPoint.Address.ToString(); Exception error1; - _uv.ip4_addr(addressText, endpoint.Port, out addr, out error1); + _uv.ip4_addr(addressText, endPoint.Port, out addr, out error1); if (error1 != null) { Exception error2; - _uv.ip6_addr(addressText, endpoint.Port, out addr, out error2); + _uv.ip6_addr(addressText, endPoint.Port, out addr, out error2); if (error2 != null) { throw error1; @@ -65,38 +69,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking return socketAddress.GetIPEndPoint(); } - public void Open(IntPtr hSocket) - { - _uv.tcp_open(this, hSocket); - } - public void NoDelay(bool enable) { _uv.tcp_nodelay(this, enable); } - - /// - /// Returns an for the given host an port. - /// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any. - /// - public static IPEndPoint CreateIPEndpoint(ServerAddress address) - { - // TODO: IPv6 support - IPAddress ip; - - if (!IPAddress.TryParse(address.Host, out ip)) - { - if (string.Equals(address.Host, "localhost", StringComparison.OrdinalIgnoreCase)) - { - ip = IPAddress.Loopback; - } - else - { - ip = IPAddress.IPv6Any; - } - } - - return new IPEndPoint(ip, address.Port); - } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs index 57fbc17017..a8a9c5af66 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; @@ -113,84 +114,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel engine.Start(threadCount); var atLeastOneListener = false; - foreach (var address in _serverAddresses.Addresses.ToArray()) + var listenOptions = Options.ListenOptions; + + if (listenOptions.Any()) { - var parsedAddress = ServerAddress.FromUrl(address); - atLeastOneListener = true; - - if (!parsedAddress.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + var addresses = _serverAddresses.Addresses; + if (addresses.SingleOrDefault() != "http://localhost:5000") { - try - { - _disposables.Push(engine.CreateServer(parsedAddress)); - } - catch (AggregateException ex) - { - if ((ex.InnerException as UvException)?.StatusCode == Constants.EADDRINUSE) - { - throw new IOException($"Failed to bind to address {parsedAddress}: address already in use.", ex); - } + var joined = string.Join(", ", addresses); + throw new NotSupportedException($"Specifying address(es) '{joined}' is incompatible with also configuring endpoint(s) in UseKestrel."); + } - throw; + _serverAddresses.Addresses.Clear(); + } + else + { + // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature. + var copiedAddresses = _serverAddresses.Addresses.ToArray(); + _serverAddresses.Addresses.Clear(); + + foreach (var address in copiedAddresses) + { + var parsedAddress = ServerAddress.FromUrl(address); + + if (parsedAddress.IsUnixPipe) + { + listenOptions.Add(new ListenOptions(parsedAddress.UnixPipePath) + { + Scheme = parsedAddress.Scheme, + PathBase = parsedAddress.PathBase + }); + } + else + { + if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. + StartLocalhost(engine, parsedAddress); + + // 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 + { + // These endPoints will be added later to _serverAddresses.Addresses + listenOptions.Add(new ListenOptions(CreateIPEndPoint(parsedAddress)) + { + Scheme = parsedAddress.Scheme, + PathBase = parsedAddress.PathBase + }); + } } } - else + } + + foreach (var endPoint in listenOptions) + { + atLeastOneListener = true; + + try { - if (parsedAddress.Port == 0) + _disposables.Push(engine.CreateServer(endPoint)); + } + catch (AggregateException ex) + { + if ((ex.InnerException as UvException)?.StatusCode == Constants.EADDRINUSE) { - 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."); + throw new IOException($"Failed to bind to address {endPoint}: address already in use.", ex); } - var ipv4Address = parsedAddress.WithHost("127.0.0.1"); - var exceptions = new List(); - - try - { - _disposables.Push(engine.CreateServer(ipv4Address)); - } - catch (AggregateException ex) when (ex.InnerException is UvException) - { - var uvEx = (UvException)ex.InnerException; - if (uvEx.StatusCode == Constants.EADDRINUSE) - { - throw new IOException($"Failed to bind to address {parsedAddress.ToString()} on the IPv4 loopback interface: port already in use.", ex); - } - else - { - _logger.LogWarning(0, $"Unable to bind to {parsedAddress.ToString()} on the IPv4 loopback interface: ({uvEx.Message})"); - exceptions.Add(uvEx); - } - } - - var ipv6Address = parsedAddress.WithHost("[::1]"); - - try - { - _disposables.Push(engine.CreateServer(ipv6Address)); - } - catch (AggregateException ex) when (ex.InnerException is UvException) - { - var uvEx = (UvException)ex.InnerException; - if (uvEx.StatusCode == Constants.EADDRINUSE) - { - throw new IOException($"Failed to bind to address {parsedAddress.ToString()} on the IPv6 loopback interface: port already in use.", ex); - } - else - { - _logger.LogWarning(0, $"Unable to bind to {parsedAddress.ToString()} on the IPv6 loopback interface: ({uvEx.Message})"); - exceptions.Add(uvEx); - } - } - - if (exceptions.Count == 2) - { - throw new IOException($"Failed to bind to address {parsedAddress.ToString()}.", new AggregateException(exceptions)); - } + throw; } // If requested port was "0", replace with assigned dynamic port. - _serverAddresses.Addresses.Remove(address); - _serverAddresses.Addresses.Add(parsedAddress.ToString()); + _serverAddresses.Addresses.Add(endPoint.ToString()); } if (!atLeastOneListener) @@ -227,5 +226,84 @@ namespace Microsoft.AspNetCore.Server.Kestrel $"Maximum request buffer size ({Options.Limits.MaxRequestBufferSize.Value}) must be greater than or equal to maximum request line size ({Options.Limits.MaxRequestLineSize})."); } } + + private void StartLocalhost(KestrelEngine engine, ServerAddress parsedAddress) + { + 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)) + { + Scheme = parsedAddress.Scheme, + PathBase = parsedAddress.PathBase + }; + + _disposables.Push(engine.CreateServer(ipv4ListenOptions)); + } + catch (AggregateException ex) when (ex.InnerException is UvException) + { + var uvEx = (UvException)ex.InnerException; + if (uvEx.StatusCode == Constants.EADDRINUSE) + { + throw new IOException($"Failed to bind to address {parsedAddress} on the IPv4 loopback interface: port already in use.", ex); + } + else + { + _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv4 loopback interface: ({uvEx.Message})"); + exceptions.Add(uvEx); + } + } + + try + { + var ipv6ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.IPv6Loopback, parsedAddress.Port)) + { + Scheme = parsedAddress.Scheme, + PathBase = parsedAddress.PathBase + }; + + _disposables.Push(engine.CreateServer(ipv6ListenOptions)); + } + catch (AggregateException ex) when (ex.InnerException is UvException) + { + var uvEx = (UvException)ex.InnerException; + if (uvEx.StatusCode == Constants.EADDRINUSE) + { + throw new IOException($"Failed to bind to address {parsedAddress} on the IPv6 loopback interface: port already in use.", ex); + } + else + { + _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv6 loopback interface: ({uvEx.Message})"); + exceptions.Add(uvEx); + } + } + + 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); + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs index 7fe386e34c..c458d1e5f7 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs @@ -1,5 +1,9 @@ +// 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.AspNetCore.Server.Kestrel.Filter; +using System.Collections.Generic; +using System.Net; namespace Microsoft.AspNetCore.Server.Kestrel { @@ -8,6 +12,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel /// public class KestrelServerOptions { + /// + /// Configures the endpoints that Kestrel should listen to. + /// + /// + /// If this list is empty, the server.urls setting (e.g. UseUrls) is used. + /// + internal List ListenOptions { get; } = new List(); + /// /// Gets or sets whether the Server header should be included in each response. /// @@ -17,22 +29,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel public bool AddServerHeader { get; set; } = true; /// - /// Enables the UseKestrel options callback to resolve and use services registered by the application during startup. + /// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Typically initialized by . /// public IServiceProvider ApplicationServices { get; set; } - /// - /// Gets or sets an that allows each connection - /// to be intercepted and transformed. - /// Configured by the UseHttps() and - /// extension methods. - /// - /// - /// Defaults to null. - /// - public IConnectionFilter ConnectionFilter { get; set; } - /// /// /// This property is obsolete and will be removed in a future version. @@ -64,14 +65,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel /// public KestrelServerLimits Limits { get; } = new KestrelServerLimits(); - /// - /// Set to false to enable Nagle's algorithm for all connections. - /// - /// - /// Defaults to true. - /// - public bool NoDelay { get; set; } = true; - /// /// The amount of time after the server begins shutting down before connections will be forcefully closed. /// Kestrel will wait for the duration of the timeout for any ongoing request processing to complete before @@ -115,5 +108,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel return threadCount; } } + + /// + /// Bind to given IP address and port. + /// + public void Listen(IPAddress address, int port) + { + Listen(address, port, _ => { }); + } + + /// + /// Bind to given IP address and port. + /// The callback configures endpoint-specific settings. + /// + public void Listen(IPAddress address, int port, Action configure) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + Listen(new IPEndPoint(address, port), configure); + } + + /// + /// Bind to given IP endpoint. + /// + public void Listen(IPEndPoint endPoint) + { + Listen(endPoint, _ => { }); + } + + /// + /// Bind to given IP address and port. + /// The callback configures endpoint-specific settings. + /// + public void Listen(IPEndPoint endPoint, Action configure) + { + if (endPoint == null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var listenOptions = new ListenOptions(endPoint) { KestrelServerOptions = this }; + configure(listenOptions); + ListenOptions.Add(listenOptions); + } + + /// + /// Bind to given Unix domain socket path. + /// + public void ListenUnixSocket(string socketPath) + { + ListenUnixSocket(socketPath, _ => { }); + } + + /// + /// Bind to given Unix domain socket path. + /// Specify callback to configure endpoint-specific settings. + /// + public void ListenUnixSocket(string socketPath, Action configure) + { + if (socketPath == null) + { + throw new ArgumentNullException(nameof(socketPath)); + } + if (socketPath.Length == 0 || socketPath[0] != '/') + { + throw new ArgumentException("Unix socket path must be absolute.", nameof(socketPath)); + } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var listenOptions = new ListenOptions(socketPath) { KestrelServerOptions = this }; + configure(listenOptions); + ListenOptions.Add(listenOptions); + } + + /// + /// Open a socket file descriptor. + /// + public void ListenHandle(ulong handle) + { + ListenHandle(handle, _ => { }); + } + + /// + /// Open a socket file descriptor. + /// The callback configures endpoint-specific settings. + /// + public void ListenHandle(ulong handle, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var listenOptions = new ListenOptions(handle) { KestrelServerOptions = this }; + configure(listenOptions); + ListenOptions.Add(listenOptions); + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs new file mode 100644 index 0000000000..f1a458776e --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs @@ -0,0 +1,108 @@ +// 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.Net; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; + +namespace Microsoft.AspNetCore.Server.Kestrel +{ + /// + /// Describes either an , Unix domain socket path, or a file descriptor for an already open + /// socket that Kestrel should bind to or open. + /// + public class ListenOptions + { + internal ListenOptions(IPEndPoint endPoint) + { + Type = ListenType.IPEndPoint; + IPEndPoint = endPoint; + } + + internal ListenOptions(string socketPath) + { + Type = ListenType.SocketPath; + SocketPath = socketPath; + } + + internal ListenOptions(ulong fileHandle) + { + Type = ListenType.FileHandle; + FileHandle = fileHandle; + } + + /// + /// The type of interface being described: either an , Unix domain socket path, or a file descriptor. + /// + public ListenType Type { get; } + + // IPEndPoint is mutable so port 0 can be updated to the bound port. + /// + /// The to bind to. + /// Only set if the is . + /// + public IPEndPoint IPEndPoint { get; internal set; } + + /// + /// The absolute path to a Unix domain socket to bind to. + /// Only set if the is . + /// + public string SocketPath { get; } + + /// + /// A file descriptor for the socket to open. + /// Only set if the is . + /// + public ulong FileHandle { get; } + + /// + /// Enables an to resolve and use services registered by the application during startup. + /// Only set if accessed from the callback of a Listen* method. + /// + public KestrelServerOptions KestrelServerOptions { get; internal set; } + + /// + /// Set to false to enable Nagle's algorithm for all connections. + /// + /// + /// Defaults to true. + /// + public bool NoDelay { get; set; } = true; + + /// + /// Gets the that allows each connection + /// to be intercepted and transformed. + /// Configured by the UseHttps() and + /// extension methods. + /// + /// + /// Defaults to empty. + /// + public List ConnectionAdapters { get; } = new List(); + + // PathBase and Scheme are hopefully only a temporary measure for back compat with IServerAddressesFeature. + // This allows a ListenOptions to describe all the information encoded in IWebHostBuilder.UseUrls. + internal string PathBase { get; set; } + internal string Scheme { get; set; } = "http"; + + public override string ToString() + { + // Use http scheme for all addresses. If https should be used for this endPoint, + // it can still be configured for this endPoint specifically. + switch (Type) + { + case ListenType.IPEndPoint: + return $"{Scheme}://{IPEndPoint}{PathBase}"; + case ListenType.SocketPath: + // ":" is used by ServerAddress to separate the socket path from PathBase. + return $"{Scheme}://unix:{SocketPath}:{PathBase}"; + case ListenType.FileHandle: + // This was never supported via --server.urls, so no need to include Scheme or PathBase. + return "http://"; + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/ListenType.cs b/src/Microsoft.AspNetCore.Server.Kestrel/ListenType.cs new file mode 100644 index 0000000000..7ead1d91e0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/ListenType.cs @@ -0,0 +1,15 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel +{ + /// + /// Enumerates the types. + /// + public enum ListenType + { + IPEndPoint, + SocketPath, + FileHandle + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs b/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs index 4771fe54f7..cfa0bff4e8 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs @@ -169,4 +169,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel }; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Systemd/KesterlServerOptionsSystemdExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Systemd/KesterlServerOptionsSystemdExtensions.cs new file mode 100644 index 0000000000..ca24796a65 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Systemd/KesterlServerOptionsSystemdExtensions.cs @@ -0,0 +1,45 @@ +// 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.Diagnostics; +using System.Globalization; +using Microsoft.AspNetCore.Server.Kestrel; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class KesterlServerOptionsSystemdExtensions + { + // SD_LISTEN_FDS_START https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html + private const ulong SdListenFdsStart = 3; + private const string ListenPidEnvVar = "LISTEN_PID"; + + /// + /// Open file descriptor (SD_LISTEN_FDS_START) initialized by systemd socket-based activation logic if available. + /// + /// + /// The . + /// + public static KestrelServerOptions UseSystemd(this KestrelServerOptions options) + { + return options.UseSystemd(_ => { }); + } + + /// + /// Open file descriptor (SD_LISTEN_FDS_START) initialized by systemd socket-based activation logic if available. + /// Specify callback to configure endpoint-specific settings. + /// + /// + /// The . + /// + public static KestrelServerOptions UseSystemd(this KestrelServerOptions options, Action configure) + { + if (string.Equals(Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture), Environment.GetEnvironmentVariable(ListenPidEnvVar), StringComparison.Ordinal)) + { + options.ListenHandle(SdListenFdsStart, configure); + } + + return options; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.net45.json b/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.net45.json new file mode 100644 index 0000000000..95752b8370 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.net45.json @@ -0,0 +1,46 @@ +[ + { + "OldTypeId": "public static class Microsoft.AspNetCore.Hosting.KestrelServerOptionsConnectionLoggingExtensions", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.ConnectionFilterContext", + "Kind": "Removal" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.LoggingConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.NoOpConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter get_ConnectionFilter()", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Void set_ConnectionFilter(Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter value)", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Boolean get_NoDelay()", + "NewTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.ListenOptions", + "NewMemberId": "public System.Boolean get_NoDelay()", + "Kind": "Modification" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Void set_NoDelay(System.Boolean value)", + "NewTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.ListenOptions", + "NewMemberId": "public System.Void set_NoDelay(System.Boolean value)", + "Kind": "Modification" + } +] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.netcore.json b/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.netcore.json new file mode 100644 index 0000000000..95752b8370 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/exceptions.netcore.json @@ -0,0 +1,46 @@ +[ + { + "OldTypeId": "public static class Microsoft.AspNetCore.Hosting.KestrelServerOptionsConnectionLoggingExtensions", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.ConnectionFilterContext", + "Kind": "Removal" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.LoggingConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.Filter.NoOpConnectionFilter : Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter get_ConnectionFilter()", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Void set_ConnectionFilter(Microsoft.AspNetCore.Server.Kestrel.Filter.IConnectionFilter value)", + "Kind": "Removal" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Boolean get_NoDelay()", + "NewTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.ListenOptions", + "NewMemberId": "public System.Boolean get_NoDelay()", + "Kind": "Modification" + }, + { + "OldTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions", + "OldMemberId": "public System.Void set_NoDelay(System.Boolean value)", + "NewTypeId": "public class Microsoft.AspNetCore.Server.Kestrel.ListenOptions", + "NewMemberId": "public System.Void set_NoDelay(System.Boolean value)", + "Kind": "Modification" + } +] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/project.json b/src/Microsoft.AspNetCore.Server.Kestrel/project.json index 2e7d82d6bb..fc40afeee6 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/project.json +++ b/src/Microsoft.AspNetCore.Server.Kestrel/project.json @@ -37,6 +37,7 @@ }, "netstandard1.3": { "dependencies": { + "System.Diagnostics.Process": "4.4.0-*", "System.Threading.Thread": "4.4.0-*", "System.Threading.ThreadPool": "4.4.0-*" } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs index e2513ece90..2b57c4d575 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs @@ -16,13 +16,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public class AddressRegistrationTests { - [Theory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv4))] + [Theory, MemberData(nameof(AddressRegistrationDataIPv4))] public async Task RegisterAddresses_IPv4_Success(string addressInput, Func testUrls) { await RegisterAddresses_Success(addressInput, testUrls); @@ -35,14 +36,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await RegisterAddresses_Success(addressInput, testUrls); } - [ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv4Port443))] - [PortSupportedCondition(443)] - public async Task RegisterAddresses_IPv4Port443_Success(string addressInput, Func testUrls) + [Theory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(IPEndPointRegistrationDataRandomPort))] + [IPv6SupportedCondition] + public async Task RegisterIPEndPoint_RandomPort_Success(IPEndPoint endPoint, Func testUrl) { - await RegisterAddresses_Success(addressInput, testUrls); + await RegisterIPEndPoint_Success(endPoint, testUrl); } - [ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv6))] + [ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(IPEndPointRegistrationDataPort443))] + [IPv6SupportedCondition] + [PortSupportedCondition(443)] + public async Task RegisterIPEndPoint_Port443_Success(IPEndPoint endpoint, Func testUrl) + { + await RegisterIPEndPoint_Success(endpoint, testUrl); + } + + [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6))] [IPv6SupportedCondition] public async Task RegisterAddresses_IPv6_Success(string addressInput, Func testUrls) { @@ -57,15 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await RegisterAddresses_Success(addressInput, testUrls); } - [ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv6Port443))] - [IPv6SupportedCondition] - [PortSupportedCondition(443)] - public async Task RegisterAddresses_IPv6Port443_Success(string addressInput, Func testUrls) - { - await RegisterAddresses_Success(addressInput, testUrls); - } - - [ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv6ScopeId))] + [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6ScopeId))] [IPv6SupportedCondition] public async Task RegisterAddresses_IPv6ScopeId_Success(string addressInput, Func testUrls) { @@ -75,10 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private async Task RegisterAddresses_Success(string addressInput, Func testUrls) { var hostBuilder = new WebHostBuilder() - .UseKestrel(options => - { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); - }) + .UseKestrel() .UseUrls(addressInput) .Configure(ConfigureEchoAddress); @@ -97,6 +95,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + private async Task RegisterIPEndPoint_Success(IPEndPoint endPoint, Func testUrl) + { + var hostBuilder = new WebHostBuilder() + .UseKestrel(options => + { + options.Listen(endPoint, listenOptions => + { + if (testUrl(listenOptions.IPEndPoint).StartsWith("https")) + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + } + }); + }) + .Configure(ConfigureEchoAddress); + + using (var host = hostBuilder.Build()) + { + host.Start(); + + var options = ((IOptions)host.Services.GetService(typeof(IOptions))).Value; + Assert.Single(options.ListenOptions); + var listenOptions = options.ListenOptions[0]; + + var response = await HttpClientSlim.GetStringAsync(testUrl(listenOptions.IPEndPoint), validateCertificate: false); + + // Compare the response with Uri.ToString(), rather than testUrl directly. + // Required to handle IPv6 addresses with zone index, like "fe80::3%1" + Assert.Equal(new Uri(testUrl(listenOptions.IPEndPoint)).ToString(), response); + } + } + [Fact] public void ThrowsWhenBindingToIPv4AddressInUse() { @@ -200,37 +229,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" }); // Static ports - var port1 = GetNextPort(); - var port2 = GetNextPort(); + var port = GetNextPort(); // Loopback - dataset.Add($"http://127.0.0.1:{port1};https://127.0.0.1:{port2}", - _ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" }); + dataset.Add($"http://127.0.0.1:{port}", _ => new[] { $"http://127.0.0.1:{port}/" }); // Localhost - dataset.Add($"http://localhost:{port1};https://localhost:{port2}", - _ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/", - $"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/" }); + dataset.Add($"http://localhost:{port}", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/" }); // Any - dataset.Add($"http://*:{port1}/;https://*:{port2}/", - _ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" }); - dataset.Add($"http://+:{port1}/;https://+:{port2}/", - _ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" }); + dataset.Add($"http://*:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" }); + dataset.Add($"http://+:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" }); // Path after port - dataset.Add($"http://127.0.0.1:{port1}/base/path;https://127.0.0.1:{port2}/base/path", - _ => new[] { $"http://127.0.0.1:{port1}/base/path", $"https://127.0.0.1:{port2}/base/path" }); + dataset.Add($"http://127.0.0.1:{port}/base/path", _ => new[] { $"http://127.0.0.1:{port}/base/path" }); // Dynamic port and non-loopback addresses - dataset.Add("http://127.0.0.1:0/;https://127.0.0.1:0/", GetTestUrls); - dataset.Add($"http://{Dns.GetHostName()}:0/;https://{Dns.GetHostName()}:0/", GetTestUrls); + dataset.Add("http://127.0.0.1:0/", GetTestUrls); + dataset.Add($"http://{Dns.GetHostName()}:0/", GetTestUrls); var ipv4Addresses = GetIPAddresses() .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); foreach (var ip in ipv4Addresses) { - dataset.Add($"http://{ip}:0/;https://{ip}:0/", GetTestUrls); + dataset.Add($"http://{ip}:0/", GetTestUrls); + } + + return dataset; + } + } + + public static TheoryData> IPEndPointRegistrationDataRandomPort + { + get + { + var dataset = new TheoryData>(); + + // Static port + var port = GetNextPort(); + + // Loopback + dataset.Add(new IPEndPoint(IPAddress.Loopback, port), _ => $"http://127.0.0.1:{port}/"); + dataset.Add(new IPEndPoint(IPAddress.Loopback, port), _ => $"https://127.0.0.1:{port}/"); + + // IPv6 loopback + dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, port), _ => FixTestUrl($"http://[::1]:{port}/")); + dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, port), _ => FixTestUrl($"https://[::1]:{port}/")); + + // Any + dataset.Add(new IPEndPoint(IPAddress.Any, port), _ => $"http://127.0.0.1:{port}/"); + dataset.Add(new IPEndPoint(IPAddress.Any, port), _ => $"https://127.0.0.1:{port}/"); + + // IPv6 Any + dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => $"http://127.0.0.1:{port}/"); + dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => FixTestUrl($"http://[::1]:{port}/")); + dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => $"https://127.0.0.1:{port}/"); + dataset.Add(new IPEndPoint(IPAddress.IPv6Any, port), _ => FixTestUrl($"https://[::1]:{port}/")); + + // Dynamic port + dataset.Add(new IPEndPoint(IPAddress.Loopback, 0), endPoint => $"http://127.0.0.1:{endPoint.Port}/"); + dataset.Add(new IPEndPoint(IPAddress.Loopback, 0), endPoint => $"https://127.0.0.1:{endPoint.Port}/"); + + var ipv4Addresses = GetIPAddresses() + .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); + foreach (var ip in ipv4Addresses) + { + dataset.Add(new IPEndPoint(ip, 0), endPoint => FixTestUrl($"http://{endPoint}/")); + dataset.Add(new IPEndPoint(ip, 0), endPoint => FixTestUrl($"https://{endPoint}/")); } return dataset; @@ -252,16 +317,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - public static TheoryData> AddressRegistrationDataIPv4Port443 + public static TheoryData> IPEndPointRegistrationDataPort443 { get { - var dataset = new TheoryData>(); + var dataset = new TheoryData>(); - // Default port for HTTPS (443) - dataset.Add("https://127.0.0.1", _ => new[] { "https://127.0.0.1/" }); - dataset.Add("https://localhost", _ => new[] { "https://127.0.0.1/" }); - dataset.Add("https://*", _ => new[] { "https://127.0.0.1/" }); + dataset.Add(new IPEndPoint(IPAddress.Loopback, 443), _ => "https://127.0.0.1/"); + dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, 443), _ => FixTestUrl("https://[::1]/")); + dataset.Add(new IPEndPoint(IPAddress.Any, 443), _ => "https://127.0.0.1/"); + dataset.Add(new IPEndPoint(IPAddress.IPv6Any, 443), _ => FixTestUrl("https://[::1]/")); return dataset; } @@ -278,34 +343,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" }); // Static ports - var port1 = GetNextPort(); - var port2 = GetNextPort(); + var port = GetNextPort(); // Loopback - dataset.Add($"http://[::1]:{port1}/;https://[::1]:{port2}/", - _ => new[] { $"http://[::1]:{port1}/", $"https://[::1]:{port2}/" }); + dataset.Add($"http://[::1]:{port}/", + _ => new[] { $"http://[::1]:{port}/" }); // Localhost - dataset.Add($"http://localhost:{port1};https://localhost:{port2}", - _ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", - $"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" }); + dataset.Add($"http://localhost:{port}", + _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Any - dataset.Add($"http://*:{port1}/;https://*:{port2}/", - _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", - $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" }); - dataset.Add($"http://+:{port1}/;https://+:{port2}/", - _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", - $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" }); + dataset.Add($"http://*:{port}/", + _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); + dataset.Add($"http://+:{port}/", + _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Explicit IPv4 and IPv6 on same port - dataset.Add($"http://127.0.0.1:{port1}/;http://[::1]:{port1}/;https://127.0.0.1:{port2}/;https://[::1]:{port2}/", - _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", - $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" }); + dataset.Add($"http://127.0.0.1:{port}/;http://[::1]:{port}/", + _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" }); // Path after port - dataset.Add($"http://[::1]:{port1}/base/path;https://[::1]:{port2}/base/path", - _ => new[] { $"http://[::1]:{port1}/base/path", $"https://[::1]:{port2}/base/path" }); + dataset.Add($"http://[::1]:{port}/base/path", + _ => new[] { $"http://[::1]:{port}/base/path" }); // Dynamic port and non-loopback addresses var ipv6Addresses = GetIPAddresses() @@ -313,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Where(ip => ip.ScopeId == 0); foreach (var ip in ipv6Addresses) { - dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls); + dataset.Add($"http://[{ip}]:0/", GetTestUrls); } return dataset; @@ -335,21 +395,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - public static TheoryData> AddressRegistrationDataIPv6Port443 - { - get - { - var dataset = new TheoryData>(); - - // Default port for HTTPS (443) - dataset.Add("https://[::1]", _ => new[] { "https://[::1]/" }); - dataset.Add("https://localhost", _ => new[] { "https://127.0.0.1/", "https://[::1]/" }); - dataset.Add("https://*", _ => new[] { "https://[::1]/" }); - - return dataset; - } - } - public static TheoryData> AddressRegistrationDataIPv6ScopeId { get @@ -362,7 +407,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Where(ip => ip.ScopeId != 0); foreach (var ip in ipv6Addresses) { - dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls); + dataset.Add($"http://[{ip}]:0/", GetTestUrls); } return dataset; @@ -380,11 +425,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private static string[] GetTestUrls(IServerAddressesFeature addressesFeature) { return addressesFeature.Addresses - .Select(a => a.Replace("://+", "://localhost")) - .Select(a => a.EndsWith("/") ? a : a + "/") + .Select(FixTestUrl) .ToArray(); } + private static string FixTestUrl(string url) + { + var fixedUrl = url.Replace("://+", "://localhost") + .Replace("0.0.0.0", Dns.GetHostName()) + .Replace("[::]", Dns.GetHostName()); + + if (!fixedUrl.EndsWith("/")) + { + fixedUrl = fixedUrl + "/"; + } + + return fixedUrl; + } + private void ConfigureEchoAddress(IApplicationBuilder app) { app.Run(context => diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs index 0f2caf96b9..ab8f3a5c02 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { using (var host = StartHost(protocol: "https")) { - Assert.Equal("test", await HttpClientSlim.GetStringAsync(host.GetUri(), validateCertificate: false)); + Assert.Equal("test", await HttpClientSlim.GetStringAsync(host.GetUri(isHttps: true), validateCertificate: false)); } } @@ -59,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var host = StartHost(protocol: "https", handler: (context) => context.Request.Body.CopyToAsync(context.Response.Body))) { - Assert.Equal("test post", await HttpClientSlim.PostAsync(host.GetUri(), + Assert.Equal("test post", await HttpClientSlim.PostAsync(host.GetUri(isHttps: true), new StringContent("test post"), validateCertificate: false)); } } @@ -77,10 +78,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private IWebHost StartHost(string protocol = "http", int statusCode = 200, Func handler = null) { var host = new WebHostBuilder() - .UseUrls($"{protocol}://127.0.0.1:0") .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + if (protocol == "https") + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + } + }); }) .Configure((app) => { diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpsTests.cs index cffbd96ddd..85ed2e6fd6 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpsTests.cs @@ -30,9 +30,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var hostBuilder = new WebHostBuilder() .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); }) - .UseUrls("https://127.0.0.1:0/") .UseLoggerFactory(loggerFactory) .Configure(app => { }); @@ -61,9 +63,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var hostBuilder = new WebHostBuilder() .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); }) - .UseUrls("https://127.0.0.1:0/") .UseLoggerFactory(loggerFactory) .Configure(app => { }); @@ -90,12 +94,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task DoesNotThrowObjectDisposedExceptionOnConnectionAbort() { - var x509Certificate2 = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"); + var x509Certificate2 = new X509Certificate2("TestResources/testCert.pfx", "testPassword"); var loggerFactory = new HandshakeErrorLoggerFactory(); var hostBuilder = new WebHostBuilder() .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); }) .UseUrls("https://127.0.0.1:0/") .UseLoggerFactory(loggerFactory) @@ -141,14 +148,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted() { var tcs = new TaskCompletionSource(); - var x509Certificate2 = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"); + var x509Certificate2 = new X509Certificate2("TestResources/testCert.pfx", "testPassword"); var loggerFactory = new HandshakeErrorLoggerFactory(); var hostBuilder = new WebHostBuilder() .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); }) - .UseUrls("https://127.0.0.1:0/") .UseLoggerFactory(loggerFactory) .Configure(app => app.Run(async httpContext => { @@ -194,9 +203,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var hostBuilder = new WebHostBuilder() .UseKestrel(options => { - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); }) - .UseUrls("https://127.0.0.1:0/") .UseLoggerFactory(loggerFactory) .Configure(app => { }); @@ -221,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public ILogger CreateLogger(string categoryName) { - if (categoryName == nameof(HttpsConnectionFilter)) + if (categoryName == nameof(HttpsConnectionAdapter)) { return FilterLogger; } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs index f2702be8f7..11e8f4fa0f 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs @@ -10,9 +10,9 @@ namespace Microsoft.AspNetCore.Hosting { public static class IWebHostPortExtensions { - public static string GetHost(this IWebHost host) + public static string GetHost(this IWebHost host, bool isHttps = false) { - return host.GetUri().Host; + return host.GetUri(isHttps).Host; } public static int GetPort(this IWebHost host) @@ -37,13 +37,29 @@ namespace Microsoft.AspNetCore.Hosting public static IEnumerable GetUris(this IWebHost host) { return host.ServerFeatures.Get().Addresses - .Select(a => a.Replace("://+", "://localhost")) .Select(a => new Uri(a)); } - public static Uri GetUri(this IWebHost host) + public static Uri GetUri(this IWebHost host, bool isHttps = false) { - return host.GetUris().First(); + var uri = host.GetUris().First(); + + if (isHttps && uri.Scheme == "http") + { + var uriBuilder = new UriBuilder(uri) + { + Scheme = "https", + }; + + if (uri.Port == 80) + { + uriBuilder.Port = 443; + } + + return uriBuilder.Uri; + } + + return uri; } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionFilterTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs similarity index 76% rename from test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionFilterTests.cs rename to test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs index e1f091cb3e..6637baea9c 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionFilterTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -11,18 +12,20 @@ using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { - public class LoggingConnectionFilterTests + public class LoggingConnectionAdapterTests { [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task LoggingConnectionFilterCanBeAddedBeforeAndAfterHttpsFilter() { var host = new WebHostBuilder() - .UseUrls($"https://127.0.0.1:0") - .UseKestrel(options => - { - options.UseConnectionLogging(); - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); - }) + .UseKestrel(options => + { + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + listenOptions.UseConnectionLogging(); + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + }); + }) .Configure(app => { app.Run(context => diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs index 00bece92ff..0fea1d1ee4 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs @@ -82,11 +82,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var clientFinishedSendingRequestBody = new ManualResetEvent(false); var lastBytesWritten = DateTime.MaxValue; - using (var host = StartWebHost(maxRequestBufferSize, data, startReadingRequestBody, clientFinishedSendingRequestBody)) + using (var host = StartWebHost(maxRequestBufferSize, data, ssl, startReadingRequestBody, clientFinishedSendingRequestBody)) { - var port = host.GetPort(ssl ? "https" : "http"); + var port = host.GetPort(); using (var socket = CreateSocket(port)) - using (var stream = await CreateStreamAsync(socket, ssl, host.GetHost())) + using (var stream = await CreateStreamAsync(socket, ssl, host.GetHost(ssl))) { await WritePostRequestHeaders(stream, data.Length); @@ -161,14 +161,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - private static IWebHost StartWebHost(long? maxRequestBufferSize, byte[] expectedBody, ManualResetEvent startReadingRequestBody, + private static IWebHost StartWebHost(long? maxRequestBufferSize, byte[] expectedBody, bool useSsl, ManualResetEvent startReadingRequestBody, ManualResetEvent clientFinishedSendingRequestBody) { var host = new WebHostBuilder() .UseKestrel(options => { + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + if (useSsl) + { + listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword"); + } + }); + options.Limits.MaxRequestBufferSize = maxRequestBufferSize; - options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); if (maxRequestBufferSize.HasValue && maxRequestBufferSize.Value < options.Limits.MaxRequestLineSize) @@ -176,7 +183,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize; } }) - .UseUrls("http://127.0.0.1:0/", "https://127.0.0.1:0/") .UseContentRoot(Directory.GetCurrentDirectory()) .Configure(app => app.Run(async context => { diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs index 21bd934ff8..468b6ef9b4 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs @@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests disposedTcs.TrySetResult(c.Response.StatusCode); }); - using (var server = new TestServer(handler, new TestServiceContext(), "http://127.0.0.1:0", mockHttpContextFactory.Object)) + using (var server = new TestServer(handler, new TestServiceContext(), mockHttpContextFactory.Object)) { if (!sendMalformedRequest) { diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile new file mode 100644 index 0000000000..464f6a4ec0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile @@ -0,0 +1,28 @@ +FROM microsoft/dotnet:1.1-runtime-deps + +# The "container" environment variable is read by systemd. +ENV container=docker + +# This is required by systemd and won't work without "dotnet run --privileged". +VOLUME ["/sys/fs/cgroup"] + +# Create activate-kestrel.service to launch the "publish" app on new requests to 8080. +EXPOSE 8080 +ADD .dotnet/ /usr/share/dotnet/ +ADD publish/ /publish/ +ADD activate-kestrel.socket /etc/systemd/system/activate-kestrel.socket +ADD activate-kestrel.service /etc/systemd/system/activate-kestrel.service + +# Install and configure systemd which requires dbus for graceful shutdown. +RUN ["apt-get", "-o", "Acquire::Check-Valid-Until=false", "update"] +RUN ["apt-get", "install", "-y", "dbus"] +RUN ["systemctl", "mask", "getty.target", "console-getty.service"] +RUN ["cp", "/lib/systemd/system/dbus.service", "/etc/systemd/system/"] +RUN ["sed", "-i", "s/OOMScoreAdjust=-900//", "/etc/systemd/system/dbus.service"] + +# Automatically start activate-kestrel.service on boot. +RUN ["ln", "-s", "/usr/share/dotnet/dotnet", "/usr/bin/dotnet"] +RUN ["ln", "-s", "/usr/lib/systemd/system/activate-kestrel.service", "/etc/systemd/system/multi-user.target.wants/activate-kestrel.service"] + +# Launch systemd. +CMD ["/sbin/init"] diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.service b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.service new file mode 100644 index 0000000000..392bf823cd --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.service @@ -0,0 +1,8 @@ +[Unit] +Requires=activate-kestrel.socket + +[Service] +ExecStart=/usr/bin/dotnet SampleApp.dll +WorkingDirectory=/publish +NonBlocking=true + diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.socket b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.socket new file mode 100644 index 0000000000..1407c09f88 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/activate-kestrel.socket @@ -0,0 +1,7 @@ +[Unit] +Description=Kestrel Activation + +[Socket] +ListenStream=8080 +NoDelay=true + diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh new file mode 100755 index 0000000000..b450ff3e54 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +# Ensure that dotnet is added to the PATH. +# build.sh should always be run before this script to create the .build/ directory and restore packages. +scriptDir=$(dirname "${BASH_SOURCE[0]}") +repoDir=$(cd $scriptDir/../../.. && pwd) +source ./.build/KoreBuild.sh -r $repoDir --quiet + +dotnet publish -f netcoreapp1.1 ./samples/SampleApp/ +cp -R ./samples/SampleApp/bin/Debug/netcoreapp1.1/publish/ $scriptDir +cp -R ~/.dotnet/ $scriptDir + +image=$(docker build -qf $scriptDir/Dockerfile $scriptDir) +container=$(docker run -Ptd --privileged $image) + +# Try to connect to SampleApp once a second up to 10 times. +for i in {1..10}; do curl $(docker port $container 8080/tcp) && exit 0 || sleep 1; done + +exit -1 diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Writing.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Writing.cs index 07d8c9a3bc..39ac17d1c8 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Writing.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Writing.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -98,7 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance }; var listenerContext = new ListenerContext(serviceContext) { - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000)) }; var connectionContext = new ConnectionContext(listenerContext) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs index 6fe3bc1d6d..29ea2ba050 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs @@ -4,9 +4,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; @@ -16,21 +18,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { public class ChunkedRequestTests { - public static TheoryData ConnectionFilterData + public static TheoryData ConnectionAdapterData => new TheoryData { - get + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) { - return new TheoryData - { - { - new TestServiceContext() - }, - { - new TestServiceContext(new PassThroughConnectionFilter()) - } - }; + ConnectionAdapters = { new PassThroughConnectionAdapter() } } - } + }; private async Task App(HttpContext httpContext) { @@ -61,10 +56,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10TransferEncoding(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10TransferEncoding(ListenOptions listenOptions) { - using (var server = new TestServer(App, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(App, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -88,10 +85,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10KeepAliveTransferEncoding(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10KeepAliveTransferEncoding(ListenOptions listenOptions) { - using (var server = new TestServer(AppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(AppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -127,9 +126,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; @@ -140,7 +141,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -177,9 +178,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task TrailingHeadersAreParsed(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task TrailingHeadersAreParsed(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); var requestCount = 10; var requestsReceived = 0; @@ -211,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { var response = string.Join("\r\n", new string[] { "HTTP/1.1 200 OK", @@ -262,13 +264,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(ListenOptions listenOptions) { const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string headerLine = "Header: value"; const string trailingHeaderLine = "Trailing-Header: trailing-value"; + var testContext = new TestServiceContext(); testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize = transferEncodingHeaderLine.Length + 2 + headerLine.Length + 2 + @@ -278,7 +281,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var buffer = new byte[128]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -305,20 +308,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task TrailingHeadersCountTowardsHeaderCountLimit(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task TrailingHeadersCountTowardsHeaderCountLimit(ListenOptions listenOptions) { const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string headerLine = "Header: value"; const string trailingHeaderLine = "Trailing-Header: trailing-value"; + var testContext = new TestServiceContext(); testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2; using (var server = new TestServer(async context => { var buffer = new byte[128]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -345,9 +349,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ExtensionsAreIgnored(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ExtensionsAreIgnored(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); var requestCount = 10; var requestsReceived = 0; @@ -379,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { var response = string.Join("\r\n", new string[] { "HTTP/1.1 200 OK", @@ -430,9 +435,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task InvalidLengthResultsIn400(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task InvalidLengthResultsIn400(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); using (var server = new TestServer(async httpContext => { var response = httpContext.Response; @@ -448,7 +454,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -472,9 +478,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task InvalidSizedDataResultsIn400(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task InvalidSizedDataResultsIn400(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); using (var server = new TestServer(async httpContext => { var response = httpContext.Response; @@ -490,7 +497,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -516,13 +523,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ChunkedNotFinalTransferCodingResultsIn400(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ChunkedNotFinalTransferCodingResultsIn400(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); using (var server = new TestServer(httpContext => { return TaskCache.CompletedTask; - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedResponseTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedResponseTests.cs index 9bbe7c88a7..ad35e4339a 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedResponseTests.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; using Xunit; @@ -14,32 +16,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { public class ChunkedResponseTests { - public static TheoryData ConnectionFilterData + public static TheoryData ConnectionAdapterData => new TheoryData { - get + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) { - return new TheoryData - { - { - new TestServiceContext() - }, - { - new TestServiceContext(new PassThroughConnectionFilter()) - } - }; + ConnectionAdapters = { new PassThroughConnectionAdapter() } } - } + }; [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ResponsesAreChunkedAutomatically(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ResponsesAreChunkedAutomatically(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -64,14 +61,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -91,14 +90,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -125,15 +126,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { httpContext.Response.Headers["Connection"] = "close"; await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -159,16 +162,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroLengthWritesAreIgnored(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroLengthWritesAreIgnored(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(new byte[0], 0, 0); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -193,9 +198,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroLengthWritesFlushHeaders(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroLengthWritesFlushHeaders(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var flushed = new SemaphoreSlim(0, 1); using (var server = new TestServer(async httpContext => @@ -206,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await flushed.WaitAsync(); await response.WriteAsync("Hello World!"); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -235,14 +242,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; await response.Body.WriteAsync(new byte[0], 0, 0); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -263,15 +272,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ConnectionClosedIfExceptionThrownAfterWrite(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedIfExceptionThrownAfterWrite(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12); throw new Exception(); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -294,15 +305,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; await response.Body.WriteAsync(new byte[0], 0, 0); throw new Exception(); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -324,9 +337,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task WritesAreFlushedPriorToResponseCompletion(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task WritesAreFlushedPriorToResponseCompletion(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var flushWh = new ManualResetEventSlim(); using (var server = new TestServer(async httpContext => @@ -338,7 +353,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests flushWh.Wait(); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -368,9 +383,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ChunksCanBeWrittenManually(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ChunksCanBeWrittenManually(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var response = httpContext.Response; @@ -379,7 +396,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nHello \r\n"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nWorld!\r\n"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionFilterTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionAdapterTests.cs similarity index 69% rename from test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionFilterTests.cs rename to test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionAdapterTests.cs index a27e8a40bd..81fb5629c2 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionFilterTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionAdapterTests.cs @@ -3,26 +3,34 @@ using System; using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Filter; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; +using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Internal; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { - public class ConnectionFilterTests + public class ConnectionAdapterTests { [Fact] - public async Task CanReadAndWriteWithRewritingConnectionFilter() + public async Task CanReadAndWriteWithRewritingConnectionAdapter() { - var filter = new RewritingConnectionFilter(); - var serviceContext = new TestServiceContext(filter); + var adapter = new RewritingConnectionAdapter(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { adapter } + }; + + var serviceContext = new TestServiceContext(); var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?"; - using (var server = new TestServer(TestApp.EchoApp, serviceContext)) + using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -37,15 +45,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } - Assert.Equal(sendString.Length, filter.BytesRead); + Assert.Equal(sendString.Length, adapter.BytesRead); } [Fact] - public async Task CanReadAndWriteWithAsyncConnectionFilter() + public async Task CanReadAndWriteWithAsyncConnectionAdapter() { - var serviceContext = new TestServiceContext(new AsyncConnectionFilter()); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { new AsyncConnectionAdapter() } + }; - using (var server = new TestServer(TestApp.EchoApp, serviceContext)) + var serviceContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -65,15 +78,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Fact] - public async Task ThrowingSynchronousConnectionFilterDoesNotCrashServer() + public async Task ThrowingSynchronousConnectionAdapterDoesNotCrashServer() { - var serviceContext = new TestServiceContext(new ThrowingConnectionFilter()); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { new ThrowingConnectionAdapter() } + }; - using (var server = new TestServer(TestApp.EchoApp, serviceContext)) + var serviceContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions)) { using (var connection = server.CreateConnection()) { - // Will throw because the exception in the connection filter will close the connection. + // Will throw because the exception in the connection adapter will close the connection. await Assert.ThrowsAsync(async () => { await connection.Send( @@ -91,42 +109,50 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } - private class RewritingConnectionFilter : IConnectionFilter + private class RewritingConnectionAdapter : IConnectionAdapter { private RewritingStream _rewritingStream; - public Task OnConnectionAsync(ConnectionFilterContext context) + public Task OnConnectionAsync(ConnectionAdapterContext context) { - _rewritingStream = new RewritingStream(context.Connection); - context.Connection = _rewritingStream; - return TaskCache.CompletedTask; + _rewritingStream = new RewritingStream(context.ConnectionStream); + return Task.FromResult(new AdaptedConnection(_rewritingStream)); } public int BytesRead => _rewritingStream.BytesRead; - } + } - private class AsyncConnectionFilter : IConnectionFilter + private class AsyncConnectionAdapter : IConnectionAdapter { - public async Task OnConnectionAsync(ConnectionFilterContext context) + public async Task OnConnectionAsync(ConnectionAdapterContext context) { - var oldConnection = context.Connection; - - // Set Connection to null to ensure it isn't used until the returned task completes. - context.Connection = null; await Task.Delay(100); - - context.Connection = new RewritingStream(oldConnection); + return new AdaptedConnection(new RewritingStream(context.ConnectionStream)); } } - private class ThrowingConnectionFilter : IConnectionFilter + private class ThrowingConnectionAdapter : IConnectionAdapter { - public Task OnConnectionAsync(ConnectionFilterContext context) + public Task OnConnectionAsync(ConnectionAdapterContext context) { throw new Exception(); } } + private class AdaptedConnection : IAdaptedConnection + { + public AdaptedConnection(Stream adaptedStream) + { + ConnectionStream = adaptedStream; + } + + public Stream ConnectionStream { get; } + + public void PrepareRequest(IFeatureCollection requestFeatures) + { + } + } + private class RewritingStream : Stream { private readonly Stream _innerStream; diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs index 779cc60372..518bb721e5 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs @@ -1,6 +1,7 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests }; var context = new ListenerContext(serviceContext) { - ServerAddress = ServerAddress.FromUrl("http://127.0.0.1:0"), + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), Thread = engine.Threads[0] }; diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/CreateIPEndpointTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/CreateIPEndpointTests.cs index 7a8742e864..a7bb17c55f 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/CreateIPEndpointTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/CreateIPEndpointTests.cs @@ -3,7 +3,6 @@ using System.Net; using Microsoft.AspNetCore.Server.Kestrel; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests @@ -11,14 +10,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public class CreateIPEndpointTests { [Theory] - [InlineData("localhost", "127.0.0.1")] // https://github.com/aspnet/KestrelHttpServer/issues/231 [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 = UvTcpHandle.CreateIPEndpoint(ServerAddress.FromUrl($"http://{host}:5000/")); + 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.KestrelTests/EngineTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs index f6ea43495e..dff37141cb 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; @@ -26,55 +28,47 @@ namespace Microsoft.AspNetCore.Server.KestrelTests /// public class EngineTests { - public static TheoryData ConnectionFilterData + public static TheoryData ConnectionAdapterData => new TheoryData { - get + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) { - return new TheoryData - { - { - new TestServiceContext() - }, - { - new TestServiceContext(new PassThroughConnectionFilter()) - } - }; + ConnectionAdapters = { new PassThroughConnectionAdapter() } } - } + }; - [Theory] - [MemberData(nameof(ConnectionFilterData))] - public void EngineCanStartAndStop(TestServiceContext testContext) + [Fact] + public void EngineCanStartAndStop() { - var engine = new KestrelEngine(testContext); + var engine = new KestrelEngine(new TestServiceContext()); engine.Start(1); engine.Dispose(); } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public void ListenerCanCreateAndDispose(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public void ListenerCanCreateAndDispose(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); testContext.App = TestApp.EchoApp; var engine = new KestrelEngine(testContext); engine.Start(1); - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); - var started = engine.CreateServer(address); + var started = engine.CreateServer(listenOptions); started.Dispose(); engine.Dispose(); } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public void ConnectionCanReadAndWrite(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public void ConnectionCanReadAndWrite(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); testContext.App = TestApp.EchoApp; var engine = new KestrelEngine(testContext); engine.Start(1); - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); - var started = engine.CreateServer(address); + var started = engine.CreateServer(listenOptions); - var socket = TestConnection.CreateConnectedLoopbackSocket(address.Port); + var socket = TestConnection.CreateConnectedLoopbackSocket(listenOptions.IPEndPoint.Port); var data = "Hello World"; socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}")); var buffer = new byte[data.Length]; @@ -89,10 +83,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10RequestReceivesHttp11Response(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10RequestReceivesHttp11Response(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -112,10 +108,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http11(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http11(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -143,9 +141,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task HeadersAndStreamsAreReused(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task HeadersAndStreamsAreReused(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); var streamCount = 0; var requestHeadersCount = 0; var responseHeadersCount = 0; @@ -222,10 +221,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10ContentLength(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10ContentLength(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -245,10 +246,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10KeepAlive(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10KeepAlive(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -278,10 +281,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10KeepAliveNotUsedIfResponseContentLengthNotSet(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10KeepAliveNotUsedIfResponseContentLengthNotSet(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -311,10 +316,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Http10KeepAliveContentLength(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Http10KeepAliveContentLength(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -346,10 +353,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task Expect100ContinueForBody(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task Expect100ContinueForBody(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -376,10 +385,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task DisconnectingClient(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task DisconnectingClient(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) { var socket = TestConnection.CreateConnectedLoopbackSocket(server.Port); await Task.Delay(200); @@ -404,10 +415,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EmptyApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -434,13 +447,15 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(10))); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -476,10 +491,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EmptyApp, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -497,9 +514,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var request = httpContext.Request; @@ -510,7 +529,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var statusString = await reader.ReadLineAsync(); response.StatusCode = int.Parse(statusString); } - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -548,14 +567,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { // This will hang if 0 content length is not assumed by the server Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(10))); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -594,16 +615,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ConnectionClosedAfter101Response(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedAfter101Response(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(async httpContext => { var request = httpContext.Request; var stream = await httpContext.Features.Get().UpgradeAsync(); var response = Encoding.ASCII.GetBytes("hello, world"); await stream.WriteAsync(response, 0, response.Length); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -637,9 +660,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ThrowingResultsIn500Response(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowingResultsIn500Response(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + bool onStartingCalled = false; var testLogger = new TestApplicationErrorLogger(); @@ -657,7 +682,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests // Anything added to the ResponseHeaders dictionary is ignored response.Headers["Content-Length"] = "11"; throw new Exception(); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -687,9 +712,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ThrowingAfterWritingKillsConnection(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowingAfterWritingKillsConnection(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + bool onStartingCalled = false; var testLogger = new TestApplicationErrorLogger(); @@ -707,7 +734,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); throw new Exception(); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -729,9 +756,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ThrowingAfterPartialWriteKillsConnection(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowingAfterPartialWriteKillsConnection(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + bool onStartingCalled = false; var testLogger = new TestApplicationErrorLogger(); @@ -749,7 +778,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5); throw new Exception(); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -770,10 +799,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(1, testLogger.ApplicationErrorsLogged); } - [MemberData(nameof(ConnectionFilterData))] - public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes(ListenOptions listenOptions) { - using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + var testContext = new TestServiceContext(); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -819,9 +850,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var onStartingCallCount1 = 0; var onStartingCallCount2 = 0; var failedWriteCount = 0; @@ -853,7 +886,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Same(onStartingException, writeException.InnerException); failedWriteCount++; - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -884,9 +917,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var onCompletedCalled1 = false; var onCompletedCalled2 = false; @@ -910,7 +945,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -934,9 +969,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task RequestsCanBeAbortedMidRead(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var readTcs = new TaskCompletionSource(); var registrationTcs = new TaskCompletionSource(); var requestId = 0; @@ -975,7 +1012,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests readTcs.SetException(new Exception("This shouldn't be reached.")); } - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1005,9 +1042,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(2, abortedRequestId); } - [MemberData(nameof(ConnectionFilterData))] - public async Task FailedWritesResultInAbortedRequest(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task FailedWritesResultInAbortedRequest(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + // This should match _maxBytesPreCompleted in SocketOutput var maxBytesPreCompleted = 65536; // Ensure string is long enough to disable write-behind buffering @@ -1044,7 +1083,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } writeTcs.SetException(new Exception("This shouldn't be reached.")); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1066,9 +1105,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + var testLogger = new TestApplicationErrorLogger(); testContext.Log = new KestrelTrace(testLogger); @@ -1077,7 +1118,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var response = httpContext.Response; response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1099,14 +1140,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + using (var server = new TestServer(httpContext => { httpContext.Abort(); return TaskCache.CompletedTask; - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1121,9 +1164,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task RequestHeadersAreResetOnEachRequest(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task RequestHeadersAreResetOnEachRequest(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + IHeaderDictionary originalRequestHeaders = null; var firstRequest = true; @@ -1143,7 +1188,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } return TaskCache.CompletedTask; - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1168,9 +1213,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task ResponseHeadersAreResetOnEachRequest(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task ResponseHeadersAreResetOnEachRequest(ListenOptions listenOptions) { + var testContext = new TestServiceContext(); + IHeaderDictionary originalResponseHeaders = null; var firstRequest = true; @@ -1190,7 +1237,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } return TaskCache.CompletedTask; - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1243,11 +1290,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions) { const string response = "hello, world"; + var testContext = new TestServiceContext(); + var callOrder = new Stack(); var onStartingTcs = new TaskCompletionSource(); @@ -1267,7 +1316,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests context.Response.ContentLength = response.Length; await context.Response.WriteAsync(response); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1292,11 +1341,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions) { const string response = "hello, world"; + var testContext = new TestServiceContext(); + var callOrder = new Stack(); var onCompletedTcs = new TaskCompletionSource(); @@ -1316,7 +1367,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests context.Response.ContentLength = response.Length; await context.Response.WriteAsync(response); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -1341,11 +1392,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Theory] - [MemberData(nameof(ConnectionFilterData))] - public async Task UpgradeRequestIsNotKeptAliveOrChunked(TestServiceContext testContext) + [MemberData(nameof(ConnectionAdapterData))] + public async Task UpgradeRequestIsNotKeptAliveOrChunked(ListenOptions listenOptions) { const string message = "Hello World"; + var testContext = new TestServiceContext(); + using (var server = new TestServer(async context => { var upgradeFeature = context.Features.Get(); @@ -1359,7 +1412,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } await duplexStream.WriteAsync(buffer, 0, read); - }, testContext)) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs index bc76f2c09f..55ec8ee48b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Net; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel; @@ -28,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests }; var listenerContext = new ListenerContext(serviceContext) { - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000)) }; var connectionContext = new ConnectionContext(listenerContext); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 318061b832..5bf5fabb26 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests }; var listenerContext = new ListenerContext(_serviceContext) { - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000)) }; _connectionContext = new ConnectionContext(listenerContext) { @@ -440,7 +441,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public void InitializeStreamsResetsStreams() { // Arrange - var messageBody = MessageBody.For(HttpVersion.Http11, (FrameRequestHeaders)_frame.RequestHeaders, _frame); + var messageBody = MessageBody.For(Kestrel.Internal.Http.HttpVersion.Http11, (FrameRequestHeaders)_frame.RequestHeaders, _frame); _frame.InitializeStreams(messageBody); var originalRequestBody = _frame.RequestBody; diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionAdapterTests.cs similarity index 65% rename from test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs rename to test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionAdapterTests.cs index 148260cf21..d37027adc6 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionAdapterTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Net.Http; using System.Net.Security; using System.Net.Sockets; @@ -13,33 +14,33 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Filter; +using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Logging; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { - public class HttpsConnectionFilterTests + public class HttpsConnectionAdapterTests { - private static string _serverAddress = "https://127.0.0.1:0/"; - private static X509Certificate2 _x509Certificate2 = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"); + private static X509Certificate2 _x509Certificate2 = new X509Certificate2("TestResources/testCert.pfx", "testPassword"); // https://github.com/aspnet/KestrelHttpServer/issues/240 // This test currently fails on mono because of an issue with SslStream. [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] - public async Task CanReadAndWriteWithHttpsConnectionFilter() + public async Task CanReadAndWriteWithHttpsConnectionAdapter() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 }, - new NoOpConnectionFilter()) - ); + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = + { + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 }) + } + }; - using (var server = new TestServer(App, serviceContext, _serverAddress)) + using (var server = new TestServer(App, serviceContext, listenOptions)) { var result = await HttpClientSlim.PostAsync($"https://localhost:{server.Port}/", new FormUrlEncodedContent(new[] { @@ -54,16 +55,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task RequireCertificateFailsWhenNoCertificate() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = + { + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2, ClientCertificateMode = ClientCertificateMode.RequireCertificate - }, - new NoOpConnectionFilter()) - ); + }) + } + }; - using (var server = new TestServer(App, serviceContext, _serverAddress)) + + using (var server = new TestServer(App, serviceContext, listenOptions)) { await Assert.ThrowsAnyAsync( () => HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/")); @@ -73,21 +79,25 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task AllowCertificateContinuesWhenNoCertificate() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = ClientCertificateMode.AllowCertificate - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = ClientCertificateMode.AllowCertificate + }) + } + }; using (var server = new TestServer(context => { Assert.Equal(context.Features.Get(), null); return context.Response.WriteAsync("hello world"); }, - serviceContext, _serverAddress)) + serviceContext, listenOptions)) { var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); Assert.Equal("hello world", result); @@ -97,24 +107,24 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact] public void ThrowsWhenNoServerCertificateIsProvided() { - Assert.Throws(() => new HttpsConnectionFilter( - new HttpsConnectionFilterOptions(), - new NoOpConnectionFilter()) + Assert.Throws(() => new HttpsConnectionAdapter( + new HttpsConnectionAdapterOptions()) ); } [Fact] public async Task UsesProvidedServerCertificate() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2 - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 }) + } + }; - using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, _serverAddress)) + using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, listenOptions)) { using (var client = new TcpClient()) { @@ -132,15 +142,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task CertificatePassedToHttpContext() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = ClientCertificateMode.RequireCertificate, - ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = ClientCertificateMode.RequireCertificate, + ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true + }) + } + }; using (var server = new TestServer(context => { @@ -150,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.NotNull(context.Connection.ClientCertificate); return context.Response.WriteAsync("hello world"); }, - serviceContext, _serverAddress)) + serviceContext, listenOptions)) { using (var client = new TcpClient()) { @@ -167,16 +181,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task HttpsSchemePassedToRequestFeature() { - var serviceContext = new TestServiceContext( - new HttpsConnectionFilter( - new HttpsConnectionFilterOptions - { - ServerCertificate = _x509Certificate2 - }, - new NoOpConnectionFilter()) - ); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = + { + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 }) + } + }; + var serviceContext = new TestServiceContext(); - using (var server = new TestServer(context => context.Response.WriteAsync(context.Request.Scheme), serviceContext, _serverAddress)) + using (var server = new TestServer(context => context.Response.WriteAsync(context.Request.Scheme), serviceContext, listenOptions)) { var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); Assert.Equal("https", result); @@ -186,17 +200,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task DoesNotSupportTls10() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = ClientCertificateMode.RequireCertificate, - ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = ClientCertificateMode.RequireCertificate, + ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true + }) + } + }; - using (var server = new TestServer(context => context.Response.WriteAsync("hello world"), serviceContext, _serverAddress)) + using (var server = new TestServer(context => context.Response.WriteAsync("hello world"), serviceContext, listenOptions)) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any @@ -216,23 +234,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public async Task ClientCertificateValidationGetsCalledWithNotNullParameters(ClientCertificateMode mode) { var clientCertificateValidationCalled = false; - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = mode, - ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { - clientCertificateValidationCalled = true; - Assert.NotNull(certificate); - Assert.NotNull(chain); - return true; - } - }, - new NoOpConnectionFilter()) - ); + ServerCertificate = _x509Certificate2, + ClientCertificateMode = mode, + ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => + { + clientCertificateValidationCalled = true; + Assert.NotNull(certificate); + Assert.NotNull(chain); + return true; + } + }) + } + }; - using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, _serverAddress)) + using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, listenOptions)) { using (var client = new TcpClient()) { @@ -249,17 +271,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData(ClientCertificateMode.RequireCertificate)] public async Task ValidationFailureRejectsConnection(ClientCertificateMode mode) { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = mode, - ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => false - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = mode, + ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => false + }) + } + }; - using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, _serverAddress)) + using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, listenOptions)) { using (var client = new TcpClient()) { @@ -275,16 +301,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData(ClientCertificateMode.RequireCertificate)] public async Task RejectsConnectionOnSslPolicyErrorsWhenNoValidation(ClientCertificateMode mode) { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = mode, - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = mode + }) + } + }; - using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, _serverAddress)) + using (var server = new TestServer(context => TaskCache.CompletedTask, serviceContext, listenOptions)) { using (var client = new TcpClient()) { @@ -298,15 +328,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")] public async Task CertificatePassedToHttpContextIsNotDisposed() { - var serviceContext = new TestServiceContext(new HttpsConnectionFilter( - new HttpsConnectionFilterOptions + var serviceContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { - ServerCertificate = _x509Certificate2, - ClientCertificateMode = ClientCertificateMode.RequireCertificate, - ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true - }, - new NoOpConnectionFilter()) - ); + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions + { + ServerCertificate = _x509Certificate2, + ClientCertificateMode = ClientCertificateMode.RequireCertificate, + ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true + }) + } + }; RequestDelegate app = context => { @@ -318,7 +352,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests return context.Response.WriteAsync("hello world"); }; - using (var server = new TestServer(app, serviceContext, _serverAddress)) + using (var server = new TestServer(app, serviceContext, listenOptions)) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs index 540922a009..8b7fa7d599 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs @@ -3,13 +3,14 @@ using System; using System.Linq; +using System.Net; using System.Reflection; using Microsoft.AspNetCore.Server.Kestrel; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { - public class KestrelServerInformationTests + public class KestrelServerOptionsTests { #pragma warning disable CS0618 [Fact] @@ -39,6 +40,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } #pragma warning restore CS0612 + [Fact] + public void NoDelayDefaultsToTrue() + { + var o1 = new KestrelServerOptions(); + o1.Listen(IPAddress.Loopback, 0); + o1.Listen(IPAddress.Loopback, 0, d => + { + d.NoDelay = false; + }); + + Assert.True(o1.ListenOptions[0].NoDelay); + Assert.False(o1.ListenOptions[1].NoDelay); + } + [Fact] public void SetThreadCountUsingProcessorCount() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ListenerPrimaryTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ListenerPrimaryTests.cs index b6afadda5b..43e7b78e24 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ListenerPrimaryTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ListenerPrimaryTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; @@ -55,33 +56,35 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) { - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener var kestrelThreadPrimary = new KestrelThread(kestrelEngine); await kestrelThreadPrimary.StartAsync(); - var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); + var listenerPrimary = new ListenerPrimary(serviceContextPrimary); + await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary); + var address = listenOptions.ToString(); // Until a secondary listener is added, TCP connections get dispatched directly - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); // Add secondary listener var kestrelThreadSecondary = new KestrelThread(kestrelEngine); await kestrelThreadSecondary.StartAsync(); - var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); - await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary); + var listenerSecondary = new ListenerSecondary(serviceContextSecondary); + await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary); // Once a secondary listener is added, TCP connections start getting dispatched to it - await AssertResponseEventually(address.ToString(), "Secondary", allowed: new[] { "Primary" }); + await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" }); // TCP connections will still get round-robined to the primary listener - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); await listenerSecondary.DisposeAsync(); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); @@ -129,25 +132,26 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) { - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener var kestrelThreadPrimary = new KestrelThread(kestrelEngine); await kestrelThreadPrimary.StartAsync(); - var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); + var listenerPrimary = new ListenerPrimary(serviceContextPrimary); + await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary); + var address = listenOptions.ToString(); // Add secondary listener var kestrelThreadSecondary = new KestrelThread(kestrelEngine); await kestrelThreadSecondary.StartAsync(); - var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); - await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary); + var listenerSecondary = new ListenerSecondary(serviceContextSecondary); + await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary); // TCP Connections get round-robined - await AssertResponseEventually(address.ToString(), "Secondary", allowed: new[] { "Primary" }); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); + await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" }); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); // Create a pipe connection and keep it open without sending any data var connectTcs = new TaskCompletionSource(); @@ -183,9 +187,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await connectTcs.Task; // TCP connections will still get round-robined between only the two listeners - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); + Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); await kestrelThreadPrimary.PostAsync(_ => pipe.Dispose(), null); @@ -196,9 +200,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } // Same for after the non-listener pipe connection is closed - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); await listenerSecondary.DisposeAsync(); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); @@ -250,21 +254,22 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) { - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener var kestrelThreadPrimary = new KestrelThread(kestrelEngine); await kestrelThreadPrimary.StartAsync(); - var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); + var listenerPrimary = new ListenerPrimary(serviceContextPrimary); + await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary); + var address = listenOptions.ToString(); // Add secondary listener with wrong pipe message var kestrelThreadSecondary = new KestrelThread(kestrelEngine); await kestrelThreadSecondary.StartAsync(); - var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); - await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), address, kestrelThreadSecondary); + var listenerSecondary = new ListenerSecondary(serviceContextSecondary); + await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), listenOptions, kestrelThreadSecondary); // Wait up to 10 seconds for error to be logged for (var i = 0; i < 10 && primaryTrace.Logger.TotalErrorsLogged == 0; i++) @@ -273,9 +278,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } // TCP Connections don't get round-robined - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); await listenerSecondary.DisposeAsync(); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/MultipleLoopTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/MultipleLoopTests.cs index d5926a14ec..687cf6577e 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/MultipleLoopTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/MultipleLoopTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; @@ -163,8 +164,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var serverListenTcp = new UvTcpHandle(_logger); serverListenTcp.Init(loop, (a, b) => { }); - var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); - serverListenTcp.Bind(address); + var endPoint = new IPEndPoint(IPAddress.Loopback, 0); + serverListenTcp.Bind(endPoint); var port = serverListenTcp.GetSockIPEndPoint().Port; serverListenTcp.Listen(128, (handle, status, error, state) => { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/NetworkingTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/NetworkingTests.cs index ccd320a220..ebc3ebeb22 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/NetworkingTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/NetworkingTests.cs @@ -63,8 +63,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests loop.Init(_uv); var tcp = new UvTcpHandle(_logger); tcp.Init(loop, (a, b) => { }); - var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); - tcp.Bind(address); + var endPoint = new IPEndPoint(IPAddress.Loopback, 0); + tcp.Bind(endPoint); tcp.Dispose(); loop.Run(); loop.Dispose(); @@ -78,8 +78,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests loop.Init(_uv); var tcp = new UvTcpHandle(_logger); tcp.Init(loop, (a, b) => { }); - var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); - tcp.Bind(address); + var endPoint = new IPEndPoint(IPAddress.Loopback, 0); + tcp.Bind(endPoint); var port = tcp.GetSockIPEndPoint().Port; tcp.Listen(10, (stream, status, error, state) => { @@ -107,8 +107,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests loop.Init(_uv); var tcp = new UvTcpHandle(_logger); tcp.Init(loop, (a, b) => { }); - var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); - tcp.Bind(address); + var endPoint = new IPEndPoint(IPAddress.Loopback, 0); + tcp.Bind(endPoint); var port = tcp.GetSockIPEndPoint().Port; tcp.Listen(10, (_, status, error, state) => { @@ -157,8 +157,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests loop.Init(_uv); var tcp = new UvTcpHandle(_logger); tcp.Init(loop, (a, b) => { }); - var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); - tcp.Bind(address); + var endPoint = new IPEndPoint(IPAddress.Loopback, 0); + tcp.Bind(endPoint); var port = tcp.GetSockIPEndPoint().Port; tcp.Listen(10, (_, status, error, state) => { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs index a793cf0269..7a1d479f29 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs @@ -1,9 +1,11 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Testing; using Xunit; @@ -16,6 +18,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var testContext = new TestServiceContext(); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + PathBase = "/\u0041\u030A" + }; + using (var server = new TestServer(async context => { Assert.Equal("/\u0041\u030A", context.Request.PathBase.Value); @@ -23,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests context.Response.Headers["Content-Length"] = new[] { "11" }; await context.Response.WriteAsync("Hello World"); - }, testContext, "http://127.0.0.1:0/\u0041\u030A")) + }, testContext, listenOptions)) { using (var connection = server.CreateConnection()) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/StreamSocketOutputTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/StreamSocketOutputTests.cs index ab8f83e46e..669efdf99e 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/StreamSocketOutputTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/StreamSocketOutputTests.cs @@ -3,7 +3,7 @@ using System; using System.IO; -using Microsoft.AspNetCore.Server.Kestrel.Filter.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Testing; using Xunit; diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionAdapter.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionAdapter.cs new file mode 100644 index 0000000000..2494e4bc5d --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionAdapter.cs @@ -0,0 +1,35 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; +using Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers +{ + public class PassThroughConnectionAdapter : IConnectionAdapter + { + public Task OnConnectionAsync(ConnectionAdapterContext context) + { + var adapted = new AdaptedConnection(new LoggingStream(context.ConnectionStream, new TestApplicationErrorLogger())); + return Task.FromResult(adapted); + } + + private class AdaptedConnection : IAdaptedConnection + { + public AdaptedConnection(Stream stream) + { + ConnectionStream = stream; + } + + public Stream ConnectionStream { get; } + + public void PrepareRequest(IFeatureCollection requestFeatures) + { + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs deleted file mode 100644 index b4492bb8b6..0000000000 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs +++ /dev/null @@ -1,21 +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.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Filter; -using Microsoft.AspNetCore.Server.Kestrel.Filter.Internal; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers -{ - - public class PassThroughConnectionFilter : IConnectionFilter - { - public Task OnConnectionAsync(ConnectionFilterContext context) - { - context.Connection = new LoggingStream(context.Connection, new TestApplicationErrorLogger()); - return TaskCache.CompletedTask; - } - } -} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs index 96d63ca04f..5a798e804b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs @@ -2,13 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests }; var listenerContext = new ListenerContext(serviceContext) { - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000)) }; var connectionContext = new ConnectionContext(listenerContext) { diff --git a/test/shared/MockConnection.cs b/test/shared/MockConnection.cs index eff3da34fb..c68870cde6 100644 --- a/test/shared/MockConnection.cs +++ b/test/shared/MockConnection.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel; @@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Testing RequestAbortedSource = new CancellationTokenSource(); ListenerContext = new ListenerContext(new ServiceContext {ServerOptions = options}) { - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000)) }; } diff --git a/test/shared/TestServer.cs b/test/shared/TestServer.cs index f52cc190d3..480e456bd2 100644 --- a/test/shared/TestServer.cs +++ b/test/shared/TestServer.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; @@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Testing { private KestrelEngine _engine; private IDisposable _server; - private ServerAddress _address; + private ListenOptions _listenOptions; public TestServer(RequestDelegate app) : this(app, new TestServiceContext()) @@ -24,18 +25,24 @@ namespace Microsoft.AspNetCore.Testing } public TestServer(RequestDelegate app, TestServiceContext context) - : this(app, context, "http://127.0.0.1:0/") + : this(app, context, httpContextFactory: null) { } - public TestServer(RequestDelegate app, TestServiceContext context, string serverAddress) - : this(app, context, serverAddress, null) + public TestServer(RequestDelegate app, TestServiceContext context, IHttpContextFactory httpContextFactory) + : this(app, context, new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), httpContextFactory) { } - public TestServer(RequestDelegate app, TestServiceContext context, string serverAddress, IHttpContextFactory httpContextFactory) + public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions listenOptions) + : this(app, context, listenOptions, null) + { + } + + public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions listenOptions, IHttpContextFactory httpContextFactory) { Context = context; + _listenOptions = listenOptions; context.FrameFactory = connectionContext => { @@ -46,8 +53,7 @@ namespace Microsoft.AspNetCore.Testing { _engine = new KestrelEngine(context); _engine.Start(1); - _address = ServerAddress.FromUrl(serverAddress); - _server = _engine.CreateServer(_address); + _server = _engine.CreateServer(_listenOptions); } catch { @@ -57,7 +63,7 @@ namespace Microsoft.AspNetCore.Testing } } - public int Port => _address.Port; + public int Port => _listenOptions.IPEndPoint.Port; public TestServiceContext Context { get; } diff --git a/test/shared/TestServiceContext.cs b/test/shared/TestServiceContext.cs index 9da3e8811b..7214c5e109 100644 --- a/test/shared/TestServiceContext.cs +++ b/test/shared/TestServiceContext.cs @@ -4,7 +4,7 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel; -using Microsoft.AspNetCore.Server.Kestrel.Filter; +using Microsoft.AspNetCore.Server.Kestrel.Adapter; using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; @@ -26,12 +26,6 @@ namespace Microsoft.AspNetCore.Testing ServerOptions.ShutdownTimeout = TimeSpan.FromSeconds(5); } - public TestServiceContext(IConnectionFilter filter) - : this() - { - ServerOptions.ConnectionFilter = filter; - } - public string DateHeaderValue { get; } public RequestDelegate App