Create a direct way to configure Kestrel endpoints

- Replace endpoint configuration via .UseUrls() or --server.urls with Listen*
  methods on KestrelSerrverOptions.
- Replace IConnectionFilter with IConnectionAdapter which no longer exposes
  ServerAddress via a context.
- Simplify libuv Listener classes
- Support systemd socket activation
- Add docker-based test for systemd socket activation to be run on Travis
This commit is contained in:
Stephen Halter 2017-01-04 10:21:21 -08:00
parent b46e48fe01
commit 2351c1b558
83 changed files with 2088 additions and 1512 deletions

View File

@ -1,6 +1,8 @@
language: csharp language: csharp
sudo: required sudo: required
dist: trusty dist: trusty
services:
- docker
addons: addons:
apt: apt:
packages: 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 - 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: script:
- ./build.sh --quiet verify - ./build.sh --quiet verify
- if test "$TRAVIS_OS_NAME" != "osx"; then bash test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh; fi

View File

@ -2,6 +2,7 @@
// 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.IO; using System.IO;
using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -40,8 +41,10 @@ namespace LargeResponseApp
public static void Main(string[] args) public static void Main(string[] args)
{ {
var host = new WebHostBuilder() var host = new WebHostBuilder()
.UseKestrel() .UseKestrel(options =>
.UseUrls("http://localhost:5001/") {
options.Listen(IPAddress.Loopback, 5001);
})
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>() .UseStartup<Startup>()
.Build(); .Build();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -33,24 +34,33 @@ namespace SampleApp
public static void Main(string[] args) public static void Main(string[] args)
{ {
var host = new WebHostBuilder() var hostBuilder = new WebHostBuilder().UseKestrel(options =>
.UseKestrel(options => {
options.Listen(IPAddress.Loopback, 5000, listenOptions =>
{ {
// options.ThreadCount = 4; // Uncomment the following to enable Nagle's algorithm for this endpoint.
options.NoDelay = true; //listenOptions.NoDelay = false;
options.UseHttps("testCert.pfx", "testPassword");
options.UseConnectionLogging();
})
.UseUrls("http://localhost:5000", "https://localhost:5001")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
// The following section should be used to demo sockets listenOptions.UseConnectionLogging();
//var addresses = application.GetAddresses(); });
//addresses.Clear(); options.Listen(IPAddress.Loopback, 5001, listenOptions =>
//addresses.Add("http://unix:/tmp/kestrel-test.sock"); {
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<Startup>();
var host = hostBuilder.Build();
host.Run(); host.Run();
} }
} }

View File

@ -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<IAdaptedConnection> 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<ITlsConnectionFeature>(new TlsConnectionFeature { ClientCertificate = clientCertificate });
}
requestFeatures.Get<IHttpRequestFeature>().Scheme = "https";
}
}
private class ClosedAdaptedConnection : IAdaptedConnection
{
public Stream ConnectionStream { get; } = new ClosedStream();
public void PrepareRequest(IFeatureCollection requestFeatures)
{
}
}
}
}

View File

@ -8,9 +8,9 @@ using System.Security.Cryptography.X509Certificates;
namespace Microsoft.AspNetCore.Server.Kestrel.Https namespace Microsoft.AspNetCore.Server.Kestrel.Https
{ {
public class HttpsConnectionFilterOptions public class HttpsConnectionAdapterOptions
{ {
public HttpsConnectionFilterOptions() public HttpsConnectionAdapterOptions()
{ {
ClientCertificateMode = ClientCertificateMode.NoCertificate; ClientCertificateMode = ClientCertificateMode.NoCertificate;
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11; SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;

View File

@ -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<ITlsConnectionFeature>(new TlsConnectionFeature { ClientCertificate = clientCertificate });
}
features.Get<IHttpRequestFeature>().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
}
}
}

View File

@ -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
{
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="options">
/// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure.
/// </param>
/// <param name="fileName">
/// The name of a certificate file, relative to the directory that contains the application content files.
/// </param>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, string fileName)
{
var env = options.ApplicationServices.GetRequiredService<IHostingEnvironment>();
return options.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName)));
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="options">
/// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure.
/// </param>
/// <param name="fileName">
/// The name of a certificate file, relative to the directory that contains the application content files.
/// </param>
/// <param name="password">
/// The password required to access the X.509 certificate data.
/// </param>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, string fileName, string password)
{
var env = options.ApplicationServices.GetRequiredService<IHostingEnvironment>();
return options.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password));
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="options">
/// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure.
/// </param>
/// <param name="serverCertificate">
/// The X.509 certificate.
/// </param>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, X509Certificate2 serverCertificate)
{
return options.UseHttps(new HttpsConnectionFilterOptions { ServerCertificate = serverCertificate });
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="options">
/// The Microsoft.AspNetCore.Server.KestrelServerOptions to configure.
/// </param>
/// <param name="httpsOptions">
/// Options to configure HTTPS.
/// </param>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseHttps(this KestrelServerOptions options, HttpsConnectionFilterOptions httpsOptions)
{
var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter();
var loggerFactory = options.ApplicationServices.GetRequiredService<ILoggerFactory>();
options.ConnectionFilter = new HttpsConnectionFilter(httpsOptions, prevFilter, loggerFactory);
return options;
}
}
}

View File

@ -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
{
/// <summary>
/// Extension methods fro <see cref="ListenOptions"/> that configure Kestrel to use HTTPS for a given endpoint.
/// </summary>
public static class ListenOptionsHttpsExtensions
{
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="listenOptions">
/// The <see cref="ListenOptions"/> to configure.
/// </param>
/// <param name="fileName">
/// The name of a certificate file, relative to the directory that contains the application content files.
/// </param>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName)
{
var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<IHostingEnvironment>();
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName)));
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="listenOptions">
/// The <see cref="ListenOptions"/> to configure.
/// </param>
/// <param name="fileName">
/// The name of a certificate file, relative to the directory that contains the application content files.
/// </param>
/// <param name="password">
/// The password required to access the X.509 certificate data.
/// </param>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password)
{
var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<IHostingEnvironment>();
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password));
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="listenOptions">
/// The <see cref="ListenOptions"/> to configure.
/// </param>
/// <param name="serverCertificate">
/// The X.509 certificate.
/// </param>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate)
{
return listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = serverCertificate });
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="listenOptions">
/// The <see cref="ListenOptions"/> to configure.
/// </param>
/// <param name="httpsOptions">
/// Options to configure HTTPS.
/// </param>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions)
{
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
listenOptions.ConnectionAdapters.Add(new HttpsConnectionAdapter(httpsOptions, loggerFactory));
return listenOptions;
}
}
}

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context);
}
}

View File

@ -7,13 +7,13 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; 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; private readonly Stream _filteredStream;
public FilteredStreamAdapter( public AdaptedPipeline(
string connectionId, string connectionId,
Stream filteredStream, Stream filteredStream,
MemoryPool memory, MemoryPool memory,

View File

@ -8,7 +8,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal
{ {
internal class LoggingStream : Stream internal class LoggingStream : Stream
{ {

View File

@ -8,16 +8,16 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.Extensions.Internal; 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 SocketInput _input;
private readonly ISocketOutput _output; private readonly ISocketOutput _output;
private Task<int> _cachedTask = TaskCache<int>.DefaultCompletedTask; private Task<int> _cachedTask = TaskCache<int>.DefaultCompletedTask;
public LibuvStream(SocketInput input, ISocketOutput output) public RawStream(SocketInput input, ISocketOutput output)
{ {
_input = input; _input = input;
_output = output; _output = output;

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Filter.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal
{ {
public class StreamSocketOutput : ISocketOutput public class StreamSocketOutput : ISocketOutput
{ {

View File

@ -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
{
/// <summary>
/// Emits verbose logs for bytes read from and written to the connection.
/// </summary>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions)
{
return listenOptions.UseConnectionLogging(nameof(LoggingConnectionAdapter));
}
/// <summary>
/// Emits verbose logs for bytes read from and written to the connection.
/// </summary>
/// <returns>
/// The <see cref="ListenOptions"/>.
/// </returns>
public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions, string loggerName)
{
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionAdapter));
listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(logger));
return listenOptions;
}
}
}

View File

@ -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<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
{
return Task.FromResult<IAdaptedConnection>(
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)
{
}
}
}
}

View File

@ -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<IFeatureCollection> PrepareRequest { get; set; }
}
}

View File

@ -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);
}
}

View File

