diff options
author | Stephen Halter <halter73@gmail.com> | 2020-03-26 03:50:01 +0300 |
---|---|---|
committer | Stephen Halter <halter73@gmail.com> | 2020-03-26 05:40:19 +0300 |
commit | 89a9cbcbb029a21aae3920688d8ac0437199c1d9 (patch) | |
tree | 5db935e649b578ebfc31a114bbfd7c8582390d9f | |
parent | 590a124b5bc218243fbe2e5cbe782f6723f3d930 (diff) |
Test System.Net.Connections interfaces in middlewarehalter73/test-system-net-connections
4 files changed, 415 insertions, 9 deletions
diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index 70bcc5980b..8f44d0db15 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -183,6 +183,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); var factory = _transportFactories.Last(); var transport = await factory.BindAsync(options.EndPoint, options: null).ConfigureAwait(false); + transport = options.BuildConnectionListener(transport); var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport); diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index 16a8e05477..8a1fae3593 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { internal readonly List<Func<ConnectionDelegate, ConnectionDelegate>> _middleware = new List<Func<ConnectionDelegate, ConnectionDelegate>>(); internal readonly List<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>> _multiplexedMiddleware = new List<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>>(); + internal readonly List<Func<System.Net.Connections.IConnectionListener, System.Net.Connections.IConnectionListener>> _listenerFilters = new List<Func<System.Net.Connections.IConnectionListener, System.Net.Connections.IConnectionListener>>(); internal ListenOptions(IPEndPoint endPoint) { @@ -130,6 +131,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core return this; } + public ListenOptions UseListenerFilter(Func<System.Net.Connections.IConnectionListener, System.Net.Connections.IConnectionListener> listenerFilter) + { + _listenerFilters.Add(listenerFilter); + return this; + } + public ConnectionDelegate Build() { ConnectionDelegate app = context => @@ -162,6 +169,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core return app; } + public System.Net.Connections.IConnectionListener BuildConnectionListener(System.Net.Connections.IConnectionListener previous) + { + foreach (var filter in _listenerFilters) + { + previous = filter(previous); + } + + return previous; + } + internal virtual async Task BindAsync(AddressBindContext context) { await AddressBinder.BindEndpointAsync(this, context).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index d664fa5ee0..6aed992049 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -217,15 +217,7 @@ namespace Microsoft.AspNetCore.Hosting var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance; listenOptions.IsTls = true; - listenOptions.Use(next => - { - // Set the list of protocols from listen options - httpsOptions.HttpProtocols = listenOptions.Protocols; - var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory); - return middleware.OnConnectionAsync; - }); - - return listenOptions; + return listenOptions.UseListenerFilter(previous => new HttpsConnectionMiddleware2(previous, httpsOptions, loggerFactory)); } } } diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware2.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware2.cs new file mode 100644 index 0000000000..8a0346c061 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware2.cs @@ -0,0 +1,396 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Connections; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Certificates.Generation; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal +{ + internal class HttpsConnectionMiddleware2 : IConnectionListener + { + private readonly IConnectionListener _previous; + private readonly HttpsConnectionAdapterOptions _options; + private readonly ILogger _logger; + private readonly X509Certificate2 _serverCertificate; + + public HttpsConnectionMiddleware2(IConnectionListener previous, HttpsConnectionAdapterOptions options, ILoggerFactory loggerFactory) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol(). + if (options.HttpProtocols == HttpProtocols.Http2) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + throw new NotSupportedException(CoreStrings.HTTP2NoTlsOsx); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(6, 2)) + { + throw new NotSupportedException(CoreStrings.HTTP2NoTlsWin7); + } + } + + _previous = previous; + // capture the certificate now so it can't be switched after validation + _serverCertificate = options.ServerCertificate; + if (_serverCertificate == null && options.ServerCertificateSelector == null) + { + throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options)); + } + + // If a selector is provided then ignore the cert, it may be a default cert. + if (options.ServerCertificateSelector != null) + { + // SslStream doesn't allow both. + _serverCertificate = null; + } + else + { + EnsureCertificateIsAllowedForServerAuth(_serverCertificate); + } + + _options = options; + _logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware2>(); + } + + + public async ValueTask<IConnection> AcceptAsync(CancellationToken cancellationToken = default) + { + while (true) + { + var connection = await _previous.AcceptAsync(cancellationToken); + + // HttpsConnectionMiddleware2 has already run! + if (connection.ConnectionProperties.TryGet<ITlsConnectionFeature>(out _)) + { + return connection; + } + + try + { + return await AdaptConnectionAsync(connection); + } + catch + { + // Try again. The exception is already logged by AdaptConnectionAsync(). + } + } + } + + public ValueTask DisposeAsync() + { + return _previous.DisposeAsync(); + } + + private async Task<IConnection> AdaptConnectionAsync(IConnection innerConnection) + { + bool certificateRequired; + SslStream sslStream; + Connections.ConnectionContext connectionContextWrapper = null; + + if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) + { + sslStream = new SslStream(innerConnection.Stream); + certificateRequired = false; + } + else + { + sslStream = new SslStream(innerConnection.Stream, + 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; + } + + var sslConnection = new SslConnection(innerConnection, sslStream); + using (var cancellationTokeSource = new CancellationTokenSource(_options.HandshakeTimeout)) + { + try + { + // Adapt to the SslStream signature + ServerCertificateSelectionCallback selector = null; + if (_options.ServerCertificateSelector is object) + { + selector = (sender, name) => + { + connectionContextWrapper = ConvertToConnectionContext(sslConnection); + var cert = _options.ServerCertificateSelector(connectionContextWrapper, name); + if (cert != null) + { + EnsureCertificateIsAllowedForServerAuth(cert); + } + return cert; + }; + } + + var sslOptions = new SslServerAuthenticationOptions + { + ServerCertificate = _serverCertificate, + ServerCertificateSelectionCallback = selector, + ClientCertificateRequired = certificateRequired, + EnabledSslProtocols = _options.SslProtocols, + CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, + ApplicationProtocols = new List<SslApplicationProtocol>() + }; + + // This is order sensitive + if ((_options.HttpProtocols & HttpProtocols.Http2) != 0) + { + sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http2); + // https://tools.ietf.org/html/rfc7540#section-9.2.1 + sslOptions.AllowRenegotiation = false; + } + + if ((_options.HttpProtocols & HttpProtocols.Http1) != 0) + { + sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http11); + } + + _options.OnAuthenticate?.Invoke(connectionContextWrapper ?? ConvertToConnectionContext(sslConnection), sslOptions); + + await sslStream.AuthenticateAsServerAsync(sslOptions, cancellationTokeSource.Token); + } + catch (OperationCanceledException) + { + _logger.LogDebug(2, CoreStrings.AuthenticationTimedOut); + await sslConnection.DisposeAsync(); + throw; + } + catch (IOException ex) + { + _logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed); + await sslConnection.DisposeAsync(); + throw; + } + catch (AuthenticationException ex) + { + if (_serverCertificate == null || + !CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) || + CertificateManager.CheckDeveloperCertificateKey(_serverCertificate)) + { + _logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed); + } + else + { + _logger.LogError(3, ex, CoreStrings.BadDeveloperCertificateState); + } + + await sslConnection.DisposeAsync(); + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unexpected exception!"); + await sslConnection.DisposeAsync(); + } + + return sslConnection; + } + } + + private static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate) + { + if (!CertificateLoader.IsCertificateAllowedForServerAuth(certificate)) + { + throw new InvalidOperationException(CoreStrings.FormatInvalidServerCertificateEku(certificate.Thumbprint)); + } + } + + private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certificate) + { + if (certificate == null) + { + return null; + } + + if (certificate is X509Certificate2 cert2) + { + return cert2; + } + + return new X509Certificate2(certificate); + } + + private static Connections.ConnectionContext ConvertToConnectionContext(IConnection connection) + { + return new SystemNetConnectionsConnectionContext(connection); + } + + private class SslConnection : + IConnection, + IDuplexPipe, + IConnectionProperties, + ITlsConnectionFeature, + ITlsApplicationProtocolFeature, + ITlsHandshakeFeature + { + private readonly IConnection _innerConnection; + private readonly SslStream _sslStream; + private X509Certificate2 _clientCertificate; + + // REVIEW: This is super annoying + private bool _streamAccessed; + + public SslConnection(IConnection innerConnection, SslStream sslStream) + { + _innerConnection = innerConnection; + _sslStream = sslStream; + } + + public EndPoint LocalEndPoint => _innerConnection.LocalEndPoint; + public EndPoint RemoteEndPoint => _innerConnection.RemoteEndPoint; + public IConnectionProperties ConnectionProperties => this; + + public Stream Stream + { + get + { + if (Input is object) + { + throw new InvalidOperationException("IConnection.Stream cannot be accessed after IConnection.Pipe"); + } + + _streamAccessed = true; + return _sslStream; + } + } + + public IDuplexPipe Pipe + { + get + { + if (_streamAccessed) + { + throw new InvalidOperationException("IConnection.Pipe cannot be accessed after IConnection.Stream"); + } + + if (Input is null) + { + Input = PipeReader.Create(_sslStream); + Output = PipeWriter.Create(_sslStream); + } + + return this; + } + } + + public PipeReader Input { get; set; } + public PipeWriter Output { get; set; } + public async ValueTask DisposeAsync() + { + if (Input is object) + { + await Input.CompleteAsync(); + await Output.CompleteAsync(); + } + + await _sslStream.DisposeAsync(); + await _innerConnection.DisposeAsync(); + } + + bool IConnectionProperties.TryGet(Type propertyType, out object property) + { + if (propertyType == typeof(ITlsConnectionFeature) || + propertyType == typeof(ITlsHandshakeFeature) || + propertyType == typeof(ITlsApplicationProtocolFeature)) + { + property = this; + return true; + } + else if (propertyType == typeof(SslStream)) + { + property = _sslStream; + return true; + } + + return _innerConnection.ConnectionProperties.TryGet(propertyType, out property); + } + + // Feature implementations + X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get => _clientCertificate ??= ConvertToX509Certificate2(_sslStream.RemoteCertificate); + set => _clientCertificate = value; + } + + ReadOnlyMemory<byte> ITlsApplicationProtocolFeature.ApplicationProtocol => _sslStream.NegotiatedApplicationProtocol.Protocol; + SslProtocols ITlsHandshakeFeature.Protocol => _sslStream.SslProtocol; + CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => _sslStream.CipherAlgorithm; + int ITlsHandshakeFeature.CipherStrength => _sslStream.CipherStrength; + HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => _sslStream.HashAlgorithm; + int ITlsHandshakeFeature.HashStrength => _sslStream.HashStrength; + ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => _sslStream.KeyExchangeAlgorithm; + int ITlsHandshakeFeature.KeyExchangeStrength => _sslStream.KeyExchangeStrength; + + Task<X509Certificate2> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(((ITlsConnectionFeature)this).ClientCertificate); + } + } + + // REVIEW: This should be in the runtime for people who don't want to implement IDuplexPipe and IConnection + // in the same class. + private class StreamDuplexPipe : IDuplexPipe + { + public StreamDuplexPipe(Stream innerStreawm) + { + Input = PipeReader.Create(innerStreawm); + Output = PipeWriter.Create(innerStreawm); + } + + public PipeReader Input { get; set; } + + public PipeWriter Output { get; set; } + } + } +} |