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

View File

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

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
{
public class HttpsConnectionFilterOptions
public class HttpsConnectionAdapterOptions
{
public HttpsConnectionFilterOptions()
public HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
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.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;
public FilteredStreamAdapter(
public AdaptedPipeline(
string connectionId,
Stream filteredStream,
MemoryPool memory,

View File

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

View File

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

View File

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

View File

@ -31,7 +31,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public IPEndPoint LocalEndPoint { 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Adapter;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ServerOptions = context.ListenerContext.ServiceContext.ServerOptions;
_pathBase = ServerAddress.PathBase;
_pathBase = context.ListenerContext.ListenOptions.PathBase;
FrameControl = this;
_keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds;
@ -97,23 +98,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public ConnectionContext ConnectionContext { get; }
public SocketInput Input { get; set; }
public ISocketOutput Output { get; set; }
public Action<IFeatureCollection> PrepareRequest
{
get
{
return ConnectionContext.PrepareRequest;
}
set
{
ConnectionContext.PrepareRequest = value;
}
}
public IEnumerable<IAdaptedConnection> AdaptedConnections { get; set; }
protected IConnectionControl ConnectionControl => ConnectionContext.ConnectionControl;
protected IKestrelTrace Log => ConnectionContext.ListenerContext.ServiceContext.Log;
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
private KestrelServerOptions ServerOptions { get; }
private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint;
@ -367,7 +357,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
LocalPort = LocalEndPoint?.Port ?? 0;
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;
_abortedCts = null;

View File

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

View File

@ -1,6 +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 Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public class ListenerContext
@ -12,10 +15,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public ServiceContext ServiceContext { get; set; }
public ServerAddress ServerAddress { get; set; }
public ListenOptions ListenOptions { 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
/// connections may be passed to a secondary listener to handle.
/// </summary>
public abstract class ListenerPrimary : Listener
public class ListenerPrimary : Listener
{
private readonly List<UvPipeHandle> _dispatchPipes = new List<UvPipeHandle>();
private int _dispatchIndex;
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// 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 }) });
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(
string pipeName,
byte[] pipeMessage,
ServerAddress address,
ListenOptions listenOptions,
KestrelThread thread)
{
_pipeName = pipeName;
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false);
}
await StartAsync(address, thread).ConfigureAwait(false);
await StartAsync(listenOptions, thread).ConfigureAwait(false);
await Thread.PostAsync(state => ((ListenerPrimary)state).PostCallback(),
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
/// UNIX domain socket.
/// </summary>
public abstract class ListenerSecondary : ListenerContext, IAsyncDisposable
public class ListenerSecondary : ListenerContext, IAsyncDisposable
{
private string _pipeName;
private byte[] _pipeMessage;
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private Libuv.uv_buf_t _buf;
private bool _closed;
protected ListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
public ListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
{
_ptr = Marshal.AllocHGlobal(4);
}
@ -35,14 +35,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public Task StartAsync(
string pipeName,
byte[] pipeMessage,
ServerAddress address,
ListenOptions listenOptions,
KestrelThread thread)
{
_pipeName = pipeName;
_pipeMessage = pipeMessage;
_buf = thread.Loop.Libuv.buf_init(_ptr, 4);
ServerAddress = address;
ListenOptions = listenOptions;
Thread = thread;
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()
{
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>
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";
private static int? GetECONNRESET()

View File

@ -61,49 +61,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
#endif
}
public IDisposable CreateServer(ServerAddress address)
public IDisposable CreateServer(ListenOptions listenOptions)
{
var listeners = new List<IAsyncDisposable>();
var usingPipes = address.IsUnixPipe;
try
{
var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
var pipeMessage = Guid.NewGuid().ToByteArray();
var single = Threads.Count == 1;
var first = true;
foreach (var thread in Threads)
if (Threads.Count == 1)
{
if (single)
{
var listener = usingPipes ?
(Listener) new PipeListener(ServiceContext) :
new TcpListener(ServiceContext);
listeners.Add(listener);
listener.StartAsync(address, thread).Wait();
}
else if (first)
{
var listener = usingPipes
? (ListenerPrimary) new PipeListenerPrimary(ServiceContext)
: new TcpListenerPrimary(ServiceContext);
var listener = new Listener(ServiceContext);
listeners.Add(listener);
listener.StartAsync(listenOptions, Threads[0]).Wait();
}
else
{
var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
var pipeMessage = Guid.NewGuid().ToByteArray();
listeners.Add(listener);
listener.StartAsync(pipeName, pipeMessage, address, thread).Wait();
}
else
{
var listener = usingPipes
? (ListenerSecondary) new PipeListenerSecondary(ServiceContext)
: new TcpListenerSecondary(ServiceContext);
listeners.Add(listener);
listener.StartAsync(pipeName, pipeMessage, address, thread).Wait();
}
var listenerPrimary = new ListenerPrimary(ServiceContext);
listeners.Add(listenerPrimary);
listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, Threads[0]).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(() =>
@ -114,7 +98,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
catch
{
DisposeListeners(listeners);
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
// 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.
// 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
//
@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
// Reference:
// - 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 (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
// 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;
}
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.
using System;
using System.Diagnostics;
using System.Net;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
@ -24,20 +25,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_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;
var addressText = endpoint.Address.ToString();
var addressText = endPoint.Address.ToString();
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)
{
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)
{
throw error1;
@ -65,38 +69,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
return socketAddress.GetIPEndPoint();
}
public void Open(IntPtr hSocket)
{
_uv.tcp_open(this, hSocket);
}
public void NoDelay(bool 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.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
@ -113,84 +114,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel
engine.Start(threadCount);
var atLeastOneListener = false;
foreach (var address in _serverAddresses.Addresses.ToArray())
var listenOptions = Options.ListenOptions;
if (listenOptions.Any())
{
var parsedAddress = ServerAddress.FromUrl(address);
atLeastOneListener = true;
if (!parsedAddress.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
var addresses = _serverAddresses.Addresses;
if (addresses.SingleOrDefault() != "http://localhost:5000")
{
try
{
_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);
}
var joined = string.Join(", ", addresses);
throw new NotSupportedException($"Specifying address(es) '{joined}' is incompatible with also configuring endpoint(s) in UseKestrel.");
}
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");
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));
}
throw;
}
// If requested port was "0", replace with assigned dynamic port.
_serverAddresses.Addresses.Remove(address);
_serverAddresses.Addresses.Add(parsedAddress.ToString());
_serverAddresses.Addresses.Add(endPoint.ToString());
}
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}).");
}
}
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 Microsoft.AspNetCore.Server.Kestrel.Filter;
using System.Collections.Generic;
using System.Net;
namespace Microsoft.AspNetCore.Server.Kestrel
{
@ -8,6 +12,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// </summary>
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>
/// Gets or sets whether the <c>Server</c> header should be included in each response.
/// </summary>
@ -17,22 +29,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel
public bool AddServerHeader { get; set; } = true;
/// <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})"/>.
/// </summary>
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>
/// <para>
/// This property is obsolete and will be removed in a future version.
@ -64,14 +65,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// </summary>
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>
/// 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
@ -115,5 +108,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel
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": {
"dependencies": {
"System.Diagnostics.Process": "4.4.0-*",
"System.Threading.Thread": "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.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
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)
{
await RegisterAddresses_Success(addressInput, testUrls);
@ -35,14 +36,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
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))]
[PortSupportedCondition(443)]
public async Task RegisterAddresses_IPv4Port443_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
[Theory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(IPEndPointRegistrationDataRandomPort))]
[IPv6SupportedCondition]
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]
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);
}
[ConditionalTheory(Skip = "SslStream hanging on write after update to CoreFx 4.4 (https://github.com/dotnet/corefx/issues/14698)"), MemberData(nameof(AddressRegistrationDataIPv6Port443))]
[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))]
[ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6ScopeId))]
[IPv6SupportedCondition]
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)
{
var hostBuilder = new WebHostBuilder()
.UseKestrel(options =>
{
options.UseHttps(@"TestResources/testCert.pfx", "testPassword");
})
.UseKestrel()
.UseUrls(addressInput)
.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]
public void ThrowsWhenBindingToIPv4AddressInUse()
{
@ -200,37 +229,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" });
// Static ports
var port1 = GetNextPort();
var port2 = GetNextPort();
var port = GetNextPort();
// Loopback
dataset.Add($"http://127.0.0.1:{port1};https://127.0.0.1:{port2}",
_ => new[] { $"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}/" });
// Localhost
dataset.Add($"http://localhost:{port1};https://localhost:{port2}",
_ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/",
$"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/" });
dataset.Add($"http://localhost:{port}", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/" });
// Any
dataset.Add($"http://*:{port1}/;https://*:{port2}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"https://127.0.0.1:{port2}/" });
dataset.Add($"http://+:{port1}/;https://+:{port2}/",
_ => 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://+:{port}/", _ => new[] { $"http://127.0.0.1:{port}/" });
// Path after port
dataset.Add($"http://127.0.0.1:{port1}/base/path;https://127.0.0.1:{port2}/base/path",
_ => new[] { $"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" });
// Dynamic port and non-loopback addresses
dataset.Add("http://127.0.0.1:0/;https://127.0.0.1:0/", GetTestUrls);
dataset.Add($"http://{Dns.GetHostName()}:0/;https://{Dns.GetHostName()}:0/", GetTestUrls);
dataset.Add("http://127.0.0.1:0/", GetTestUrls);
dataset.Add($"http://{Dns.GetHostName()}:0/", GetTestUrls);
var ipv4Addresses = GetIPAddresses()
.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork);
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;
@ -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
{
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();
var dataset = new TheoryData<IPEndPoint, Func<IPEndPoint, string>>();
// Default port for HTTPS (443)
dataset.Add("https://127.0.0.1", _ => new[] { "https://127.0.0.1/" });
dataset.Add("https://localhost", _ => new[] { "https://127.0.0.1/" });
dataset.Add("https://*", _ => new[] { "https://127.0.0.1/" });
dataset.Add(new IPEndPoint(IPAddress.Loopback, 443), _ => "https://127.0.0.1/");
dataset.Add(new IPEndPoint(IPAddress.IPv6Loopback, 443), _ => FixTestUrl("https://[::1]/"));
dataset.Add(new IPEndPoint(IPAddress.Any, 443), _ => "https://127.0.0.1/");
dataset.Add(new IPEndPoint(IPAddress.IPv6Any, 443), _ => FixTestUrl("https://[::1]/"));
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/" });
// Static ports
var port1 = GetNextPort();
var port2 = GetNextPort();
var port = GetNextPort();
// Loopback
dataset.Add($"http://[::1]:{port1}/;https://[::1]:{port2}/",
_ => new[] { $"http://[::1]:{port1}/", $"https://[::1]:{port2}/" });
dataset.Add($"http://[::1]:{port}/",
_ => new[] { $"http://[::1]:{port}/" });
// Localhost
dataset.Add($"http://localhost:{port1};https://localhost:{port2}",
_ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/",
$"https://localhost:{port2}/", $"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
dataset.Add($"http://localhost:{port}",
_ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
// Any
dataset.Add($"http://*:{port1}/;https://*:{port2}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/",
$"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
dataset.Add($"http://+:{port1}/;https://+:{port2}/",
_ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/",
$"https://127.0.0.1:{port2}/", $"https://[::1]:{port2}/" });
dataset.Add($"http://*:{port}/",
_ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
dataset.Add($"http://+:{port}/",
_ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{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}/",
_ => new[] { $"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:{port}/", $"http://[::1]:{port}/" });
// Path after port
dataset.Add($"http://[::1]:{port1}/base/path;https://[::1]:{port2}/base/path",
_ => new[] { $"http://[::1]:{port1}/base/path", $"https://[::1]:{port2}/base/path" });
dataset.Add($"http://[::1]:{port}/base/path",
_ => new[] { $"http://[::1]:{port}/base/path" });
// Dynamic port and non-loopback addresses
var ipv6Addresses = GetIPAddresses()
@ -313,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
.Where(ip => ip.ScopeId == 0);
foreach (var ip in ipv6Addresses)
{
dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls);
dataset.Add($"http://[{ip}]:0/", GetTestUrls);
}
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
{
get
@ -362,7 +407,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
.Where(ip => ip.ScopeId != 0);
foreach (var ip in ipv6Addresses)
{
dataset.Add($"http://[{ip}]:0/;https://[{ip}]:0/", GetTestUrls);
dataset.Add($"http://[{ip}]:0/", GetTestUrls);
}
return dataset;
@ -380,11 +425,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
private static string[] GetTestUrls(IServerAddressesFeature addressesFeature)
{
return addressesFeature.Addresses
.Select(a => a.Replace("://+", "://localhost"))
.Select(a => a.EndsWith("/") ? a : a + "/")
.Select(FixTestUrl)
.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)
{
app.Run(context =>

View File

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

View File

@ -30,9 +30,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder()
.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)
.Configure(app => { });
@ -61,9 +63,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder()
.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)
.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)")]
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 hostBuilder = new WebHostBuilder()
.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)
@ -141,14 +148,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted()
{
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 hostBuilder = new WebHostBuilder()
.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)
.Configure(app => app.Run(async httpContext =>
{
@ -194,9 +203,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var hostBuilder = new WebHostBuilder()
.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)
.Configure(app => { });
@ -221,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public ILogger CreateLogger(string categoryName)
{
if (categoryName == nameof(HttpsConnectionFilter))
if (categoryName == nameof(HttpsConnectionAdapter))
{
return FilterLogger;
}

View File

@ -10,9 +10,9 @@ namespace Microsoft.AspNetCore.Hosting
{
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)
@ -37,13 +37,29 @@ namespace Microsoft.AspNetCore.Hosting
public static IEnumerable<Uri> GetUris(this IWebHost host)
{
return host.ServerFeatures.Get<IServerAddressesFeature>().Addresses
.Select(a => a.Replace("://+", "://localhost"))
.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.
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -11,18 +12,20 @@ using Xunit;
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)")]
public async Task LoggingConnectionFilterCanBeAddedBeforeAndAfterHttpsFilter()
{
var host = new WebHostBuilder()
.UseUrls($"https://127.0.0.1:0")
.UseKestrel(options =>
{
options.UseConnectionLogging();
options.UseHttps(@"TestResources/testCert.pfx", "testPassword");
})
.UseKestrel(options =>
{
options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions =>
{
listenOptions.UseConnectionLogging();
listenOptions.UseHttps("TestResources/testCert.pfx", "testPassword");
});
})
.Configure(app =>
{
app.Run(context =>

View File

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

View File

@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
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)
{

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

View File

@ -4,9 +4,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
@ -16,21 +18,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
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>
{
{
new TestServiceContext()
},
{
new TestServiceContext(new PassThroughConnectionFilter())
}
};
ConnectionAdapters = { new PassThroughConnectionAdapter() }
}
}
};
private async Task App(HttpContext httpContext)
{
@ -61,10 +56,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task Http10TransferEncoding(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
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())
{
@ -88,10 +85,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task Http10KeepAliveTransferEncoding(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
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())
{
@ -127,9 +126,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
@ -140,7 +141,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -177,9 +178,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task TrailingHeadersAreParsed(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersAreParsed(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
var requestCount = 10;
var requestsReceived = 0;
@ -211,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext))
}, testContext, listenOptions))
{
var response = string.Join("\r\n", new string[] {
"HTTP/1.1 200 OK",
@ -262,13 +264,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(ListenOptions listenOptions)
{
const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
const string headerLine = "Header: value";
const string trailingHeaderLine = "Trailing-Header: trailing-value";
var testContext = new TestServiceContext();
testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize =
transferEncodingHeaderLine.Length + 2 +
headerLine.Length + 2 +
@ -278,7 +281,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
var buffer = new byte[128];
while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -305,20 +308,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task TrailingHeadersCountTowardsHeaderCountLimit(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task TrailingHeadersCountTowardsHeaderCountLimit(ListenOptions listenOptions)
{
const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
const string headerLine = "Header: value";
const string trailingHeaderLine = "Trailing-Header: trailing-value";
var testContext = new TestServiceContext();
testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2;
using (var server = new TestServer(async context =>
{
var buffer = new byte[128];
while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -345,9 +349,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ExtensionsAreIgnored(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ExtensionsAreIgnored(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
var requestCount = 10;
var requestsReceived = 0;
@ -379,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext))
}, testContext, listenOptions))
{
var response = string.Join("\r\n", new string[] {
"HTTP/1.1 200 OK",
@ -430,9 +435,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task InvalidLengthResultsIn400(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task InvalidLengthResultsIn400(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
@ -448,7 +454,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -472,9 +478,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task InvalidSizedDataResultsIn400(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task InvalidSizedDataResultsIn400(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
@ -490,7 +497,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
response.Headers["Content-Length"] = new[] { "11" };
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -516,13 +523,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ChunkedNotFinalTransferCodingResultsIn400(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ChunkedNotFinalTransferCodingResultsIn400(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(httpContext =>
{
return TaskCache.CompletedTask;
}, testContext))
}, testContext, listenOptions))
{
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.
using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing;
using Xunit;
@ -14,32 +16,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
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>
{
{
new TestServiceContext()
},
{
new TestServiceContext(new PassThroughConnectionFilter())
}
};
ConnectionAdapters = { new PassThroughConnectionAdapter() }
}
}
};
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ResponsesAreChunkedAutomatically(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreChunkedAutomatically(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -64,14 +61,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!");
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -91,14 +90,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!");
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -125,15 +126,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Connection"] = "close";
await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!");
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -159,16 +162,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ZeroLengthWritesAreIgnored(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroLengthWritesAreIgnored(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
await response.Body.WriteAsync(new byte[0], 0, 0);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -193,9 +198,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ZeroLengthWritesFlushHeaders(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ZeroLengthWritesFlushHeaders(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
var flushed = new SemaphoreSlim(0, 1);
using (var server = new TestServer(async httpContext =>
@ -206,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await flushed.WaitAsync();
await response.WriteAsync("Hello World!");
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -235,14 +242,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.Body.WriteAsync(new byte[0], 0, 0);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -263,15 +272,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ConnectionClosedIfExceptionThrownAfterWrite(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedIfExceptionThrownAfterWrite(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12);
throw new Exception();
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -294,15 +305,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.Body.WriteAsync(new byte[0], 0, 0);
throw new Exception();
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -324,9 +337,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task WritesAreFlushedPriorToResponseCompletion(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task WritesAreFlushedPriorToResponseCompletion(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
var flushWh = new ManualResetEventSlim();
using (var server = new TestServer(async httpContext =>
@ -338,7 +353,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
flushWh.Wait();
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
@ -368,9 +383,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ChunksCanBeWrittenManually(TestServiceContext testContext)
[MemberData(nameof(ConnectionAdapterData))]
public async Task ChunksCanBeWrittenManually(ListenOptions listenOptions)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async httpContext =>
{
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\nWorld!\r\n"), 0, 11);
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5);
}, testContext))
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{

View File

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

View File

@ -1,6 +1,7 @@
// 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.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
};
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]
};

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
@ -13,33 +14,33 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
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.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
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
// 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)")]
public async Task CanReadAndWriteWithHttpsConnectionFilter()
public async Task CanReadAndWriteWithHttpsConnectionAdapter()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions { ServerCertificate = _x509Certificate2 },
new NoOpConnectionFilter())
);
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
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}/",
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)")]
public async Task RequireCertificateFailsWhenNoCertificate()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
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>(
() => 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)")]
public async Task AllowCertificateContinuesWhenNoCertificate()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.AllowCertificate
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.AllowCertificate
})
}
};
using (var server = new TestServer(context =>
{
Assert.Equal(context.Features.Get<ITlsConnectionFeature>(), null);
return context.Response.WriteAsync("hello world");
},
serviceContext, _serverAddress))
serviceContext, listenOptions))
{
var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
@ -97,24 +107,24 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Fact]
public void ThrowsWhenNoServerCertificateIsProvided()
{
Assert.Throws<ArgumentException>(() => new HttpsConnectionFilter(
new HttpsConnectionFilterOptions(),
new NoOpConnectionFilter())
Assert.Throws<ArgumentException>(() => new HttpsConnectionAdapter(
new HttpsConnectionAdapterOptions())
);
}
[Fact]
public async Task UsesProvidedServerCertificate()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 })
}
};
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())
{
@ -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)")]
public async Task CertificatePassedToHttpContext()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
})
}
};
using (var server = new TestServer(context =>
{
@ -150,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.NotNull(context.Connection.ClientCertificate);
return context.Response.WriteAsync("hello world");
},
serviceContext, _serverAddress))
serviceContext, listenOptions))
{
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)")]
public async Task HttpsSchemePassedToRequestFeature()
{
var serviceContext = new TestServiceContext(
new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
{
ServerCertificate = _x509Certificate2
},
new NoOpConnectionFilter())
);
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 })
}
};
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);
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)")]
public async Task DoesNotSupportTls10()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
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
// 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)
{
var clientCertificateValidationCalled = false;
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) =>
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
clientCertificateValidationCalled = true;
Assert.NotNull(certificate);
Assert.NotNull(chain);
return true;
}
},
new NoOpConnectionFilter())
);
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) =>
{
clientCertificateValidationCalled = true;
Assert.NotNull(certificate);
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())
{
@ -249,17 +271,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(ClientCertificateMode.RequireCertificate)]
public async Task ValidationFailureRejectsConnection(ClientCertificateMode mode)
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => false
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
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())
{
@ -275,16 +301,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(ClientCertificateMode.RequireCertificate)]
public async Task RejectsConnectionOnSslPolicyErrorsWhenNoValidation(ClientCertificateMode mode)
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
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())
{
@ -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)")]
public async Task CertificatePassedToHttpContextIsNotDisposed()
{
var serviceContext = new TestServiceContext(new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
var serviceContext = new TestServiceContext();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters =
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
},
new NoOpConnectionFilter())
);
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
})
}
};
RequestDelegate app = context =>
{
@ -318,7 +352,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
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
// 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.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Server.Kestrel;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class KestrelServerInformationTests
public class KestrelServerOptionsTests
{
#pragma warning disable CS0618
[Fact]
@ -39,6 +40,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
#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]
public void SetThreadCountUsingProcessorCount()
{

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
@ -55,33 +56,35 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
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 pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary);
var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// 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.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
// Add secondary listener
var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary);
var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary);
// 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
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync();
await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1));
@ -129,25 +132,26 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
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 pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary);
var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// Add secondary listener
var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, address, kestrelThreadSecondary);
var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadSecondary);
// TCP Connections get round-robined
await AssertResponseEventually(address.ToString(), "Secondary", allowed: new[] { "Primary" });
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" });
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
// Create a pipe connection and keep it open without sending any data
var connectTcs = new TaskCompletionSource<object>();
@ -183,9 +187,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await connectTcs.Task;
// TCP connections will still get round-robined between only the two listeners
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
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
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync();
await kestrelThreadSecondary.StopAsync(TimeSpan.FromSeconds(1));
@ -250,21 +254,22 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
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 pipeMessage = Guid.NewGuid().ToByteArray();
// Start primary listener
var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
await kestrelThreadPrimary.StartAsync();
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, address, kestrelThreadPrimary);
var listenerPrimary = new ListenerPrimary(serviceContextPrimary);
await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, kestrelThreadPrimary);
var address = listenOptions.ToString();
// Add secondary listener with wrong pipe message
var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
await kestrelThreadSecondary.StartAsync();
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), address, kestrelThreadSecondary);
var listenerSecondary = new ListenerSecondary(serviceContextSecondary);
await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), listenOptions, kestrelThreadSecondary);
// Wait up to 10 seconds for error to be logged
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
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address));
await listenerSecondary.DisposeAsync();
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.
using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
@ -163,8 +164,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var serverListenTcp = new UvTcpHandle(_logger);
serverListenTcp.Init(loop, (a, b) => { });
var address = ServerAddress.FromUrl($"http://127.0.0.1:0/");
serverListenTcp.Bind(address);
var endPoint = new IPEndPoint(IPAddress.Loopback, 0);
serverListenTcp.Bind(endPoint);
var port = serverListenTcp.GetSockIPEndPoint().Port;
serverListenTcp.Listen(128, (handle, status, error, state) =>
{

View File

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

View File

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

View File

@ -3,7 +3,7 @@
using System;
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.Testing;
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.
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
};
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.
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel;
@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Testing
RequestAbortedSource = new CancellationTokenSource();
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.
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Testing
{
private KestrelEngine _engine;
private IDisposable _server;
private ServerAddress _address;
private ListenOptions _listenOptions;
public TestServer(RequestDelegate app)
: this(app, new TestServiceContext())
@ -24,18 +25,24 @@ namespace Microsoft.AspNetCore.Testing
}
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)
: this(app, context, serverAddress, null)
public TestServer(RequestDelegate app, TestServiceContext context, IHttpContextFactory httpContextFactory)
: 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;
_listenOptions = listenOptions;
context.FrameFactory = connectionContext =>
{
@ -46,8 +53,7 @@ namespace Microsoft.AspNetCore.Testing
{
_engine = new KestrelEngine(context);
_engine.Start(1);
_address = ServerAddress.FromUrl(serverAddress);
_server = _engine.CreateServer(_address);
_server = _engine.CreateServer(_listenOptions);
}
catch
{
@ -57,7 +63,7 @@ namespace Microsoft.AspNetCore.Testing
}
}
public int Port => _address.Port;
public int Port => _listenOptions.IPEndPoint.Port;
public TestServiceContext Context { get; }

View File

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