@ -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
{
/// <summary>
/// Emits verbose logs for bytes read from and written to the connection.
/// </summary>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseConnectionLogging(this KestrelServerOptions options)
{
return options.UseConnectionLogging(nameof(LoggingConnectionFilter));
}
/// <summary>
/// Emits verbose logs for bytes read from and written to the connection.
/// </summary>
/// <returns>
/// The Microsoft.AspNetCore.Server.KestrelServerOptions.
/// </returns>
public static KestrelServerOptions UseConnectionLogging(this KestrelServerOptions options, string loggerName)
{
var prevFilter = options.ConnectionFilter ?? new NoOpConnectionFilter();
var loggerFactory = options.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionFilter));
options.ConnectionFilter = new LoggingConnectionFilter(logger, prevFilter);
return options;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -2,12 +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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Filter; using Microsoft.AspNetCore.Server.Kestrel.Adapter;
using Microsoft.AspNetCore.Server.Kestrel.Filter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private static readonly Action<UvStreamHandle, int, object> _readCallback = private static readonly Action<UvStreamHandle, int, object> _readCallback =
(handle, status, state) => ReadCallback(handle, status, state); (handle, status, state) => ReadCallback(handle, status, state);
private static readonly Func<UvStreamHandle, int, object, Libuv.uv_buf_t> _allocCallback = private static readonly Func<UvStreamHandle, int, object, Libuv.uv_buf_t> _allocCallback =
(handle, suggestedsize, state) => AllocCallback(handle, suggestedsize, state); (handle, suggestedsize, state) => AllocCallback(handle, suggestedsize, state);
@ -32,9 +34,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private readonly UvStreamHandle _socket; private readonly UvStreamHandle _socket;
private readonly Frame _frame; private readonly Frame _frame;
private ConnectionFilterContext _filterContext; private readonly List<IConnectionAdapter> _connectionAdapters;
private LibuvStream _libuvStream; private AdaptedPipeline _adaptedPipeline;
private FilteredStreamAdapter _filteredStreamAdapter; private Stream _filteredStream;
private Task _readInputTask; private Task _readInputTask;
private TaskCompletionSource<object> _socketClosedTcs = new TaskCompletionSource<object>(); private TaskCompletionSource<object> _socketClosedTcs = new TaskCompletionSource<object>();
@ -47,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public Connection(ListenerContext context, UvStreamHandle socket) : base(context) public Connection(ListenerContext context, UvStreamHandle socket) : base(context)
{ {
_socket = socket; _socket = socket;
_connectionAdapters = context.ListenOptions.ConnectionAdapters;
socket.Connection = this; socket.Connection = this;
ConnectionControl = this; ConnectionControl = this;
@ -80,57 +83,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private Func<ConnectionContext, Frame> FrameFactory => ListenerContext.ServiceContext.FrameFactory; private Func<ConnectionContext, Frame> FrameFactory => ListenerContext.ServiceContext.FrameFactory;
private IKestrelTrace Log => ListenerContext.ServiceContext.Log; private IKestrelTrace Log => ListenerContext.ServiceContext.Log;
private IThreadPool ThreadPool => ListenerContext.ServiceContext.ThreadPool; private IThreadPool ThreadPool => ListenerContext.ServiceContext.ThreadPool;
private ServerAddress ServerAddress => ListenerContext.ServerAddress;
private KestrelThread Thread => ListenerContext.Thread; private KestrelThread Thread => ListenerContext.Thread;
public void Start() public void Start()
{ {
Log.ConnectionStart(ConnectionId); Log.ConnectionStart(ConnectionId);
// Start socket prior to applying the ConnectionFilter // Start socket prior to applying the ConnectionAdapter
_socket.ReadStart(_allocCallback, _readCallback, this); _socket.ReadStart(_allocCallback, _readCallback, this);
if (ServerOptions.ConnectionFilter == null) if (_connectionAdapters.Count == 0)
{ {
_frame.Start(); _frame.Start();
} }
else else
{ {
_libuvStream = new LibuvStream(Input, Output); // ApplyConnectionAdaptersAsync should never throw. If it succeeds, it will call _frame.Start().
// Otherwise, it will close the connection.
_filterContext = new ConnectionFilterContext var ignore = ApplyConnectionAdaptersAsync();
{
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);
}
} }
} }
@ -161,13 +131,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
var connection = (Connection)state; 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; var connection2 = (Connection)state2;
connection2._filterContext.Connection.Dispose(); connection2._filteredStream.Dispose();
connection2._filteredStreamAdapter.Dispose(); connection2._adaptedPipeline.Dispose();
}, connection); }, connection);
} }
}, this); }, this);
@ -187,31 +157,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
_frame.SetBadRequestState(RequestRejectionReason.RequestTimeout); _frame.SetBadRequestState(RequestRejectionReason.RequestTimeout);
} }
StopAsync(); StopAsync();
} }
Interlocked.Exchange(ref _lastTimestamp, timestamp); 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; for (var i = 0; i < _connectionAdapters.Count; i++)
_frame.Output = _filteredStreamAdapter.SocketOutput; {
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. if (adapterContext.ConnectionStream != rawStream)
// This can happen if a client opens a connection and immediately closes it. {
_readInputTask = _socketClosedTcs.Task.Status == TaskStatus.WaitingForActivation ? _filteredStream = adapterContext.ConnectionStream;
_filteredStreamAdapter.ReadInputAsync() : _adaptedPipeline = new AdaptedPipeline(ConnectionId, adapterContext.ConnectionStream,
TaskCache.CompletedTask; 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) private static Libuv.uv_buf_t AllocCallback(UvStreamHandle handle, int suggestedSize, object state)

View File

@ -31,7 +31,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public IPEndPoint LocalEndPoint { get; set; } public IPEndPoint LocalEndPoint { get; set; }
public string ConnectionId { get; set; } public string ConnectionId { get; set; }
public Action<IFeatureCollection> PrepareRequest { get; set; }
} }
} }

View File

@ -13,6 +13,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Adapter;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ServerOptions = context.ListenerContext.ServiceContext.ServerOptions; ServerOptions = context.ListenerContext.ServiceContext.ServerOptions;
_pathBase = ServerAddress.PathBase; _pathBase = context.ListenerContext.ListenOptions.PathBase;
FrameControl = this; FrameControl = this;
_keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds; _keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds;
@ -97,23 +98,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public ConnectionContext ConnectionContext { get; } public ConnectionContext ConnectionContext { get; }
public SocketInput Input { get; set; } public SocketInput Input { get; set; }
public ISocketOutput Output { get; set; } public ISocketOutput Output { get; set; }
public Action<IFeatureCollection> PrepareRequest public IEnumerable<IAdaptedConnection> AdaptedConnections { get; set; }
{
get
{
return ConnectionContext.PrepareRequest;
}
set
{
ConnectionContext.PrepareRequest = value;
}
}
protected IConnectionControl ConnectionControl => ConnectionContext.ConnectionControl; protected IConnectionControl ConnectionControl => ConnectionContext.ConnectionControl;
protected IKestrelTrace Log => ConnectionContext.ListenerContext.ServiceContext.Log; protected IKestrelTrace Log => ConnectionContext.ListenerContext.ServiceContext.Log;
private DateHeaderValueManager DateHeaderValueManager => ConnectionContext.ListenerContext.ServiceContext.DateHeaderValueManager; 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 // Hold direct reference to ServerOptions since this is used very often in the request processing path
private KestrelServerOptions ServerOptions { get; } private KestrelServerOptions ServerOptions { get; }
private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint; private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint;
@ -367,7 +357,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
LocalPort = LocalEndPoint?.Port ?? 0; LocalPort = LocalEndPoint?.Port ?? 0;
ConnectionIdFeature = ConnectionId; 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; _manuallySetRequestAbortToken = null;
_abortedCts = null; _abortedCts = null;

View File

