Better logging for connection errors during HTTPS handshake

This commit is contained in:
Stephen Halter 2016-07-25 16:44:05 -07:00
parent 7704ddfc7f
commit 1020d69171
5 changed files with 87 additions and 10 deletions

View File

@ -2,11 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Net.Security; using System.Net.Security;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Filter; using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Https namespace Microsoft.AspNetCore.Server.Kestrel.Https
{ {
@ -14,8 +16,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
{ {
private readonly HttpsConnectionFilterOptions _options; private readonly HttpsConnectionFilterOptions _options;
private readonly IConnectionFilter _previous; private readonly IConnectionFilter _previous;
private readonly ILogger _logger;
public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous) public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous)
: this(options, previous, loggerFactory: null)
{
}
public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous, ILoggerFactory loggerFactory)
{ {
if (options == null) if (options == null)
{ {
@ -32,6 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
_options = options; _options = options;
_previous = previous; _previous = previous;
_logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionFilter));
} }
public async Task OnConnectionAsync(ConnectionFilterContext context) public async Task OnConnectionAsync(ConnectionFilterContext context)
@ -41,11 +50,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase)) if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{ {
SslStream sslStream; SslStream sslStream;
bool certificateRequired;
if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate)
{ {
sslStream = new SslStream(context.Connection); sslStream = new SslStream(context.Connection);
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, clientCertificateRequired: false, certificateRequired = false;
enabledSslProtocols: _options.SslProtocols, checkCertificateRevocation: _options.CheckCertificateRevocation);
} }
else else
{ {
@ -81,8 +91,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
return true; return true;
}); });
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, clientCertificateRequired: true,
enabledSslProtocols: _options.SslProtocols, checkCertificateRevocation: _options.CheckCertificateRevocation); certificateRequired = true;
}
context.Connection = sslStream;
try
{
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, certificateRequired,
_options.SslProtocols, _options.CheckCertificateRevocation);
}
catch (IOException ex)
{
_logger?.LogInformation(1, ex, "Failed to authenticate HTTPS connection.");
return;
} }
var previousPrepareRequest = context.PrepareRequest; var previousPrepareRequest = context.PrepareRequest;
@ -98,7 +121,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
features.Get<IHttpRequestFeature>().Scheme = "https"; features.Get<IHttpRequestFeature>().Scheme = "https";
}; };
context.Connection = sslStream;
} }
} }

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Filter; using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Hosting namespace Microsoft.AspNetCore.Hosting
{ {
@ -83,7 +84,8 @@ namespace Microsoft.AspNetCore.Hosting
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, HttpsConnectionFilterOptions httpsOptions) public static KestrelServerOptions UseHttps(this KestrelServerOptions options, HttpsConnectionFilterOptions httpsOptions)
{ {
var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter(); var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter();
options.ConnectionFilter = new HttpsConnectionFilter(httpsOptions, prevFilter); var loggerFactory = options.ApplicationServices.GetRequiredService<ILoggerFactory>();
options.ConnectionFilter = new HttpsConnectionFilter(httpsOptions, prevFilter, loggerFactory);
return options; return options;
} }
} }

View File

@ -146,8 +146,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (!_requestRejected) if (!_requestRejected)
{ {
// SetBadRequestState logs the error.
SetBadRequestState(ex); SetBadRequestState(ex);
Log.LogWarning(0, ex, "Connection processing ended abnormally");
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@ -17,6 +17,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests namespace Microsoft.AspNetCore.Server.KestrelTests
@ -32,8 +34,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public async Task CanReadAndWriteWithHttpsConnectionFilter() public async Task CanReadAndWriteWithHttpsConnectionFilter()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 },
{ ServerCertificate = _x509Certificate2 },
new NoOpConnectionFilter()) new NoOpConnectionFilter())
); );
@ -330,6 +331,34 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
[Fact]
public async Task ClientHandshakeFailureLoggedAsInformation()
{
var logger = new HandshakerErrorLogger();
var mockLoggerFactory = new Mock<ILoggerFactory>(MockBehavior.Strict);
mockLoggerFactory.Setup(m => m.CreateLogger(nameof(HttpsConnectionFilter))).Returns(logger);
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 },
new NoOpConnectionFilter(),
mockLoggerFactory.Object)
);
using (var server = new TestServer(App, serviceContext, _serverAddress))
{
using (await HttpClientSlim.GetSocket(new Uri($"https://localhost:{server.Port}/")))
{
// Close socket immediately
}
await logger.LogTcs.Task;
Assert.Equal(1, logger.LastEventId.Id);
Assert.Equal(LogLevel.Information, logger.LastLogLevel);
mockLoggerFactory.VerifyAll();
}
}
private static async Task App(HttpContext httpContext) private static async Task App(HttpContext httpContext)
{ {
var request = httpContext.Request; var request = httpContext.Request;
@ -376,5 +405,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Null(line); Assert.Null(line);
} }
} }
private class HandshakerErrorLogger : ILogger
{
public LogLevel LastLogLevel { get; set; }
public EventId LastEventId { get; set; }
public TaskCompletionSource<object> LogTcs { get; } = new TaskCompletionSource<object>();
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
LastLogLevel = logLevel;
LastEventId = eventId;
LogTcs.SetResult(null);
}
public bool IsEnabled(LogLevel logLevel)
{
throw new NotImplementedException();
}
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
}
} }
} }

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Testing
} }
} }
private static async Task<Socket> GetSocket(Uri requestUri) public static async Task<Socket> GetSocket(Uri requestUri)
{ {
var tcs = new TaskCompletionSource<Socket>(); var tcs = new TaskCompletionSource<Socket>();