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.
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.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Https
{
@ -14,8 +16,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
{
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)
{
@ -32,6 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
_options = options;
_previous = previous;
_logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionFilter));
}
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))
{
SslStream sslStream;
bool certificateRequired;
if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate)
{
sslStream = new SslStream(context.Connection);
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, clientCertificateRequired: false,
enabledSslProtocols: _options.SslProtocols, checkCertificateRevocation: _options.CheckCertificateRevocation);
certificateRequired = false;
}
else
{
@ -81,8 +91,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
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;
@ -98,7 +121,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.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.Https;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Hosting
{
@ -83,7 +84,8 @@ namespace Microsoft.AspNetCore.Hosting
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, HttpsConnectionFilterOptions httpsOptions)
{
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;
}
}

View File

@ -146,8 +146,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (!_requestRejected)
{
// SetBadRequestState logs the error.
SetBadRequestState(ex);
Log.LogWarning(0, ex, "Connection processing ended abnormally");
}
}
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.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
@ -32,8 +34,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public async Task CanReadAndWriteWithHttpsConnectionFilter()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
{ ServerCertificate = _x509Certificate2 },
new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 },
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)
{
var request = httpContext.Request;
@ -376,5 +405,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
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>();