@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
/// <summary> /// <summary>
/// Base class for listeners in Kestrel. Listens for incoming connections /// Base class for listeners in Kestrel. Listens for incoming connections
/// </summary> /// </summary>
public abstract class Listener : ListenerContext, IAsyncDisposable public class Listener : ListenerContext, IAsyncDisposable
{ {
private bool _closed; private bool _closed;
protected Listener(ServiceContext serviceContext) public Listener(ServiceContext serviceContext)
: base(serviceContext) : base(serviceContext)
{ {
} }
@ -26,20 +26,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public IKestrelTrace Log => ServiceContext.Log; public IKestrelTrace Log => ServiceContext.Log;
public Task StartAsync( public Task StartAsync(
ServerAddress address, ListenOptions listenOptions,
KestrelThread thread) KestrelThread thread)
{ {
ServerAddress = address; ListenOptions = listenOptions;
Thread = thread; Thread = thread;
var tcs = new TaskCompletionSource<int>(this); var tcs = new TaskCompletionSource<int>(this);
Thread.Post(state => Thread.Post(state =>
{ {
var tcs2 = (TaskCompletionSource<int>)state; var tcs2 = (TaskCompletionSource<int>) state;
try try
{ {
var listener = ((Listener)tcs2.Task.AsyncState); var listener = ((Listener) tcs2.Task.AsyncState);
listener.ListenSocket = listener.CreateListenSocket(); listener.ListenSocket = listener.CreateListenSocket();
ListenSocket.Listen(Constants.ListenBacklog, ConnectionCallback, this); ListenSocket.Listen(Constants.ListenBacklog, ConnectionCallback, this);
tcs2.SetResult(0); tcs2.SetResult(0);
@ -56,11 +56,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
/// <summary> /// <summary>
/// Creates the socket used to listen for incoming connections /// Creates the socket used to listen for incoming connections
/// </summary> /// </summary>
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) private static void ConnectionCallback(UvStreamHandle stream, int status, Exception error, object state)
{ {
var listener = (Listener)state; var listener = (Listener) state;
if (error != null) if (error != null)
{ {
@ -77,7 +127,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
/// </summary> /// </summary>
/// <param name="listenSocket">Socket being used to listen on</param> /// <param name="listenSocket">Socket being used to listen on</param>
/// <param name="status">Connection status</param> /// <param name="status">Connection status</param>
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) protected virtual void DispatchConnection(UvStreamHandle socket)
{ {

View File

@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
public class ListenerContext public class ListenerContext
@ -12,10 +15,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public ServiceContext ServiceContext { get; set; } public ServiceContext ServiceContext { get; set; }
public ServerAddress ServerAddress { get; set; } public ListenOptions ListenOptions { get; set; }
public KestrelThread Thread { get; set; } public KestrelThread Thread { get; set; }
public KestrelServerOptions ServerOptions => ServiceContext.ServerOptions; /// <summary>
/// Creates a socket which can be used to accept an incoming connection.
/// </summary>
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();
}
}
} }
} }

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
/// A primary listener waits for incoming connections on a specified socket. Incoming /// A primary listener waits for incoming connections on a specified socket. Incoming
/// connections may be passed to a secondary listener to handle. /// connections may be passed to a secondary listener to handle.
/// </summary> /// </summary>
public abstract class ListenerPrimary : Listener public class ListenerPrimary : Listener
{ {
private readonly List<UvPipeHandle> _dispatchPipes = new List<UvPipeHandle>(); private readonly List<UvPipeHandle> _dispatchPipes = new List<UvPipeHandle>();
private int _dispatchIndex; private int _dispatchIndex;
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// but it has no other functional significance // but it has no other functional significance
private readonly ArraySegment<ArraySegment<byte>> _dummyMessage = new ArraySegment<ArraySegment<byte>>(new[] { new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 }) }); private readonly ArraySegment<ArraySegment<byte>> _dummyMessage = new ArraySegment<ArraySegment<byte>>(new[] { new ArraySegment<byte>(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( public async Task StartAsync(
string pipeName, string pipeName,
byte[] pipeMessage, byte[] pipeMessage,
ServerAddress address, ListenOptions listenOptions,
KestrelThread thread) KestrelThread thread)
{ {
_pipeName = pipeName; _pipeName = pipeName;
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false); Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false);
} }
await StartAsync(address, thread).ConfigureAwait(false); await StartAsync(listenOptions, thread).ConfigureAwait(false);
await Thread.PostAsync(state => ((ListenerPrimary)state).PostCallback(), await Thread.PostAsync(state => ((ListenerPrimary)state).PostCallback(),
this).ConfigureAwait(false); this).ConfigureAwait(false);

View File

@ -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 /// A secondary listener is delegated requests from a primary listener via a named pipe or
/// UNIX domain socket. /// UNIX domain socket.
/// </summary> /// </summary>
public abstract class ListenerSecondary : ListenerContext, IAsyncDisposable public class ListenerSecondary : ListenerContext, IAsyncDisposable
{ {
private string _pipeName; private string _pipeName;
private byte[] _pipeMessage; private byte[] _pipeMessage;
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private Libuv.uv_buf_t _buf; private Libuv.uv_buf_t _buf;
private bool _closed; private bool _closed;
protected ListenerSecondary(ServiceContext serviceContext) : base(serviceContext) public ListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
{ {
_ptr = Marshal.AllocHGlobal(4); _ptr = Marshal.AllocHGlobal(4);
} }
@ -35,14 +35,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public Task StartAsync( public Task StartAsync(
string pipeName, string pipeName,
byte[] pipeMessage, byte[] pipeMessage,
ServerAddress address, ListenOptions listenOptions,
KestrelThread thread) KestrelThread thread)
{ {
_pipeName = pipeName; _pipeName = pipeName;
_pipeMessage = pipeMessage; _pipeMessage = pipeMessage;
_buf = thread.Loop.Libuv.buf_init(_ptr, 4); _buf = thread.Loop.Libuv.buf_init(_ptr, 4);
ServerAddress = address; ListenOptions = listenOptions;
Thread = thread; Thread = thread;
DispatchPipe = new UvPipeHandle(Log); DispatchPipe = new UvPipeHandle(Log);
@ -173,11 +173,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
/// <summary>
/// Creates a socket which can be used to accept an incoming connection
/// </summary>
protected abstract UvStreamHandle CreateAcceptSocket();
private void FreeBuffer() private void FreeBuffer()
{ {
var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero); var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero);

View File

@ -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
{
/// <summary>
/// Implementation of <see cref="Listener"/> that uses UNIX domain sockets as its transport.
/// </summary>
public class PipeListener : Listener
{
public PipeListener(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates the socket used to listen for incoming connections
/// </summary>
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;
}
/// <summary>
/// Handles an incoming connection
/// </summary>
/// <param name="listenSocket">Socket being used to listen on</param>
/// <param name="status">Connection status</param>
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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// An implementation of <see cref="ListenerPrimary"/> using UNIX sockets.
/// </summary>
public class PipeListenerPrimary : ListenerPrimary
{
public PipeListenerPrimary(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates the socket used to listen for incoming connections
/// </summary>
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;
}
/// <summary>
/// Handles an incoming connection
/// </summary>
/// <param name="listenSocket">Socket being used to listen on</param>
/// <param name="status">Connection status</param>
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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// An implementation of <see cref="ListenerSecondary"/> using UNIX sockets.
/// </summary>
public class PipeListenerSecondary : ListenerSecondary
{
public PipeListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates a socket which can be used to accept an incoming connection
/// </summary>
protected override UvStreamHandle CreateAcceptSocket()
{
var acceptSocket = new UvPipeHandle(Log);
acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle, false);
return acceptSocket;
}
}
}

View File

@ -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
{
/// <summary>
/// Implementation of <see cref="Listener"/> that uses TCP sockets as its transport.
/// </summary>
public class TcpListener : Listener
{
public TcpListener(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates the socket used to listen for incoming connections
/// </summary>
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;
}
/// <summary>
/// Handle an incoming connection
/// </summary>
/// <param name="listenSocket">Socket being used to listen on</param>
/// <param name="status">Connection status</param>
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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// An implementation of <see cref="ListenerPrimary"/> using TCP sockets.
/// </summary>
public class TcpListenerPrimary : ListenerPrimary
{
public TcpListenerPrimary(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates the socket used to listen for incoming connections
/// </summary>
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;
}
/// <summary>
/// Handles an incoming connection
/// </summary>
/// <param name="listenSocket">Socket being used to listen on</param>
/// <param name="status">Connection status</param>
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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// An implementation of <see cref="ListenerSecondary"/> using TCP sockets.
/// </summary>
public class TcpListenerSecondary : ListenerSecondary
{
public TcpListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
{
}
/// <summary>
/// Creates a socket which can be used to accept an incoming connection
/// </summary>
protected override UvStreamHandle CreateAcceptSocket()
{
var acceptSocket = new UvTcpHandle(Log);
acceptSocket.Init(Thread.Loop, Thread.QueueCloseHandle);
acceptSocket.NoDelay(ServerOptions.NoDelay);
return acceptSocket;
}
}
}

View File

@ -19,6 +19,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// </summary> /// </summary>
public const string UnixPipeHostPrefix = "unix:/"; public const string UnixPipeHostPrefix = "unix:/";
/// <summary>
/// Prefix of host name used to specify pipe file descriptor in the configuration.
/// </summary>
public const string PipeDescriptorPrefix = "pipefd:";
/// <summary>
/// Prefix of host name used to specify socket descriptor in the configuration.
/// </summary>
public const string SocketDescriptorPrefix = "sockfd:";
public const string ServerName = "Kestrel"; public const string ServerName = "Kestrel";
private static int? GetECONNRESET() private static int? GetECONNRESET()

View File

@ -61,49 +61,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
#endif #endif
} }
public IDisposable CreateServer(ServerAddress address) public IDisposable CreateServer(ListenOptions listenOptions)
{ {
var listeners = new List<IAsyncDisposable>(); var listeners = new List<IAsyncDisposable>();
var usingPipes = address.IsUnixPipe;
try try
{ {
var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); if (Threads.Count == 1)
var pipeMessage = Guid.NewGuid().ToByteArray();
var single = Threads.Count == 1;
var first = true;
foreach (var thread in Threads)
{ {
if (single) var listener = new Listener(ServiceContext);
{ listeners.Add(listener);
var listener = usingPipes ? listener.StartAsync(listenOptions, Threads[0]).Wait();
(Listener) new PipeListener(ServiceContext) : }
new TcpListener(ServiceContext); else
listeners.Add(listener); {
listener.StartAsync(address, thread).Wait(); var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
} var pipeMessage = Guid.NewGuid().ToByteArray();
else if (first)
{
var listener = usingPipes
? (ListenerPrimary) new PipeListenerPrimary(ServiceContext)
: new TcpListenerPrimary(ServiceContext);
listeners.Add(listener); var listenerPrimary = new ListenerPrimary(ServiceContext);
listener.StartAsync(pipeName, pipeMessage, address, thread).Wait(); listeners.Add(listenerPrimary);
} listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, Threads[0]).Wait();
else
{
var listener = usingPipes
? (ListenerSecondary) new PipeListenerSecondary(ServiceContext)
: new TcpListenerSecondary(ServiceContext);
listeners.Add(listener);
listener.StartAsync(pipeName, pipeMessage, address, thread).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(() => return new Disposable(() =>
@ -114,7 +98,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
catch catch
{ {
DisposeListeners(listeners); DisposeListeners(listeners);
throw; throw;
} }
} }

View File

@ -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 // 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. // 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. // 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 // 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: // Reference:
// - Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms740506(v=vs.85).aspx // - 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: 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 // - 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 // 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; *((long*)(b + 8)) = _field2;
} }
return new IPEndPoint(new IPAddress(bytes), port); return new IPEndPoint(new IPAddress(bytes, scopeid: _field3 & 0xFFFFFFFF), port);
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Diagnostics;
using System.Net; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
@ -24,20 +25,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_uv.tcp_init(loop, this); _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; SockAddr addr;
var addressText = endpoint.Address.ToString(); var addressText = endPoint.Address.ToString();
Exception error1; 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) if (error1 != null)
{ {
Exception error2; 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) if (error2 != null)
{ {
throw error1; throw error1;
@ -65,38 +69,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
return socketAddress.GetIPEndPoint(); return socketAddress.GetIPEndPoint();
} }
public void Open(IntPtr hSocket)
{
_uv.tcp_open(this, hSocket);
}
public void NoDelay(bool enable) public void NoDelay(bool enable)
{ {
_uv.tcp_nodelay(this, enable); _uv.tcp_nodelay(this, enable);
} }
/// <summary>
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
/// </summary>
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);
}
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -113,84 +114,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel
engine.Start(threadCount); engine.Start(threadCount);
var atLeastOneListener = false; var atLeastOneListener = false;
foreach (var address in _serverAddresses.Addresses.ToArray()) var listenOptions = Options.ListenOptions;
if (listenOptions.Any())
{ {
var parsedAddress = ServerAddress.FromUrl(address); var addresses = _serverAddresses.Addresses;
atLeastOneListener = true; if (addresses.SingleOrDefault() != "http://localhost:5000")
if (!parsedAddress.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{ {
try var joined = string.Join(", ", addresses);
{ throw new NotSupportedException($"Specifying address(es) '{joined}' is incompatible with also configuring endpoint(s) in UseKestrel.");
_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);
}
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"); throw;
var exceptions = new List<Exception>();
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));
}
} }
// If requested port was "0", replace with assigned dynamic port. // If requested port was "0", replace with assigned dynamic port.
_serverAddresses.Addresses.Remove(address); _serverAddresses.Addresses.Add(endPoint.ToString());
_serverAddresses.Addresses.Add(parsedAddress.ToString());
} }
if (!atLeastOneListener) 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})."); $"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<Exception>();
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));
}
}
/// <summary>
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
/// </summary>
internal static IPEndPoint CreateIPEndPoint(ServerAddress address)
{
IPAddress ip;
if (!IPAddress.TryParse(address.Host, out ip))
{
ip = IPAddress.IPv6Any;
}
return new IPEndPoint(ip, address.Port);
}
} }
} }

View File

@ -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 System;
using Microsoft.AspNetCore.Server.Kestrel.Filter; using System.Collections.Generic;
using System.Net;
namespace Microsoft.AspNetCore.Server.Kestrel namespace Microsoft.AspNetCore.Server.Kestrel
{ {
@ -8,6 +12,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// </summary> /// </summary>
public class KestrelServerOptions public class KestrelServerOptions
{ {
/// <summary>
/// Configures the endpoints that Kestrel should listen to.
/// </summary>
/// <remarks>
/// If this list is empty, the server.urls setting (e.g. UseUrls) is used.
/// </remarks>
internal List<ListenOptions> ListenOptions { get; } = new List<ListenOptions>();
/// <summary> /// <summary>
/// Gets or sets whether the <c>Server</c> header should be included in each response. /// Gets or sets whether the <c>Server</c> header should be included in each response.
/// </summary> /// </summary>
@ -17,22 +29,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel
public bool AddServerHeader { get; set; } = true; public bool AddServerHeader { get; set; } = true;
/// <summary> /// <summary>
/// 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 <see cref="Hosting.WebHostBuilderKestrelExtensions.UseKestrel(Hosting.IWebHostBuilder, Action{KestrelServerOptions})"/>. /// Typically initialized by <see cref="Hosting.WebHostBuilderKestrelExtensions.UseKestrel(Hosting.IWebHostBuilder, Action{KestrelServerOptions})"/>.
/// </summary> /// </summary>
public IServiceProvider ApplicationServices { get; set; } public IServiceProvider ApplicationServices { get; set; }
/// <summary>
/// Gets or sets an <see cref="IConnectionFilter"/> that allows each connection <see cref="System.IO.Stream"/>
/// to be intercepted and transformed.
/// Configured by the <c>UseHttps()</c> and <see cref="Hosting.KestrelServerOptionsConnectionLoggingExtensions.UseConnectionLogging(KestrelServerOptions)"/>
/// extension methods.
/// </summary>
/// <remarks>
/// Defaults to null.
/// </remarks>
public IConnectionFilter ConnectionFilter { get; set; }
/// <summary> /// <summary>
/// <para> /// <para>
/// This property is obsolete and will be removed in a future version. /// This property is obsolete and will be removed in a future version.
@ -64,14 +65,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// </summary> /// </summary>
public KestrelServerLimits Limits { get; } = new KestrelServerLimits(); public KestrelServerLimits Limits { get; } = new KestrelServerLimits();
/// <summary>
/// Set to false to enable Nagle's algorithm for all connections.
/// </summary>
/// <remarks>
/// Defaults to true.
/// </remarks>
public bool NoDelay { get; set; } = true;
/// <summary> /// <summary>
/// The amount of time after the server begins shutting down before connections will be forcefully closed. /// 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 /// 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; return threadCount;
} }
} }
/// <summary>
/// Bind to given IP address and port.
/// </summary>
public void Listen(IPAddress address, int port)
{
Listen(address, port, _ => { });
}
/// <summary>
/// Bind to given IP address and port.
/// The callback configures endpoint-specific settings.
/// </summary>
public void Listen(IPAddress address, int port, Action<ListenOptions> configure)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
Listen(new IPEndPoint(address, port), configure);
}
/// <summary>
/// Bind to given IP endpoint.
/// </summary>
public void Listen(IPEndPoint endPoint)
{
Listen(endPoint, _ => { });
}
/// <summary>
/// Bind to given IP address and port.
/// The callback configures endpoint-specific settings.
/// </summary>
public void Listen(IPEndPoint endPoint, Action<ListenOptions> 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);
}
/// <summary>
/// Bind to given Unix domain socket path.
/// </summary>
public void ListenUnixSocket(string socketPath)
{
ListenUnixSocket(socketPath, _ => { });
}
/// <summary>
/// Bind to given Unix domain socket path.
/// Specify callback to configure endpoint-specific settings.
/// </summary>
public void ListenUnixSocket(string socketPath, Action<ListenOptions> 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);
}
/// <summary>
/// Open a socket file descriptor.
/// </summary>
public void ListenHandle(ulong handle)
{
ListenHandle(handle, _ => { });
}
/// <summary>
/// Open a socket file descriptor.
/// The callback configures endpoint-specific settings.
/// </summary>
public void ListenHandle(ulong handle, Action<ListenOptions> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
var listenOptions = new ListenOptions(handle) { KestrelServerOptions = this };
configure(listenOptions);
ListenOptions.Add(listenOptions);
}
} }
} }

