aspnetcore/test/Kestrel.InMemory.Functional.../HttpsTests.cs

463 lines
19 KiB
C#

// 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.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions.Internal;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
public class HttpsTests : LoggedTest
{
private KestrelServerOptions CreateServerOptions()
{
var serverOptions = new KestrelServerOptions();
serverOptions.ApplicationServices = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
return serverOptions;
}
[Fact]
public void UseHttpsDefaultsToDefaultCert()
{
var serverOptions = CreateServerOptions();
var defaultCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
serverOptions.DefaultCertificate = defaultCert;
serverOptions.ListenLocalhost(5000, options =>
{
options.UseHttps();
});
Assert.False(serverOptions.IsDevCertLoaded);
serverOptions.ListenLocalhost(5001, options =>
{
options.UseHttps(opt =>
{
// The default cert is applied after UseHttps.
Assert.Null(opt.ServerCertificate);
});
});
Assert.False(serverOptions.IsDevCertLoaded);
}
[Fact]
public void ConfigureHttpsDefaultsNeverLoadsDefaultCert()
{
var serverOptions = CreateServerOptions();
var testCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
serverOptions.ConfigureHttpsDefaults(options =>
{
Assert.Null(options.ServerCertificate);
options.ServerCertificate = testCert;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
serverOptions.ListenLocalhost(5000, options =>
{
options.UseHttps(opt =>
{
Assert.Equal(testCert, opt.ServerCertificate);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
});
});
// Never lazy loaded
Assert.False(serverOptions.IsDevCertLoaded);
Assert.Null(serverOptions.DefaultCertificate);
}
[Fact]
public void ConfigureCertSelectorNeverLoadsDefaultCert()
{
var serverOptions = CreateServerOptions();
var testCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
serverOptions.ConfigureHttpsDefaults(options =>
{
Assert.Null(options.ServerCertificate);
Assert.Null(options.ServerCertificateSelector);
options.ServerCertificateSelector = (features, name) =>
{
return testCert;
};
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
serverOptions.ListenLocalhost(5000, options =>
{
options.UseHttps(opt =>
{
Assert.Null(opt.ServerCertificate);
Assert.NotNull(opt.ServerCertificateSelector);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
});
});
// Never lazy loaded
Assert.False(serverOptions.IsDevCertLoaded);
Assert.Null(serverOptions.DefaultCertificate);
}
[Fact]
public async Task EmptyRequestLoggedAsDebug()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
{
// Close socket immediately
}
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
}
Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
Assert.True(loggerProvider.ErrorLogger.TotalErrorsLogged == 0,
userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
}
[Fact]
public async Task ClientHandshakeFailureLoggedAsDebug()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
{
// Send null bytes and close socket
await connection.Stream.WriteAsync(new byte[10], 0, 10);
}
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
}
Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
Assert.True(loggerProvider.ErrorLogger.TotalErrorsLogged == 0,
userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
}
// Regression test for https://github.com/aspnet/KestrelHttpServer/issues/1103#issuecomment-246971172
[Fact]
public async Task DoesNotThrowObjectDisposedExceptionOnConnectionAbort()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(async httpContext =>
{
var ct = httpContext.RequestAborted;
while (!ct.IsCancellationRequested)
{
try
{
await httpContext.Response.WriteAsync($"hello, world", ct);
await Task.Delay(1000, ct);
}
catch (TaskCanceledException)
{
// Don't regard connection abort as an error
}
}
},
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12,
checkCertificateRevocation: false);
var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
await sslStream.WriteAsync(request, 0, request.Length);
await sslStream.ReadAsync(new byte[32], 0, 32);
}
}
Assert.False(loggerProvider.ErrorLogger.ObjectDisposedExceptionLogged);
}
[Fact]
public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted()
{
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(async httpContext =>
{
httpContext.Abort();
try
{
await httpContext.Response.WriteAsync($"hello, world");
tcs.SetResult(null);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
},
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12,
checkCertificateRevocation: false);
var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
await sslStream.WriteAsync(request, 0, request.Length);
await sslStream.ReadAsync(new byte[32], 0, 32);
}
await tcs.Task.DefaultTimeout();
}
}
// Regression test for https://github.com/aspnet/KestrelHttpServer/issues/1693
[Fact]
public async Task DoesNotThrowObjectDisposedExceptionOnEmptyConnection()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12,
checkCertificateRevocation: false);
}
}
Assert.False(loggerProvider.ErrorLogger.ObjectDisposedExceptionLogged);
}
// Regression test for https://github.com/aspnet/KestrelHttpServer/pull/1197
[Fact]
public void ConnectionFilterDoesNotLeakBlock()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
{
connection.Reset();
}
}
}
[Fact]
public async Task HandshakeTimesOutAndIsLoggedAsDebug()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
var testContext = new TestServiceContext(LoggerFactory);
var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager);
var handshakeStartedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
TimeSpan handshakeTimeout = default;
using (var server = new TestServer(context => Task.CompletedTask,
testContext,
listenOptions =>
{
listenOptions.UseHttps(o =>
{
o.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
o.OnHandshakeStarted = () => handshakeStartedTcs.SetResult(null);
handshakeTimeout = o.HandshakeTimeout;
});
}))
{
using (var connection = server.CreateConnection())
{
// HttpsConnectionAdapter dispatches via Task.Run() before starting the handshake.
// Wait for the handshake to start before advancing the system clock.
await handshakeStartedTcs.Task.DefaultTimeout();
// Min amount of time between requests that triggers a handshake timeout.
testContext.MockSystemClock.UtcNow += handshakeTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow);
Assert.Equal(0, await connection.Stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout());
}
}
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
}
[Fact]
public async Task ClientAttemptingToUseUnsupportedProtocolIsLoggedAsDebug()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword");
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
// SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
await Assert.ThrowsAsync<IOException>(() =>
sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls,
checkCertificateRevocation: false));
}
}
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
Assert.Equal(1, loggerProvider.FilterLogger.LastEventId);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
}
private class HandshakeErrorLoggerProvider : ILoggerProvider
{
public HttpsConnectionFilterLogger FilterLogger { get; } = new HttpsConnectionFilterLogger();
public ApplicationErrorLogger ErrorLogger { get; } = new ApplicationErrorLogger();
public ILogger CreateLogger(string categoryName)
{
if (categoryName == nameof(HttpsConnectionAdapter))
{
return FilterLogger;
}
else
{
return ErrorLogger;
}
}
public void Dispose()
{
}
}
private class HttpsConnectionFilterLogger : ILogger
{
public LogLevel LastLogLevel { get; set; }
public EventId LastEventId { get; set; }
public TaskCompletionSource<object> LogTcs { get; } = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
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();
}
}
private class ApplicationErrorLogger : ILogger
{
private List<string> _errorMessages = new List<string>();
public IEnumerable<string> ErrorMessages => _errorMessages;
public int TotalErrorsLogged => _errorMessages.Count;
public bool ObjectDisposedExceptionLogged { get; set; }
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel == LogLevel.Error)
{
var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}";
_errorMessages.Add(log);
}
if (exception is ObjectDisposedException)
{
ObjectDisposedExceptionLogged = true;
}
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
return NullScope.Instance;
}
}
}
}