This commit is contained in:
Pavel Krymets 2018-04-09 10:34:44 -07:00
parent a91238d8d2
commit dca31cc6f6
8 changed files with 266 additions and 133 deletions

View File

@ -0,0 +1,51 @@
// 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Benchmarks.Middleware
{
public class PlaintextMiddleware
{
private static readonly PathString _path = new PathString("/plaintext");
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
private readonly RequestDelegate _next;
public PlaintextMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.StartsWithSegments(_path, StringComparison.Ordinal))
{
return WriteResponse(httpContext.Response);
}
return _next(httpContext);
}
public static Task WriteResponse(HttpResponse response)
{
var payloadLength = _helloWorldPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
}
}
public static class PlaintextMiddlewareExtensions
{
public static IApplicationBuilder UsePlainText(this IApplicationBuilder builder)
{
return builder.UseMiddleware<PlaintextMiddleware>();
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Benchmarks.Middleware;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -18,6 +19,7 @@ namespace NativeIISSample
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthenticationSchemeProvider authSchemeProvider) public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthenticationSchemeProvider authSchemeProvider)
{ {
app.UsePlainText();
app.Run(async (context) => app.Run(async (context) =>
{ {
context.Response.ContentType = "text/plain"; context.Response.ContentType = "text/plain";

View File

@ -60,4 +60,11 @@ SAFEIsDigit(UCHAR c)
return isdigit( c ); return isdigit( c );
} }
#define __RETURN_GLE_FAIL(str) return HRESULT_FROM_WIN32(GetLastError());
#define __RETURN_HR_FAIL(hr, str) do { HRESULT __hr = (hr); return __hr; } while (0, 0)
#define RETURN_IF_FAILED(hr) do { HRESULT __hrRet = hr; if (FAILED(__hrRet)) { __RETURN_HR_FAIL(__hrRet, #hr); }} while (0, 0)
#define RETURN_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { __RETURN_GLE_FAIL(#ptr); }} while (0, 0)
#define RETURN_IF_HANDLE_INVALID(handle) do { HANDLE __hRet = (handle); if (__hRet == INVALID_HANDLE_VALUE) { __RETURN_GLE_FAIL(#handle); }} while (0, 0)
#endif // _MACROS_H #endif // _MACROS_H

View File

@ -20,9 +20,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
IHttpRequestFeature, IHttpRequestFeature,
IHttpResponseFeature, IHttpResponseFeature,
IHttpUpgradeFeature, IHttpUpgradeFeature,
IHttpConnectionFeature,
IHttpRequestLifetimeFeature, IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpAuthenticationFeature, IHttpAuthenticationFeature,
IServerVariablesFeature IServerVariablesFeature
{ {
@ -183,49 +181,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
bool IHttpResponseFeature.HasStarted => HasResponseStarted; bool IHttpResponseFeature.HasStarted => HasResponseStarted;
// The UpgradeAvailable Feature is set on the first request to the server. bool IHttpUpgradeFeature.IsUpgradableRequest => _server.IsWebSocketAvailible(_pInProcessHandler);
bool IHttpUpgradeFeature.IsUpgradableRequest => _websocketAvailability == WebsocketAvailabilityStatus.Available;
bool IFeatureCollection.IsReadOnly => false; bool IFeatureCollection.IsReadOnly => false;
int IFeatureCollection.Revision => _featureRevision; int IFeatureCollection.Revision => _featureRevision;
IPAddress IHttpConnectionFeature.RemoteIpAddress
{
get => RemoteIpAddress;
set => RemoteIpAddress = value;
}
IPAddress IHttpConnectionFeature.LocalIpAddress
{
get => LocalIpAddress;
set => LocalIpAddress = value;
}
int IHttpConnectionFeature.RemotePort
{
get => RemotePort;
set => RemotePort = value;
}
int IHttpConnectionFeature.LocalPort
{
get => LocalPort;
set => LocalPort = value;
}
string IHttpConnectionFeature.ConnectionId
{
get => RequestConnectionId;
set => RequestConnectionId = value;
}
string IHttpRequestIdentifierFeature.TraceIdentifier
{
get => TraceIdentifier;
set => TraceIdentifier = value;
}
ClaimsPrincipal IHttpAuthenticationFeature.User ClaimsPrincipal IHttpAuthenticationFeature.User
{ {
get => User; get => User;

View File

@ -0,0 +1,100 @@
// 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.Globalization;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal partial class IISHttpContext : IHttpConnectionFeature
{
IPAddress IHttpConnectionFeature.RemoteIpAddress
{
get
{
if (RemoteIpAddress == null)
{
InitializeRemoteEndpoint();
}
return RemoteIpAddress;
}
set => RemoteIpAddress = value;
}
IPAddress IHttpConnectionFeature.LocalIpAddress
{
get
{
if (LocalIpAddress == null)
{
InitializeRemoteEndpoint();
}
return LocalIpAddress;
}
set => LocalIpAddress = value;
}
int IHttpConnectionFeature.RemotePort
{
get
{
if (RemoteIpAddress == null)
{
InitializeRemoteEndpoint();
}
return RemotePort;
}
set => RemotePort = value;
}
int IHttpConnectionFeature.LocalPort
{
get
{
if (LocalIpAddress == null)
{
InitializeRemoteEndpoint();
}
return LocalPort;
}
set => LocalPort = value;
}
string IHttpConnectionFeature.ConnectionId
{
get
{
if (RequestConnectionId == null)
{
InitializeConnectionId();
}
return RequestConnectionId;
}
set => RequestConnectionId = value;
}
private void InitializeLocalEndpoint()
{
var localEndPoint = GetLocalEndPoint();
LocalIpAddress = localEndPoint.GetIPAddress();
LocalPort = localEndPoint.GetPort();
}
private void InitializeRemoteEndpoint()
{
var remoteEndPoint = GetRemoteEndPoint();
RemoteIpAddress = remoteEndPoint.GetIPAddress();
RemotePort = remoteEndPoint.GetPort();
}
private void InitializeConnectionId()
{
RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
}
}
}

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 System;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal partial class IISHttpContext : IHttpRequestIdentifierFeature
{
string IHttpRequestIdentifierFeature.TraceIdentifier
{
get
{
if (TraceIdentifier == null)
{
InitializeHttpRequestIdentifierFeature();
}
return TraceIdentifier;
}
set => TraceIdentifier = value;
}
private unsafe void InitializeHttpRequestIdentifierFeature()
{
// Copied from WebListener
// This is the base GUID used by HTTP.SYS for generating the activity ID.
// HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
// The requestId should be set by the NativeRequestContext
var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
*((ulong*)&guid) = RequestId;
// TODO: Also make this not slow
TraceIdentifier = guid.ToString();
}
}
}

View File

@ -28,8 +28,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private const int PauseWriterThreshold = 65536; private const int PauseWriterThreshold = 65536;
private const int ResumeWriterTheshold = PauseWriterThreshold / 2; private const int ResumeWriterTheshold = PauseWriterThreshold / 2;
// TODO make this static again.
private static WebsocketAvailabilityStatus _websocketAvailability = WebsocketAvailabilityStatus.Uninitialized;
protected readonly IntPtr _pInProcessHandler; protected readonly IntPtr _pInProcessHandler;
@ -60,7 +58,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private const string NtlmString = "NTLM"; private const string NtlmString = "NTLM";
private const string NegotiateString = "Negotiate"; private const string NegotiateString = "Negotiate";
private const string BasicString = "Basic"; private const string BasicString = "Basic";
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server) internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server)
: base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler)) : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler))
@ -72,103 +69,64 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
_server = server; _server = server;
NativeMethods.HttpSetManagedContext(pInProcessHandler, (IntPtr)_thisHandle); NativeMethods.HttpSetManagedContext(pInProcessHandler, (IntPtr)_thisHandle);
unsafe Method = GetVerb();
RawTarget = GetRawUrl();
// TODO version is slow.
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
StatusCode = 200;
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes());
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal))
{ {
Method = GetVerb(); PathBase = string.Empty;
Path = string.Empty;
}
else
{
// Path and pathbase are unescaped by RequestUriBuilder
// The UsePathBase middleware will modify the pathbase and path correctly
PathBase = string.Empty;
Path = originalPath;
}
RawTarget = GetRawUrl(); var cookedUrl = GetCookedUrl();
// TODO version is slow. QueryString = cookedUrl.GetQueryString() ?? string.Empty;
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes()); RequestHeaders = new RequestHeaders(this);
HttpResponseHeaders = new HeaderCollection();
ResponseHeaders = HttpResponseHeaders;
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) if (options.ForwardWindowsAuthentication)
{
WindowsUser = GetWindowsPrincipal();
if (options.AutomaticAuthentication)
{ {
PathBase = string.Empty; User = WindowsUser;
Path = string.Empty;
}
else
{
// Path and pathbase are unescaped by RequestUriBuilder
// The UsePathBase middleware will modify the pathbase and path correctly
PathBase = string.Empty;
Path = originalPath;
} }
}
var cookedUrl = GetCookedUrl(); ResetFeatureCollection();
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
// TODO: Avoid using long.ToString, it's pretty slow if (_server.IsWebSocketAvailible(pInProcessHandler))
RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture); {
_currentIHttpUpgradeFeature = null;
// Copied from WebListener
// This is the base GUID used by HTTP.SYS for generating the activity ID.
// HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
// The requestId should be set by the NativeRequestContext
var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
*((ulong*)&guid) = RequestId;
// TODO: Also make this not slow
TraceIdentifier = guid.ToString();
var localEndPoint = GetLocalEndPoint();
LocalIpAddress = localEndPoint.GetIPAddress();
LocalPort = localEndPoint.GetPort();
var remoteEndPoint = GetRemoteEndPoint();
RemoteIpAddress = remoteEndPoint.GetIPAddress();
RemotePort = remoteEndPoint.GetPort();
StatusCode = 200;
RequestHeaders = new RequestHeaders(this);
HttpResponseHeaders = new HeaderCollection();
ResponseHeaders = HttpResponseHeaders;
if (options.ForwardWindowsAuthentication)
{
WindowsUser = GetWindowsPrincipal();
if (options.AutomaticAuthentication)
{
User = WindowsUser;
}
}
ResetFeatureCollection();
// Check if the Http upgrade feature is available in IIS.
// To check this, we can look at the server variable WEBSOCKET_VERSION
// And see if there is a version. Same check that Katana did:
// https://github.com/aspnet/AspNetKatana/blob/9f6e09af6bf203744feb5347121fe25f6eec06d8/src/Microsoft.Owin.Host.SystemWeb/OwinAppContext.cs#L125
// Actively not locking here as acquiring a lock on every request will hurt perf more than checking the
// server variables a few extra times if a bunch of requests hit the server at the same time.
if (_websocketAvailability == WebsocketAvailabilityStatus.Uninitialized)
{
var webSocketsAvailable = NativeMethods.HttpTryGetServerVariable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported)
&& !string.IsNullOrEmpty(webSocketsSupported);
_websocketAvailability = webSocketsAvailable ?
WebsocketAvailabilityStatus.Available :
WebsocketAvailabilityStatus.NotAvailable;
}
if (_websocketAvailability == WebsocketAvailabilityStatus.NotAvailable)
{
_currentIHttpUpgradeFeature = null;
}
} }
RequestBody = new IISHttpRequestBody(this); RequestBody = new IISHttpRequestBody(this);
ResponseBody = new IISHttpResponseBody(this); ResponseBody = new IISHttpResponseBody(this);
Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize)); Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
var pipe = new Pipe(new PipeOptions( var pipe = new Pipe(
_memoryPool, new PipeOptions(
readerScheduler: PipeScheduler.ThreadPool, _memoryPool,
pauseWriterThreshold: PauseWriterThreshold, readerScheduler: PipeScheduler.ThreadPool,
resumeWriterThreshold: ResumeWriterTheshold, pauseWriterThreshold: PauseWriterThreshold,
minimumSegmentSize: MinAllocBufferSize)); resumeWriterThreshold: ResumeWriterTheshold,
minimumSegmentSize: MinAllocBufferSize));
Output = new OutputProducer(pipe); Output = new OutputProducer(pipe);
} }
@ -545,12 +503,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
} }
return null; return null;
} }
private enum WebsocketAvailabilityStatus
{
Uninitialized = 1,
Available,
NotAvailable
}
} }
} }

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
{ {
internal class IISHttpServer : IServer internal class IISHttpServer : IServer
{ {
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
private static NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest; private static NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest;
private static NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown; private static NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown;
private static NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion; private static NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion;
@ -32,8 +34,28 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private bool Stopping => _stopping == 1; private bool Stopping => _stopping == 1;
private int _outstandingRequests; private int _outstandingRequests;
private readonly TaskCompletionSource<object> _shutdownSignal = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource<object> _shutdownSignal = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private bool? _websocketAvailable;
public IFeatureCollection Features { get; } = new FeatureCollection(); public IFeatureCollection Features { get; } = new FeatureCollection();
// TODO: Remove pInProcessHandler argument
public bool IsWebSocketAvailible(IntPtr pInProcessHandler)
{
// Check if the Http upgrade feature is available in IIS.
// To check this, we can look at the server variable WEBSOCKET_VERSION
// And see if there is a version. Same check that Katana did:
// https://github.com/aspnet/AspNetKatana/blob/9f6e09af6bf203744feb5347121fe25f6eec06d8/src/Microsoft.Owin.Host.SystemWeb/OwinAppContext.cs#L125
// Actively not locking here as acquiring a lock on every request will hurt perf more than checking the
// server variables a few extra times if a bunch of requests hit the server at the same time.
if (!_websocketAvailable.HasValue)
{
_websocketAvailable = NativeMethods.HttpTryGetServerVariable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported)
&& !string.IsNullOrEmpty(webSocketsSupported);
}
return _websocketAvailable.Value;
}
public IISHttpServer(IApplicationLifetime applicationLifetime, IAuthenticationSchemeProvider authentication, IOptions<IISOptions> options) public IISHttpServer(IApplicationLifetime applicationLifetime, IAuthenticationSchemeProvider authentication, IOptions<IISOptions> options)
{ {
_applicationLifetime = applicationLifetime; _applicationLifetime = applicationLifetime;
@ -122,6 +144,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
var server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target; var server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target;
Interlocked.Increment(ref server._outstandingRequests); Interlocked.Increment(ref server._outstandingRequests);
// TODO: Add try/catch and logging here
var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler); var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler);
var task = Task.Run(() => context.ProcessRequestAsync()); var task = Task.Run(() => context.ProcessRequestAsync());