View File

@ -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
{
/// <summary>
/// Describes either an <see cref="IPEndPoint"/>, Unix domain socket path, or a file descriptor for an already open
/// socket that Kestrel should bind to or open.
/// </summary>
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;
}
/// <summary>
/// The type of interface being described: either an <see cref="IPEndPoint"/>, Unix domain socket path, or a file descriptor.
/// </summary>
public ListenType Type { get; }
// IPEndPoint is mutable so port 0 can be updated to the bound port.
/// <summary>
/// The <see cref="IPEndPoint"/> to bind to.
/// Only set if the <see cref="ListenOptions"/> <see cref="Type"/> is <see cref="ListenType.IPEndPoint"/>.
/// </summary>
public IPEndPoint IPEndPoint { get; internal set; }
/// <summary>
/// The absolute path to a Unix domain socket to bind to.
/// Only set if the <see cref="ListenOptions"/> <see cref="Type"/> is <see cref="ListenType.SocketPath"/>.
/// </summary>
public string SocketPath { get; }
/// <summary>
/// A file descriptor for the socket to open.
/// Only set if the <see cref="ListenOptions"/> <see cref="Type"/> is <see cref="ListenType.FileHandle"/>.
/// </summary>
public ulong FileHandle { get; }
/// <summary>
/// Enables an <see cref="IConnectionAdapter"/> to resolve and use services registered by the application during startup.
/// Only set if accessed from the callback of a <see cref="KestrelServerOptions"/> Listen* method.
/// </summary>
public KestrelServerOptions KestrelServerOptions { get; internal set; }
/// <summary>
/// Set to false to enable Nagle's algorithm for all connections.
/// </summary>
/// <remarks>
/// Defaults to true.
/// </remarks>
public bool NoDelay { get; set; } = true;
/// <summary>
/// Gets the <see cref="List{IConnectionAdapter}"/> that allows each connection <see cref="System.IO.Stream"/>
/// to be intercepted and transformed.
/// Configured by the <c>UseHttps()</c> and <see cref="Hosting.ListenOptionsConnectionLoggingExtensions.UseConnectionLogging(ListenOptions)"/>
/// extension methods.
/// </summary>
/// <remarks>
/// Defaults to empty.
/// </remarks>
public List<IConnectionAdapter> ConnectionAdapters { get; } = new List<IConnectionAdapter>();
// 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://<file handle>";
default:
throw new InvalidOperationException();
}
}
}
}

View File

@ -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
{
/// <summary>
/// Enumerates the <see cref="ListenOptions"/> types.
/// </summary>
public enum ListenType
{
IPEndPoint,
SocketPath,
FileHandle
}
}

View File

@ -169,4 +169,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel
}; };
} }
} }
} }

View File

