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.Linq;
using Benchmarks.Middleware;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
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.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthenticationSchemeProvider authSchemeProvider)
{
app.UsePlainText();
app.Run(async (context) =>
{
context.Response.ContentType = "text/plain";

View File

@ -60,4 +60,11 @@ SAFEIsDigit(UCHAR 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

View File

@ -20,9 +20,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
IHttpRequestFeature,
IHttpResponseFeature,
IHttpUpgradeFeature,
IHttpConnectionFeature,
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpAuthenticationFeature,
IServerVariablesFeature
{
@ -183,49 +181,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
bool IHttpResponseFeature.HasStarted => HasResponseStarted;
// The UpgradeAvailable Feature is set on the first request to the server.
bool IHttpUpgradeFeature.IsUpgradableRequest => _websocketAvailability == WebsocketAvailabilityStatus.Available;
bool IHttpUpgradeFeature.IsUpgradableRequest => _server.IsWebSocketAvailible(_pInProcessHandler);
bool IFeatureCollection.IsReadOnly => false;
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
{
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 ResumeWriterTheshold = PauseWriterThreshold / 2;
// TODO make this static again.
private static WebsocketAvailabilityStatus _websocketAvailability = WebsocketAvailabilityStatus.Uninitialized;
protected readonly IntPtr _pInProcessHandler;
@ -60,7 +58,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private const string NtlmString = "NTLM";
private const string NegotiateString = "Negotiate";
private const string BasicString = "Basic";
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server)
: base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler))
@ -72,103 +69,64 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
_server = server;
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();
// TODO version is slow.
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
var cookedUrl = GetCookedUrl();
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
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;
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;
User = WindowsUser;
}
}
var cookedUrl = GetCookedUrl();
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
ResetFeatureCollection();
// TODO: Avoid using long.ToString, it's pretty slow
RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
// 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;
}
if (_server.IsWebSocketAvailible(pInProcessHandler))
{
_currentIHttpUpgradeFeature = null;
}
RequestBody = new IISHttpRequestBody(this);
ResponseBody = new IISHttpResponseBody(this);
Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
var pipe = new Pipe(new PipeOptions(
_memoryPool,
readerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: PauseWriterThreshold,
resumeWriterThreshold: ResumeWriterTheshold,
minimumSegmentSize: MinAllocBufferSize));
var pipe = new Pipe(
new PipeOptions(
_memoryPool,
readerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: PauseWriterThreshold,
resumeWriterThreshold: ResumeWriterTheshold,
minimumSegmentSize: MinAllocBufferSize));
Output = new OutputProducer(pipe);
}
@ -545,12 +503,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
}
return null;
}
private enum WebsocketAvailabilityStatus
{
Uninitialized = 1,
Available,
NotAvailable
}
}
}

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal class IISHttpServer : IServer
{
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
private static NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest;
private static NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown;
private static NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion;
@ -32,8 +34,28 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private bool Stopping => _stopping == 1;
private int _outstandingRequests;
private readonly TaskCompletionSource<object> _shutdownSignal = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private bool? _websocketAvailable;
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)
{
_applicationLifetime = applicationLifetime;
@ -122,6 +144,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
var server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target;
Interlocked.Increment(ref server._outstandingRequests);
// TODO: Add try/catch and logging here
var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler);
var task = Task.Run(() => context.ProcessRequestAsync());