@ -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";
/// <summary>
/// Open file descriptor (SD_LISTEN_FDS_START) initialized by systemd socket-based activation logic if available.
/// </summary>
/// <returns>
/// The <see cref="KestrelServerOptions"/>.
/// </returns>
public static KestrelServerOptions UseSystemd(this KestrelServerOptions options)
{
return options.UseSystemd(_ => { });
}
/// <summary>
/// Open file descriptor (SD_LISTEN_FDS_START) initialized by systemd socket-based activation logic if available.
/// Specify callback to configure endpoint-specific settings.
/// </summary>
/// <returns>
/// The <see cref="KestrelServerOptions"/>.
/// </returns>
public static KestrelServerOptions UseSystemd(this KestrelServerOptions options, Action<ListenOptions> configure)
{
if (string.Equals(Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture), Environment.GetEnvironmentVariable(ListenPidEnvVar), StringComparison.Ordinal))
{
options.ListenHandle(SdListenFdsStart, configure);
}
return options;
}
}
}

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -37,6 +37,7 @@
}, },
"netstandard1.3": { "netstandard1.3": {
"dependencies": { "dependencies": {
"System.Diagnostics.Process": "4.4.0-*",
"System.Threading.Thread": "4.4.0-*", "System.Threading.Thread": "4.4.0-*",
"System.Threading.ThreadPool": "4.4.0-*" "System.Threading.ThreadPool": "4.4.0-*"
} }

View File

@ -16,13 +16,14 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Options;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
public class AddressRegistrationTests 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<IServerAddressesFeature, string[]> testUrls) public async Task RegisterAddresses_IPv4_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{ {
await RegisterAddresses_Success(addressInput, testUrls); await RegisterAddresses_Success(addressInput, testUrls);
@ -35,14 +36,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await RegisterAddresses_Success(addressInput, 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(AddressRegistrationDataIPv4Port443))] [Theory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(IPEndPointRegistrationDataRandomPort))]
[PortSupportedCondition(443)] [IPv6SupportedCondition]
public async Task RegisterAddresses_IPv4Port443_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls) public async Task RegisterIPEndPoint_RandomPort_Success(IPEndPoint endPoint, Func<IPEndPoint, string> 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<IPEndPoint, string> testUrl)
{
await RegisterIPEndPoint_Success(endpoint, testUrl);
}
[ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6))]
[IPv6SupportedCondition] [IPv6SupportedCondition]
public async Task RegisterAddresses_IPv6_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls) public async Task RegisterAddresses_IPv6_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{ {
@ -57,15 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await RegisterAddresses_Success(addressInput, 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(AddressRegistrationDataIPv6Port443))] [ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6ScopeId))]
[IPv6SupportedCondition]
[PortSupportedCondition(443)]
public async Task RegisterAddresses_IPv6Port443_Success(string addressInput, Func<IServerAddressesFeature, string[]> 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))]
[IPv6SupportedCondition] [IPv6SupportedCondition]
public async Task RegisterAddresses_IPv6ScopeId_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls) public async Task RegisterAddresses_IPv6ScopeId_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{ {
@ -75,10 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
private async Task RegisterAddresses_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls) private async Task RegisterAddresses_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{ {
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .UseKestrel()
{
options.UseHttps(@"TestResources/testCert.pfx", "testPassword");
})
.UseUrls(addressInput) .UseUrls(addressInput)
.Configure(ConfigureEchoAddress); .Configure(ConfigureEchoAddress);
@ -97,6 +95,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
private async Task RegisterIPEndPoint_Success(IPEndPoint endPoint, Func<IPEndPoint, string> 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<KestrelServerOptions>)host.Services.GetService(typeof(IOptions<KestrelServerOptions>))).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] [Fact]
public void ThrowsWhenBindingToIPv4AddressInUse() public void ThrowsWhenBindingToIPv4AddressInUse()
{ {
@ -200,37 +229,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" }); dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" });
// Static ports // Static ports
var port1 = GetNextPort(); var port = GetNextPort();
var port2 = GetNextPort();
// Loopback // Loopback
dataset.Add($"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}/" });
_ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" });
// Localhost // Localhost
dataset.Add($"http://localhost:{port1};https://localhost:{port2}", dataset.Add($"http://localhost:{port}", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/" });
_ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/",
$"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/" });
// Any // Any
dataset.Add($"http://*:{port1}/;https://*:{port2}/", dataset.Add($"http://*:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" });
_ => 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://+:{port1}/;https://+:{port2}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" });
// Path after port // Path after port
dataset.Add($"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" });
_ => new[] { $"http://127.0.0.1:{port1}/base/path", $"https://127.0.0.1:{port2}/base/path" });
// Dynamic port and non-loopback addresses // Dynamic port and non-loopback addresses
dataset.Add("http://127.0.0.1:0/;https://127.0.0.1:0/", GetTestUrls); dataset.Add("http://127.0.0.1:0/", GetTestUrls);
dataset.Add($"http://{Dns.GetHostName()}:0/;https://{Dns.GetHostName()}:0/", GetTestUrls); dataset.Add($"http://{Dns.GetHostName()}:0/", GetTestUrls);
var ipv4Addresses = GetIPAddresses() var ipv4Addresses = GetIPAddresses()
.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork);
foreach (var ip in ipv4Addresses) 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<IPEndPoint, Func<IPEndPoint, string>> IPEndPointRegistrationDataRandomPort
{
get
{
var dataset = new TheoryData<IPEndPoint, Func<IPEndPoint, string>>();
// 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; return dataset;
@ -252,16 +317,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv4Port443 public static TheoryData<IPEndPoint, Func<IPEndPoint, string>> IPEndPointRegistrationDataPort443
{ {
get get
{ {
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>(); var dataset = new TheoryData<IPEndPoint, Func<IPEndPoint, string>>();
// Default port for HTTPS (443) dataset.Add(new IPEndPoint(IPAddress.Loopback, 443), _ => "https://127.0.0.1/");
dataset.Add("https://127.0.0.1", _ => new[] { "https://127.0.0.1/" }); dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, 443), _ => FixTestUrl("https://[::1]/"));
dataset.Add("https://localhost", _ => new[] { "https://127.0.0.1/" }); dataset.Add(new IPEndPoint(IPAddress.Any, 443), _ => "https://127.0.0.1/");
dataset.Add("https://*", _ => new[] { "https://127.0.0.1/" }); dataset.Add(new IPEndPoint(IPAddress.IPv6Any, 443), _ => FixTestUrl("https://[::1]/"));
return dataset; 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/" }); dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" });
// Static ports // Static ports
var port1 = GetNextPort(); var port = GetNextPort();
var port2 = GetNextPort();
// Loopback // Loopback
dataset.Add($"http://[::1]:{port1}/;https://[::1]:{port2}/", dataset.Add($"http://[::1]:{port}/",
_ => new[] { $"http://[::1]:{port1}/", $"https://[::1]:{port2}/" }); _ => new[] { $"http://[::1]:{port}/" });
// Localhost // Localhost
dataset.Add($"http://localhost:{port1};https://localhost:{port2}", dataset.Add($"http://localhost:{port}",
_ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
$"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
// Any // Any
dataset.Add($"http://*:{port1}/;https://*:{port2}/", dataset.Add($"http://*:{port}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
$"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" }); dataset.Add($"http://+:{port}/",
dataset.Add($"http://+:{port1}/;https://+:{port2}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
_ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/",
$"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
// Explicit IPv4 and IPv6 on same 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}/", dataset.Add($"http://127.0.0.1:{port}/;http://[::1]:{port}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
$"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
// Path after port // Path after port
dataset.Add($"http://[::1]:{port1}/base/path;https://[::1]:{port2}/base/path", dataset.Add($"http://[::1]:{port}/base/path",
_ => new[] { $"http://[::1]:{port1}/base/path", $"https://[::1]:{port2}/base/path" }); _ => new[] { $"http://[::1]:{port}/base/path" });
// Dynamic port and non-loopback addresses // Dynamic port and non-loopback addresses
var ipv6Addresses = GetIPAddresses() var ipv6Addresses = GetIPAddresses()
@ -313,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
.Where(ip => ip.ScopeId == 0); .Where(ip => ip.ScopeId == 0);
foreach (var ip in ipv6Addresses) foreach (var ip in ipv6Addresses)
{ {
dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls); dataset.Add($"http://[{ip}]:0/", GetTestUrls);
} }
return dataset; return dataset;
@ -335,21 +395,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv6Port443
{
get
{
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();
// 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<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv6ScopeId public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv6ScopeId
{ {
get get
@ -362,7 +407,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
.Where(ip => ip.ScopeId != 0); .Where(ip => ip.ScopeId != 0);
foreach (var ip in ipv6Addresses) foreach (var ip in ipv6Addresses)
{ {
dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls); dataset.Add($"http://[{ip}]:0/", GetTestUrls);
} }
return dataset; return dataset;
@ -380,11 +425,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
private static string[] GetTestUrls(IServerAddressesFeature addressesFeature) private static string[] GetTestUrls(IServerAddressesFeature addressesFeature)
{ {
return addressesFeature.Addresses return addressesFeature.Addresses
.Select(a => a.Replace("://+", "://localhost")) .Select(FixTestUrl)
.Select(a => a.EndsWith("/") ? a : a + "/")
.ToArray(); .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) private void ConfigureEchoAddress(IApplicationBuilder app)
{ {
app.Run(context => app.Run(context =>

View File

@ -4,6 +4,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
using (var host = StartHost(protocol: "https")) 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", using (var host = StartHost(protocol: "https",
handler: (context) => context.Request.Body.CopyToAsync(context.Response.Body))) 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)); 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<HttpContext, Task> handler = null) private IWebHost StartHost(string protocol = "http", int statusCode = 200, Func<HttpContext, Task> handler = null)
{ {
var host = new WebHostBuilder() var host = new WebHostBuilder()
.UseUrls($"{protocol}://127.0.0.1:0")
.UseKestrel(options => .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) => .Configure((app) =>
{ {

View File

@ -30,9 +30,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .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) .UseLoggerFactory(loggerFactory)
.Configure(app => { }); .Configure(app => { });
@ -61,9 +63,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .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) .UseLoggerFactory(loggerFactory)
.Configure(app => { }); .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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task DoesNotThrowObjectDisposedExceptionOnConnectionAbort() 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 loggerFactory = new HandshakeErrorLoggerFactory();
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .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/") .UseUrls("https://127.0.0.1:0/")
.UseLoggerFactory(loggerFactory) .UseLoggerFactory(loggerFactory)
@ -141,14 +148,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted() public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted()
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
var x509Certificate2 = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"); var x509Certificate2 = new X509Certificate2("TestResources/testCert.pfx", "testPassword");
var loggerFactory = new HandshakeErrorLoggerFactory(); var loggerFactory = new HandshakeErrorLoggerFactory();
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .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) .UseLoggerFactory(loggerFactory)
.Configure(app => app.Run(async httpContext => .Configure(app => app.Run(async httpContext =>
{ {
@ -194,9 +203,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel(options => .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) .UseLoggerFactory(loggerFactory)
.Configure(app => { }); .Configure(app => { });
@ -221,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
if (categoryName == nameof(HttpsConnectionFilter)) if (categoryName == nameof(HttpsConnectionAdapter))
{ {
return FilterLogger; return FilterLogger;
} }

View File

@ -10,9 +10,9 @@ namespace Microsoft.AspNetCore.Hosting
{ {
public static class IWebHostPortExtensions 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) public static int GetPort(this IWebHost host)
@ -37,13 +37,29 @@ namespace Microsoft.AspNetCore.Hosting
public static IEnumerable<Uri> GetUris(this IWebHost host) public static IEnumerable<Uri> GetUris(this IWebHost host)
{ {
return host.ServerFeatures.Get<IServerAddressesFeature>().Addresses return host.ServerFeatures.Get<IServerAddressesFeature>().Addresses
.Select(a => a.Replace("://+", "://localhost"))
.Select(a => new Uri(a)); .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;
} }
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -11,18 +12,20 @@ using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task LoggingConnectionFilterCanBeAddedBeforeAndAfterHttpsFilter() public async Task LoggingConnectionFilterCanBeAddedBeforeAndAfterHttpsFilter()
{ {
var host = new WebHostBuilder() var host = new WebHostBuilder()
.UseUrls($"https://127.0.0.1:0") .UseKestrel(options =>
.UseKestrel(options => {
{ options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions =>
options.UseConnectionLogging(); {
options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); listenOptions.UseConnectionLogging();
}) listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword");
});
})
.Configure(app => .Configure(app =>
{ {
app.Run(context => app.Run(context =>

View File

@ -82,11 +82,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var clientFinishedSendingRequestBody = new ManualResetEvent(false); var clientFinishedSendingRequestBody = new ManualResetEvent(false);
var lastBytesWritten = DateTime.MaxValue; 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 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); 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) ManualResetEvent clientFinishedSendingRequestBody)
{ {
var host = new WebHostBuilder() var host = new WebHostBuilder()
.UseKestrel(options => .UseKestrel(options =>
{ {
options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions =>
{
if (useSsl)
{
listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword");
}
});
options.Limits.MaxRequestBufferSize = maxRequestBufferSize; options.Limits.MaxRequestBufferSize = maxRequestBufferSize;
options.UseHttps(@"TestResources/testCert.pfx", "testPassword");
if (maxRequestBufferSize.HasValue && if (maxRequestBufferSize.HasValue &&
maxRequestBufferSize.Value < options.Limits.MaxRequestLineSize) maxRequestBufferSize.Value < options.Limits.MaxRequestLineSize)
@ -176,7 +183,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize; options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize;
} }
}) })
.UseUrls("http://127.0.0.1:0/", "https://127.0.0.1:0/")
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.Configure(app => app.Run(async context => .Configure(app => app.Run(async context =>
{ {

View File

@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
disposedTcs.TrySetResult(c.Response.StatusCode); 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) if (!sendMalformedRequest)
{ {

View File

@ -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"]

View File

@ -0,0 +1,8 @@
[Unit]
Requires=activate-kestrel.socket
[Service]
ExecStart=/usr/bin/dotnet SampleApp.dll
WorkingDirectory=/publish
NonBlocking=true

View File

@ -0,0 +1,7 @@
[Unit]
Description=Kestrel Activation
[Socket]
ListenStream=8080
NoDelay=true

View File

@ -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

View File

@ -2,6 +2,7 @@
// 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.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -98,7 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
}; };
var listenerContext = new ListenerContext(serviceContext) var listenerContext = new ListenerContext(serviceContext)
{ {
ServerAddress = ServerAddress.FromUrl("http://localhost:5000") ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
}; };
var connectionContext = new ConnectionContext(listenerContext) var connectionContext = new ConnectionContext(listenerContext)
{ {

View File

@ -4,9 +4,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -16,21 +18,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class ChunkedRequestTests public class ChunkedRequestTests
{ {
public static TheoryData<TestServiceContext> ConnectionFilterData public static TheoryData<ListenOptions> ConnectionAdapterData => new TheoryData<ListenOptions>
{ {
get new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)),
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{ {
return new TheoryData<TestServiceContext> ConnectionAdapters = { new PassThroughConnectionAdapter() }
{
{
new TestServiceContext()
},
{
new TestServiceContext(new PassThroughConnectionFilter())
}
};
} }
} };
private async Task App(HttpContext httpContext) private async Task App(HttpContext httpContext)
{ {
@ -61,10 +56,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10TransferEncoding(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -88,10 +85,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10KeepAliveTransferEncoding(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -127,9 +126,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(TestServiceContext testContext) public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
@ -140,7 +141,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -177,9 +178,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersAreParsed(TestServiceContext testContext) public async Task TrailingHeadersAreParsed(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var requestCount = 10; var requestCount = 10;
var requestsReceived = 0; var requestsReceived = 0;
@ -211,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
var response = string.Join("\r\n", new string[] { var response = string.Join("\r\n", new string[] {
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
@ -262,13 +264,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(TestServiceContext testContext) public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(ListenOptions listenOptions)
{ {
const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
const string headerLine = "Header: value"; const string headerLine = "Header: value";
const string trailingHeaderLine = "Trailing-Header: trailing-value"; const string trailingHeaderLine = "Trailing-Header: trailing-value";
var testContext = new TestServiceContext();
testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize = testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize =
transferEncodingHeaderLine.Length + 2 + transferEncodingHeaderLine.Length + 2 +
headerLine.Length + 2 + headerLine.Length + 2 +
@ -278,7 +281,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var buffer = new byte[128]; var buffer = new byte[128];
while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -305,20 +308,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersCountTowardsHeaderCountLimit(TestServiceContext testContext) public async Task TrailingHeadersCountTowardsHeaderCountLimit(ListenOptions listenOptions)
{ {
const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
const string headerLine = "Header: value"; const string headerLine = "Header: value";
const string trailingHeaderLine = "Trailing-Header: trailing-value"; const string trailingHeaderLine = "Trailing-Header: trailing-value";
var testContext = new TestServiceContext();
testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2; testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2;
using (var server = new TestServer(async context => using (var server = new TestServer(async context =>
{ {
var buffer = new byte[128]; var buffer = new byte[128];
while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -345,9 +349,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ExtensionsAreIgnored(TestServiceContext testContext) public async Task ExtensionsAreIgnored(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var requestCount = 10; var requestCount = 10;
var requestsReceived = 0; var requestsReceived = 0;
@ -379,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
var response = string.Join("\r\n", new string[] { var response = string.Join("\r\n", new string[] {
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
@ -430,9 +435,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task InvalidLengthResultsIn400(TestServiceContext testContext) public async Task InvalidLengthResultsIn400(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
@ -448,7 +454,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -472,9 +478,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task InvalidSizedDataResultsIn400(TestServiceContext testContext) public async Task InvalidSizedDataResultsIn400(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
@ -490,7 +497,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -516,13 +523,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ChunkedNotFinalTransferCodingResultsIn400(TestServiceContext testContext) public async Task ChunkedNotFinalTransferCodingResultsIn400(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {

View File

@ -2,10 +2,12 @@
// 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.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -14,32 +16,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class ChunkedResponseTests public class ChunkedResponseTests
{ {
public static TheoryData<TestServiceContext> ConnectionFilterData public static TheoryData<ListenOptions> ConnectionAdapterData => new TheoryData<ListenOptions>
{ {
get new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)),
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{ {
return new TheoryData<TestServiceContext> ConnectionAdapters = { new PassThroughConnectionAdapter() }
{
{
new TestServiceContext()
},
{
new TestServiceContext(new PassThroughConnectionFilter())
}
};
} }
} };
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreChunkedAutomatically(TestServiceContext testContext) public async Task ResponsesAreChunkedAutomatically(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -64,14 +61,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(TestServiceContext testContext) public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!"); await httpContext.Response.WriteAsync("World!");
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -91,14 +90,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(TestServiceContext testContext) public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!"); await httpContext.Response.WriteAsync("World!");
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -125,15 +126,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(TestServiceContext testContext) public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
httpContext.Response.Headers["Connection"] = "close"; httpContext.Response.Headers["Connection"] = "close";
await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!"); await httpContext.Response.WriteAsync("World!");
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -159,16 +162,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroLengthWritesAreIgnored(TestServiceContext testContext) public async Task ZeroLengthWritesAreIgnored(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
await response.Body.WriteAsync(new byte[0], 0, 0); await response.Body.WriteAsync(new byte[0], 0, 0);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -193,9 +198,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroLengthWritesFlushHeaders(TestServiceContext testContext) public async Task ZeroLengthWritesFlushHeaders(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var flushed = new SemaphoreSlim(0, 1); var flushed = new SemaphoreSlim(0, 1);
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
@ -206,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await flushed.WaitAsync(); await flushed.WaitAsync();
await response.WriteAsync("Hello World!"); await response.WriteAsync("Hello World!");
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -235,14 +242,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(TestServiceContext testContext) public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
await response.Body.WriteAsync(new byte[0], 0, 0); await response.Body.WriteAsync(new byte[0], 0, 0);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -263,15 +272,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedIfExceptionThrownAfterWrite(TestServiceContext testContext) public async Task ConnectionClosedIfExceptionThrownAfterWrite(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12);
throw new Exception(); throw new Exception();
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -294,15 +305,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(TestServiceContext testContext) public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
await response.Body.WriteAsync(new byte[0], 0, 0); await response.Body.WriteAsync(new byte[0], 0, 0);
throw new Exception(); throw new Exception();
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -324,9 +337,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task WritesAreFlushedPriorToResponseCompletion(TestServiceContext testContext) public async Task WritesAreFlushedPriorToResponseCompletion(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var flushWh = new ManualResetEventSlim(); var flushWh = new ManualResetEventSlim();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
@ -338,7 +353,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
flushWh.Wait(); flushWh.Wait();
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -368,9 +383,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ChunksCanBeWrittenManually(TestServiceContext testContext) public async Task ChunksCanBeWrittenManually(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; 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\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("6\r\nWorld!\r\n"), 0, 11);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {

View File

@ -3,26 +3,34 @@
using System; using System;
using System.IO; using System.IO;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class ConnectionFilterTests public class ConnectionAdapterTests
{ {
[Fact] [Fact]
public async Task CanReadAndWriteWithRewritingConnectionFilter() public async Task CanReadAndWriteWithRewritingConnectionAdapter()
{ {
var filter = new RewritingConnectionFilter(); var adapter = new RewritingConnectionAdapter();
var serviceContext = new TestServiceContext(filter); 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?"; 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()) 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] [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()) using (var connection = server.CreateConnection())
{ {
@ -65,15 +78,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Fact] [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()) 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<IOException>(async () => await Assert.ThrowsAsync<IOException>(async () =>
{ {
await connection.Send( await connection.Send(
@ -91,42 +109,50 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
private class RewritingConnectionFilter : IConnectionFilter private class RewritingConnectionAdapter : IConnectionAdapter
{ {
private RewritingStream _rewritingStream; private RewritingStream _rewritingStream;
public Task OnConnectionAsync(ConnectionFilterContext context) public Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
{ {
_rewritingStream = new RewritingStream(context.Connection); _rewritingStream = new RewritingStream(context.ConnectionStream);
context.Connection = _rewritingStream; return Task.FromResult<IAdaptedConnection>(new AdaptedConnection(_rewritingStream));
return TaskCache.CompletedTask;
} }
public int BytesRead => _rewritingStream.BytesRead; public int BytesRead => _rewritingStream.BytesRead;
} }
private class AsyncConnectionFilter : IConnectionFilter private class AsyncConnectionAdapter : IConnectionAdapter
{ {
public async Task OnConnectionAsync(ConnectionFilterContext context) public async Task<IAdaptedConnection> 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); await Task.Delay(100);
return new AdaptedConnection(new RewritingStream(context.ConnectionStream));
context.Connection = new RewritingStream(oldConnection);
} }
} }
private class ThrowingConnectionFilter : IConnectionFilter private class ThrowingConnectionAdapter : IConnectionAdapter
{ {
public Task OnConnectionAsync(ConnectionFilterContext context) public Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
{ {
throw new Exception(); 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 class RewritingStream : Stream
{ {
private readonly Stream _innerStream; private readonly Stream _innerStream;

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
var context = new ListenerContext(serviceContext) 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] Thread = engine.Threads[0]
}; };

View File

@ -3,7 +3,6 @@
using System.Net; using System.Net;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests namespace Microsoft.AspNetCore.Server.KestrelTests
@ -11,14 +10,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public class CreateIPEndpointTests public class CreateIPEndpointTests
{ {
[Theory] [Theory]
[InlineData("localhost", "127.0.0.1")] // https://github.com/aspnet/KestrelHttpServer/issues/231
[InlineData("10.10.10.10", "10.10.10.10")] [InlineData("10.10.10.10", "10.10.10.10")]
[InlineData("[::1]", "::1")] [InlineData("[::1]", "::1")]
[InlineData("randomhost", "::")] // "::" is IPAddress.IPv6Any [InlineData("randomhost", "::")] // "::" is IPAddress.IPv6Any
[InlineData("*", "::")] // "::" is IPAddress.IPv6Any [InlineData("*", "::")] // "::" is IPAddress.IPv6Any
public void CorrectIPEndpointsAreCreated(string host, string expectedAddress) 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.NotNull(endpoint);
Assert.Equal(IPAddress.Parse(expectedAddress), endpoint.Address); Assert.Equal(IPAddress.Parse(expectedAddress), endpoint.Address);
Assert.Equal(5000, endpoint.Port); Assert.Equal(5000, endpoint.Port);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -26,55 +28,47 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
/// </summary> /// </summary>
public class EngineTests public class EngineTests
{ {
public static TheoryData<TestServiceContext> ConnectionFilterData public static TheoryData<ListenOptions> ConnectionAdapterData => new TheoryData<ListenOptions>
{ {
get new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)),
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{ {
return new TheoryData<TestServiceContext> ConnectionAdapters = { new PassThroughConnectionAdapter() }
{
{
new TestServiceContext()
},
{
new TestServiceContext(new PassThroughConnectionFilter())
}
};
} }
} };
[Theory] [Fact]
[MemberData(nameof(ConnectionFilterData))] public void EngineCanStartAndStop()
public void EngineCanStartAndStop(TestServiceContext testContext)
{ {
var engine = new KestrelEngine(testContext); var engine = new KestrelEngine(new TestServiceContext());
engine.Start(1); engine.Start(1);
engine.Dispose(); engine.Dispose();
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public void ListenerCanCreateAndDispose(TestServiceContext testContext) public void ListenerCanCreateAndDispose(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
testContext.App = TestApp.EchoApp; testContext.App = TestApp.EchoApp;
var engine = new KestrelEngine(testContext); var engine = new KestrelEngine(testContext);
engine.Start(1); engine.Start(1);
var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); var started = engine.CreateServer(listenOptions);
var started = engine.CreateServer(address);
started.Dispose(); started.Dispose();
engine.Dispose(); engine.Dispose();
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public void ConnectionCanReadAndWrite(TestServiceContext testContext) public void ConnectionCanReadAndWrite(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
testContext.App = TestApp.EchoApp; testContext.App = TestApp.EchoApp;
var engine = new KestrelEngine(testContext); var engine = new KestrelEngine(testContext);
engine.Start(1); engine.Start(1);
var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); var started = engine.CreateServer(listenOptions);
var started = engine.CreateServer(address);
var socket = TestConnection.CreateConnectedLoopbackSocket(address.Port); var socket = TestConnection.CreateConnectedLoopbackSocket(listenOptions.IPEndPoint.Port);
var data = "Hello World"; var data = "Hello World";
socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}")); socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}"));
var buffer = new byte[data.Length]; var buffer = new byte[data.Length];
@ -89,10 +83,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10RequestReceivesHttp11Response(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -112,10 +108,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http11(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -143,9 +141,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task HeadersAndStreamsAreReused(TestServiceContext testContext) public async Task HeadersAndStreamsAreReused(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var streamCount = 0; var streamCount = 0;
var requestHeadersCount = 0; var requestHeadersCount = 0;
var responseHeadersCount = 0; var responseHeadersCount = 0;
@ -222,10 +221,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10ContentLength(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -245,10 +246,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10KeepAlive(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -278,10 +281,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10KeepAliveNotUsedIfResponseContentLengthNotSet(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -311,10 +316,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Http10KeepAliveContentLength(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -346,10 +353,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task Expect100ContinueForBody(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -376,10 +385,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task DisconnectingClient(TestServiceContext testContext) 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); var socket = TestConnection.CreateConnectedLoopbackSocket(server.Port);
await Task.Delay(200); await Task.Delay(200);
@ -404,10 +415,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -434,13 +447,15 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(TestServiceContext testContext) public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(10))); 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()) using (var connection = server.CreateConnection())
{ {
@ -476,10 +491,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -497,9 +514,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(TestServiceContext testContext) public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var request = httpContext.Request; var request = httpContext.Request;
@ -510,7 +529,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var statusString = await reader.ReadLineAsync(); var statusString = await reader.ReadLineAsync();
response.StatusCode = int.Parse(statusString); response.StatusCode = int.Parse(statusString);
} }
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -548,14 +567,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(TestServiceContext testContext) public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
// This will hang if 0 content length is not assumed by the server // 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))); 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()) using (var connection = server.CreateConnection())
{ {
@ -594,16 +615,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedAfter101Response(TestServiceContext testContext) public async Task ConnectionClosedAfter101Response(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var request = httpContext.Request; var request = httpContext.Request;
var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync(); var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync();
var response = Encoding.ASCII.GetBytes("hello, world"); var response = Encoding.ASCII.GetBytes("hello, world");
await stream.WriteAsync(response, 0, response.Length); await stream.WriteAsync(response, 0, response.Length);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -637,9 +660,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ThrowingResultsIn500Response(TestServiceContext testContext) public async Task ThrowingResultsIn500Response(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
bool onStartingCalled = false; bool onStartingCalled = false;
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
@ -657,7 +682,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
// Anything added to the ResponseHeaders dictionary is ignored // Anything added to the ResponseHeaders dictionary is ignored
response.Headers["Content-Length"] = "11"; response.Headers["Content-Length"] = "11";
throw new Exception(); throw new Exception();
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -687,9 +712,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ThrowingAfterWritingKillsConnection(TestServiceContext testContext) public async Task ThrowingAfterWritingKillsConnection(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
bool onStartingCalled = false; bool onStartingCalled = false;
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
@ -707,7 +734,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
throw new Exception(); throw new Exception();
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -729,9 +756,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ThrowingAfterPartialWriteKillsConnection(TestServiceContext testContext) public async Task ThrowingAfterPartialWriteKillsConnection(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
bool onStartingCalled = false; bool onStartingCalled = false;
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
@ -749,7 +778,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5);
throw new Exception(); throw new Exception();
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -770,10 +799,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(1, testLogger.ApplicationErrorsLogged); Assert.Equal(1, testLogger.ApplicationErrorsLogged);
} }
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes(TestServiceContext testContext) 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()) using (var connection = server.CreateConnection())
{ {
@ -819,9 +850,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(TestServiceContext testContext) public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var onStartingCallCount1 = 0; var onStartingCallCount1 = 0;
var onStartingCallCount2 = 0; var onStartingCallCount2 = 0;
var failedWriteCount = 0; var failedWriteCount = 0;
@ -853,7 +886,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Same(onStartingException, writeException.InnerException); Assert.Same(onStartingException, writeException.InnerException);
failedWriteCount++; failedWriteCount++;
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -884,9 +917,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(TestServiceContext testContext) public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var onCompletedCalled1 = false; var onCompletedCalled1 = false;
var onCompletedCalled2 = false; var onCompletedCalled2 = false;
@ -910,7 +945,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -934,9 +969,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task RequestsCanBeAbortedMidRead(TestServiceContext testContext) public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var readTcs = new TaskCompletionSource<object>(); var readTcs = new TaskCompletionSource<object>();
var registrationTcs = new TaskCompletionSource<int>(); var registrationTcs = new TaskCompletionSource<int>();
var requestId = 0; var requestId = 0;
@ -975,7 +1012,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
readTcs.SetException(new Exception("This shouldn't be reached.")); readTcs.SetException(new Exception("This shouldn't be reached."));
} }
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1005,9 +1042,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(2, abortedRequestId); Assert.Equal(2, abortedRequestId);
} }
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task FailedWritesResultInAbortedRequest(TestServiceContext testContext) public async Task FailedWritesResultInAbortedRequest(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
// This should match _maxBytesPreCompleted in SocketOutput // This should match _maxBytesPreCompleted in SocketOutput
var maxBytesPreCompleted = 65536; var maxBytesPreCompleted = 65536;
// Ensure string is long enough to disable write-behind buffering // 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.")); writeTcs.SetException(new Exception("This shouldn't be reached."));
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1066,9 +1105,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(TestServiceContext testContext) public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
testContext.Log = new KestrelTrace(testLogger); testContext.Log = new KestrelTrace(testLogger);
@ -1077,7 +1118,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var response = httpContext.Response; var response = httpContext.Response;
response.Headers["Content-Length"] = new[] { "11" }; response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1099,14 +1140,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(TestServiceContext testContext) public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
httpContext.Abort(); httpContext.Abort();
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1121,9 +1164,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task RequestHeadersAreResetOnEachRequest(TestServiceContext testContext) public async Task RequestHeadersAreResetOnEachRequest(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
IHeaderDictionary originalRequestHeaders = null; IHeaderDictionary originalRequestHeaders = null;
var firstRequest = true; var firstRequest = true;
@ -1143,7 +1188,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1168,9 +1213,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task ResponseHeadersAreResetOnEachRequest(TestServiceContext testContext) public async Task ResponseHeadersAreResetOnEachRequest(ListenOptions listenOptions)
{ {
var testContext = new TestServiceContext();
IHeaderDictionary originalResponseHeaders = null; IHeaderDictionary originalResponseHeaders = null;
var firstRequest = true; var firstRequest = true;
@ -1190,7 +1237,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1243,11 +1290,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(TestServiceContext testContext) public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions)
{ {
const string response = "hello, world"; const string response = "hello, world";
var testContext = new TestServiceContext();
var callOrder = new Stack<int>(); var callOrder = new Stack<int>();
var onStartingTcs = new TaskCompletionSource<object>(); var onStartingTcs = new TaskCompletionSource<object>();
@ -1267,7 +1316,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
context.Response.ContentLength = response.Length; context.Response.ContentLength = response.Length;
await context.Response.WriteAsync(response); await context.Response.WriteAsync(response);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1292,11 +1341,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(TestServiceContext testContext) public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions)
{ {
const string response = "hello, world"; const string response = "hello, world";
var testContext = new TestServiceContext();
var callOrder = new Stack<int>(); var callOrder = new Stack<int>();
var onCompletedTcs = new TaskCompletionSource<object>(); var onCompletedTcs = new TaskCompletionSource<object>();
@ -1316,7 +1367,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
context.Response.ContentLength = response.Length; context.Response.ContentLength = response.Length;
await context.Response.WriteAsync(response); await context.Response.WriteAsync(response);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1341,11 +1392,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionAdapterData))]
public async Task UpgradeRequestIsNotKeptAliveOrChunked(TestServiceContext testContext) public async Task UpgradeRequestIsNotKeptAliveOrChunked(ListenOptions listenOptions)
{ {
const string message = "Hello World"; const string message = "Hello World";
var testContext = new TestServiceContext();
using (var server = new TestServer(async context => using (var server = new TestServer(async context =>
{ {
var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>(); var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
@ -1359,7 +1412,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
await duplexStream.WriteAsync(buffer, 0, read); await duplexStream.WriteAsync(buffer, 0, read);
}, testContext)) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Net;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
@ -28,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
var listenerContext = new ListenerContext(serviceContext) var listenerContext = new ListenerContext(serviceContext)
{ {
ServerAddress = ServerAddress.FromUrl("http://localhost:5000") ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
}; };
var connectionContext = new ConnectionContext(listenerContext); var connectionContext = new ConnectionContext(listenerContext);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
var listenerContext = new ListenerContext(_serviceContext) var listenerContext = new ListenerContext(_serviceContext)
{ {
ServerAddress = ServerAddress.FromUrl("http://localhost:5000") ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
}; };
_connectionContext = new ConnectionContext(listenerContext) _connectionContext = new ConnectionContext(listenerContext)
{ {
@ -440,7 +441,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public void InitializeStreamsResetsStreams() public void InitializeStreamsResetsStreams()
{ {
// Arrange // 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); _frame.InitializeStreams(messageBody);
var originalRequestBody = _frame.RequestBody; var originalRequestBody = _frame.RequestBody;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Security; using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
@ -13,33 +14,33 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; 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.Https;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests 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 // https://github.com/aspnet/KestrelHttpServer/issues/240
// This test currently fails on mono because of an issue with SslStream. // 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)")] [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( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 }, var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
new NoOpConnectionFilter()) {
); 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}/", var result = await HttpClientSlim.PostAsync($"https://localhost:{server.Port}/",
new FormUrlEncodedContent(new[] { 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task RequireCertificateFailsWhenNoCertificate() public async Task RequireCertificateFailsWhenNoCertificate()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{ {
ServerCertificate = _x509Certificate2, ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate ClientCertificateMode = ClientCertificateMode.RequireCertificate
}, })
new NoOpConnectionFilter()) }
); };
using (var server = new TestServer(App, serviceContext, _serverAddress))
using (var server = new TestServer(App, serviceContext, listenOptions))
{ {
await Assert.ThrowsAnyAsync<Exception>( await Assert.ThrowsAnyAsync<Exception>(
() => HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/")); () => 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task AllowCertificateContinuesWhenNoCertificate() public async Task AllowCertificateContinuesWhenNoCertificate()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = ClientCertificateMode.AllowCertificate {
}, ServerCertificate = _x509Certificate2,
new NoOpConnectionFilter()) ClientCertificateMode = ClientCertificateMode.AllowCertificate
); })
}
};
using (var server = new TestServer(context => using (var server = new TestServer(context =>
{ {
Assert.Equal(context.Features.Get<ITlsConnectionFeature>(), null); Assert.Equal(context.Features.Get<ITlsConnectionFeature>(), null);
return context.Response.WriteAsync("hello world"); return context.Response.WriteAsync("hello world");
}, },
serviceContext, _serverAddress)) serviceContext, listenOptions))
{ {
var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result); Assert.Equal("hello world", result);
@ -97,24 +107,24 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Fact] [Fact]
public void ThrowsWhenNoServerCertificateIsProvided() public void ThrowsWhenNoServerCertificateIsProvided()
{ {
Assert.Throws<ArgumentException>(() => new HttpsConnectionFilter( Assert.Throws<ArgumentException>(() => new HttpsConnectionAdapter(
new HttpsConnectionFilterOptions(), new HttpsConnectionAdapterOptions())
new NoOpConnectionFilter())
); );
} }
[Fact] [Fact]
public async Task UsesProvidedServerCertificate() public async Task UsesProvidedServerCertificate()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2 new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 })
}, }
new NoOpConnectionFilter()) };
);
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()) 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task CertificatePassedToHttpContext() public async Task CertificatePassedToHttpContext()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = ClientCertificateMode.RequireCertificate, {
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true ServerCertificate = _x509Certificate2,
}, ClientCertificateMode = ClientCertificateMode.RequireCertificate,
new NoOpConnectionFilter()) ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
); })
}
};
using (var server = new TestServer(context => using (var server = new TestServer(context =>
{ {
@ -150,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.NotNull(context.Connection.ClientCertificate); Assert.NotNull(context.Connection.ClientCertificate);
return context.Response.WriteAsync("hello world"); return context.Response.WriteAsync("hello world");
}, },
serviceContext, _serverAddress)) serviceContext, listenOptions))
{ {
using (var client = new TcpClient()) 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task HttpsSchemePassedToRequestFeature() public async Task HttpsSchemePassedToRequestFeature()
{ {
var serviceContext = new TestServiceContext( var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
new HttpsConnectionFilter( {
new HttpsConnectionFilterOptions ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2 new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 })
}, }
new NoOpConnectionFilter()) };
); 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); var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("https", result); 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task DoesNotSupportTls10() public async Task DoesNotSupportTls10()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = ClientCertificateMode.RequireCertificate, {
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true ServerCertificate = _x509Certificate2,
}, ClientCertificateMode = ClientCertificateMode.RequireCertificate,
new NoOpConnectionFilter()) 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 // 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 // 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) public async Task ClientCertificateValidationGetsCalledWithNotNullParameters(ClientCertificateMode mode)
{ {
var clientCertificateValidationCalled = false; var clientCertificateValidationCalled = false;
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) =>
{ {
clientCertificateValidationCalled = true; ServerCertificate = _x509Certificate2,
Assert.NotNull(certificate); ClientCertificateMode = mode,
Assert.NotNull(chain); ClientCertificateValidation = (certificate, chain, sslPolicyErrors) =>
return true; {
} clientCertificateValidationCalled = true;
}, Assert.NotNull(certificate);
new NoOpConnectionFilter()) 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()) using (var client = new TcpClient())
{ {
@ -249,17 +271,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(ClientCertificateMode.RequireCertificate)] [InlineData(ClientCertificateMode.RequireCertificate)]
public async Task ValidationFailureRejectsConnection(ClientCertificateMode mode) public async Task ValidationFailureRejectsConnection(ClientCertificateMode mode)
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = mode, {
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => false ServerCertificate = _x509Certificate2,
}, ClientCertificateMode = mode,
new NoOpConnectionFilter()) 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()) using (var client = new TcpClient())
{ {
@ -275,16 +301,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(ClientCertificateMode.RequireCertificate)] [InlineData(ClientCertificateMode.RequireCertificate)]
public async Task RejectsConnectionOnSslPolicyErrorsWhenNoValidation(ClientCertificateMode mode) public async Task RejectsConnectionOnSslPolicyErrorsWhenNoValidation(ClientCertificateMode mode)
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = mode, {
}, ServerCertificate = _x509Certificate2,
new NoOpConnectionFilter()) 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()) 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)")] [Fact(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)")]
public async Task CertificatePassedToHttpContextIsNotDisposed() public async Task CertificatePassedToHttpContextIsNotDisposed()
{ {
var serviceContext = new TestServiceContext(new HttpsConnectionFilter( var serviceContext = new TestServiceContext();
new HttpsConnectionFilterOptions var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{ {
ServerCertificate = _x509Certificate2, new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
ClientCertificateMode = ClientCertificateMode.RequireCertificate, {
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true ServerCertificate = _x509Certificate2,
}, ClientCertificateMode = ClientCertificateMode.RequireCertificate,
new NoOpConnectionFilter()) ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
); })
}
};
RequestDelegate app = context => RequestDelegate app = context =>
{ {
@ -318,7 +352,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
return context.Response.WriteAsync("hello world"); 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 // 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 // HttpClient might not send the certificate because it is invalid or it doesn't match any

View File

@ -3,13 +3,14 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class KestrelServerInformationTests public class KestrelServerOptionsTests
{ {
#pragma warning disable CS0618 #pragma warning disable CS0618
[Fact] [Fact]
@ -39,6 +40,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
#pragma warning restore CS0612 #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] [Fact]
public void SetThreadCountUsingProcessorCount() public void SetThreadCountUsingProcessorCount()
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -55,33 +56,35 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) 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 pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
var pipeMessage = Guid.NewGuid().ToByteArray(); var pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener // Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine); var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync(); await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// Until a secondary listener is added, TCP connections get dispatched directly // 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));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
// Add secondary listener // Add secondary listener
var kestrelThreadSecondary = new KestrelThread(kestrelEngine); var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync(); await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary); await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary);
// Once a secondary listener is added, TCP connections start getting dispatched to it // 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 // TCP connections will still get round-robined to the primary listener
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync(); await listenerSecondary.DisposeAsync();
await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1));
@ -129,25 +132,26 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) 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 pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
var pipeMessage = Guid.NewGuid().ToByteArray(); var pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener // Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine); var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync(); await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// Add secondary listener // Add secondary listener
var kestrelThreadSecondary = new KestrelThread(kestrelEngine); var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync(); await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary); await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary);
// TCP Connections get round-robined // TCP Connections get round-robined
await AssertResponseEventually(address.ToString(), "Secondary", allowed: new[] { "Primary" }); await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" });
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
// Create a pipe connection and keep it open without sending any data // Create a pipe connection and keep it open without sending any data
var connectTcs = new TaskCompletionSource<object>(); var connectTcs = new TaskCompletionSource<object>();
@ -183,9 +187,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await connectTcs.Task; await connectTcs.Task;
// TCP connections will still get round-robined between only the two listeners // TCP connections will still get round-robined between only the two listeners
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
await kestrelThreadPrimary.PostAsync(_ => pipe.Dispose(), null); 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 // Same for after the non-listener pipe connection is closed
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync(); await listenerSecondary.DisposeAsync();
await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1));
@ -250,21 +254,22 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary)) 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 pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
var pipeMessage = Guid.NewGuid().ToByteArray(); var pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener // Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine); var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync(); await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary); var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary); await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// Add secondary listener with wrong pipe message // Add secondary listener with wrong pipe message
var kestrelThreadSecondary = new KestrelThread(kestrelEngine); var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync(); await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary); var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), address, kestrelThreadSecondary); await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), listenOptions, kestrelThreadSecondary);
// Wait up to 10 seconds for error to be logged // Wait up to 10 seconds for error to be logged
for (var i = 0; i < 10 && primaryTrace.Logger.TotalErrorsLogged == 0; i++) 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 // TCP Connections don't get round-robined
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString())); Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync(); await listenerSecondary.DisposeAsync();
await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1)); await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1));

View File

@ -2,6 +2,7 @@
// 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.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@ -163,8 +164,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var serverListenTcp = new UvTcpHandle(_logger); var serverListenTcp = new UvTcpHandle(_logger);
serverListenTcp.Init(loop, (a, b) => { }); serverListenTcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
serverListenTcp.Bind(address); serverListenTcp.Bind(endPoint);
var port = serverListenTcp.GetSockIPEndPoint().Port; var port = serverListenTcp.GetSockIPEndPoint().Port;
serverListenTcp.Listen(128, (handle, status, error, state) => serverListenTcp.Listen(128, (handle, status, error, state) =>
{ {

View File

@ -63,8 +63,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
loop.Init(_uv); loop.Init(_uv);
var tcp = new UvTcpHandle(_logger); var tcp = new UvTcpHandle(_logger);
tcp.Init(loop, (a, b) => { }); tcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl("http://127.0.0.1:0/"); var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
tcp.Bind(address); tcp.Bind(endPoint);
tcp.Dispose(); tcp.Dispose();
loop.Run(); loop.Run();
loop.Dispose(); loop.Dispose();
@ -78,8 +78,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
loop.Init(_uv); loop.Init(_uv);
var tcp = new UvTcpHandle(_logger); var tcp = new UvTcpHandle(_logger);
tcp.Init(loop, (a, b) => { }); tcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
tcp.Bind(address); tcp.Bind(endPoint);
var port = tcp.GetSockIPEndPoint().Port; var port = tcp.GetSockIPEndPoint().Port;
tcp.Listen(10, (stream, status, error, state) => tcp.Listen(10, (stream, status, error, state) =>
{ {
@ -107,8 +107,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
loop.Init(_uv); loop.Init(_uv);
var tcp = new UvTcpHandle(_logger); var tcp = new UvTcpHandle(_logger);
tcp.Init(loop, (a, b) => { }); tcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
tcp.Bind(address); tcp.Bind(endPoint);
var port = tcp.GetSockIPEndPoint().Port; var port = tcp.GetSockIPEndPoint().Port;
tcp.Listen(10, (_, status, error, state) => tcp.Listen(10, (_, status, error, state) =>
{ {
@ -157,8 +157,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
loop.Init(_uv); loop.Init(_uv);
var tcp = new UvTcpHandle(_logger); var tcp = new UvTcpHandle(_logger);
tcp.Init(loop, (a, b) => { }); tcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl($"http://127.0.0.1:0/"); var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
tcp.Bind(address); tcp.Bind(endPoint);
var port = tcp.GetSockIPEndPoint().Port; var port = tcp.GetSockIPEndPoint().Port;
tcp.Listen(10, (_, status, error, state) => tcp.Listen(10, (_, status, error, state) =>
{ {

View File

@ -1,9 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -16,6 +18,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var testContext = new TestServiceContext(); var testContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
PathBase = "/\u0041\u030A"
};
using (var server = new TestServer(async context => using (var server = new TestServer(async context =>
{ {
Assert.Equal("/\u0041\u030A", context.Request.PathBase.Value); Assert.Equal("/\u0041\u030A", context.Request.PathBase.Value);
@ -23,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
context.Response.Headers["Content-Length"] = new[] { "11" }; context.Response.Headers["Content-Length"] = new[] { "11" };
await context.Response.WriteAsync("Hello World"); await context.Response.WriteAsync("Hello World");
}, testContext, "http://127.0.0.1:0/\u0041\u030A")) }, testContext, listenOptions))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {

View File

@ -3,7 +3,7 @@
using System; using System;
using System.IO; 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.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;

View File

@ -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<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
{
var adapted = new AdaptedConnection(new LoggingStream(context.ConnectionStream, new TestApplicationErrorLogger()));
return Task.FromResult<IAdaptedConnection>(adapted);
}
private class AdaptedConnection : IAdaptedConnection
{
public AdaptedConnection(Stream stream)
{
ConnectionStream = stream;
}
public Stream ConnectionStream { get; }
public void PrepareRequest(IFeatureCollection requestFeatures)
{
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -2,13 +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.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
var listenerContext = new ListenerContext(serviceContext) var listenerContext = new ListenerContext(serviceContext)
{ {
ServerAddress = ServerAddress.FromUrl("http://localhost:5000") ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
}; };
var connectionContext = new ConnectionContext(listenerContext) var connectionContext = new ConnectionContext(listenerContext)
{ {

View File

@ -2,6 +2,7 @@
// 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.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Testing
RequestAbortedSource = new CancellationTokenSource(); RequestAbortedSource = new CancellationTokenSource();
ListenerContext = new ListenerContext(new ServiceContext {ServerOptions = options}) ListenerContext = new ListenerContext(new ServiceContext {ServerOptions = options})
{ {
ServerAddress = ServerAddress.FromUrl("http://localhost:5000") ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
}; };
} }

View File

@ -2,6 +2,7 @@
// 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.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal;
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Testing
{ {
private KestrelEngine _engine; private KestrelEngine _engine;
private IDisposable _server; private IDisposable _server;
private ServerAddress _address; private ListenOptions _listenOptions;
public TestServer(RequestDelegate app) public TestServer(RequestDelegate app)
: this(app, new TestServiceContext()) : this(app, new TestServiceContext())
@ -24,18 +25,24 @@ namespace Microsoft.AspNetCore.Testing
} }
public TestServer(RequestDelegate app, TestServiceContext context) 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) public TestServer(RequestDelegate app, TestServiceContext context, IHttpContextFactory httpContextFactory)
: this(app, context, serverAddress, null) : 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; Context = context;
_listenOptions = listenOptions;
context.FrameFactory = connectionContext => context.FrameFactory = connectionContext =>
{ {
@ -46,8 +53,7 @@ namespace Microsoft.AspNetCore.Testing
{ {
_engine = new KestrelEngine(context); _engine = new KestrelEngine(context);
_engine.Start(1); _engine.Start(1);
_address = ServerAddress.FromUrl(serverAddress); _server = _engine.CreateServer(_listenOptions);
_server = _engine.CreateServer(_address);
} }
catch catch
{ {
@ -57,7 +63,7 @@ namespace Microsoft.AspNetCore.Testing
} }
} }
public int Port => _address.Port; public int Port => _listenOptions.IPEndPoint.Port;
public TestServiceContext Context { get; } public TestServiceContext Context { get; }

View File

@ -4,7 +4,7 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel; 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;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
@ -26,12 +26,6 @@ namespace Microsoft.AspNetCore.Testing
ServerOptions.ShutdownTimeout = TimeSpan.FromSeconds(5); ServerOptions.ShutdownTimeout = TimeSpan.FromSeconds(5);
} }
public TestServiceContext(IConnectionFilter filter)
: this()
{
ServerOptions.ConnectionFilter = filter;
}
public string DateHeaderValue { get; } public string DateHeaderValue { get; }
public RequestDelegate App public RequestDelegate App