// 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.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipelines; using System.Net; using System.Runtime.InteropServices; using System.Security.Claims; using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; namespace Microsoft.AspNetCore.Server.IISIntegration { internal abstract partial class IISHttpContext : NativeRequestContext, IDisposable { private const int MinAllocBufferSize = 2048; 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; private bool _reading; // To know whether we are currently in a read operation. private volatile bool _hasResponseStarted; private int _statusCode; private string _reasonPhrase; private readonly object _onStartingSync = new object(); private readonly object _onCompletedSync = new object(); private readonly object _stateSync = new object(); protected readonly object _createReadWriteBodySync = new object(); protected Stack, object>> _onStarting; protected Stack, object>> _onCompleted; protected Exception _applicationException; private readonly MemoryPool _memoryPool; private readonly IISHttpServer _server; private GCHandle _thisHandle; private MemoryHandle _inputHandle; private IISAwaitable _operation = new IISAwaitable(); protected Task _processBodiesTask; protected int _requestAborted; 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 memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server) : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.http_get_raw_request(pInProcessHandler)) { _thisHandle = GCHandle.Alloc(this); _memoryPool = memoryPool; _pInProcessHandler = pInProcessHandler; _server = server; NativeMethods.http_set_managed_context(pInProcessHandler, (IntPtr)_thisHandle); unsafe { Method = GetVerb(); RawTarget = GetRawUrl(); // TODO version is slow. HttpVersion = GetVersion(); Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme; KnownMethod = VerbId; var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes()); if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) { 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; } var cookedUrl = GetCookedUrl(); QueryString = cookedUrl.GetQueryString() ?? string.Empty; // 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) { NativeMethods.http_get_server_variable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported); var webSocketsAvailable = !string.IsNullOrEmpty(webSocketsSupported); _websocketAvailability = webSocketsAvailable ? WebsocketAvailabilityStatus.Available : WebsocketAvailabilityStatus.NotAvailable; } if (_websocketAvailability == WebsocketAvailabilityStatus.NotAvailable) { _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)); Output = new OutputProducer(pipe); } public Version HttpVersion { get; set; } public string Scheme { get; set; } public string Method { get; set; } public string PathBase { get; set; } public string Path { get; set; } public string QueryString { get; set; } public string RawTarget { get; set; } public CancellationToken RequestAborted { get; set; } public bool HasResponseStarted => _hasResponseStarted; public IPAddress RemoteIpAddress { get; set; } public int RemotePort { get; set; } public IPAddress LocalIpAddress { get; set; } public int LocalPort { get; set; } public string RequestConnectionId { get; set; } public string TraceIdentifier { get; set; } public ClaimsPrincipal User { get; set; } internal WindowsPrincipal WindowsUser { get; set; } public Stream RequestBody { get; set; } public Stream ResponseBody { get; set; } public Pipe Input { get; set; } public OutputProducer Output { get; set; } public IHeaderDictionary RequestHeaders { get; set; } public IHeaderDictionary ResponseHeaders { get; set; } private HeaderCollection HttpResponseHeaders { get; set; } internal HttpApiTypes.HTTP_VERB KnownMethod { get; } public int StatusCode { get { return _statusCode; } set { if (_hasResponseStarted) { ThrowResponseAlreadyStartedException(nameof(StatusCode)); } _statusCode = (ushort)value; } } public string ReasonPhrase { get { return _reasonPhrase; } set { if (_hasResponseStarted) { ThrowResponseAlreadyStartedException(nameof(ReasonPhrase)); } _reasonPhrase = value; } } internal IISHttpServer Server { get { return _server; } } private async Task InitializeResponseAwaited() { await FireOnStarting(); if (_applicationException != null) { ThrowResponseAbortedException(); } await ProduceStart(appCompleted: false); } private void ThrowResponseAbortedException() { throw new ObjectDisposedException("Unhandled application exception", _applicationException); } private async Task ProduceStart(bool appCompleted) { if (_hasResponseStarted) { return; } _hasResponseStarted = true; SendResponseHeaders(appCompleted); // On first flush for websockets, we need to flush the headers such that // IIS will know that an upgrade occured. // If we don't have anything on the Output pipe, the TryRead in ReadAndWriteLoopAsync // will fail and we will signal the upgradeTcs that we are upgrading. However, we still // didn't flush. To fix this, we flush 0 bytes right after writing the headers. Task flushTask; lock (_stateSync) { DisableReads(); flushTask = Output.FlushAsync(); } await flushTask; StartProcessingRequestAndResponseBody(); } protected Task ProduceEnd() { if (_applicationException != null) { if (_hasResponseStarted) { // We can no longer change the response, so we simply close the connection. return Task.CompletedTask; } // If the request was rejected, the error state has already been set by SetBadRequestState and // that should take precedence. else { // 500 Internal Server Error SetErrorResponseHeaders(statusCode: StatusCodes.Status500InternalServerError); } } if (!_hasResponseStarted) { return ProduceEndAwaited(); } return Task.CompletedTask; } private void SetErrorResponseHeaders(int statusCode) { StatusCode = statusCode; ReasonPhrase = string.Empty; HttpResponseHeaders.Clear(); } private async Task ProduceEndAwaited() { if (_hasResponseStarted) { return; } _hasResponseStarted = true; SendResponseHeaders(appCompleted: true); StartProcessingRequestAndResponseBody(); Task flushAsync; lock (_stateSync) { DisableReads(); flushAsync = Output.FlushAsync(); } await flushAsync; } public unsafe void SendResponseHeaders(bool appCompleted) { // Verifies we have sent the statuscode before writing a header var reasonPhrase = string.IsNullOrEmpty(ReasonPhrase) ? ReasonPhrases.GetReasonPhrase(StatusCode) : ReasonPhrase; var reasonPhraseBytes = new byte [reasonPhrase.Length + 1]; Encoding.ASCII.GetBytes(reasonPhrase, 0, reasonPhrase.Length, reasonPhraseBytes, 0); fixed (byte* pReasonPhrase = reasonPhraseBytes) { Debug.Assert((IntPtr)pReasonPhrase != IntPtr.Zero); // This copies data into the underlying buffer NativeMethods.http_set_response_status_code(_pInProcessHandler, (ushort)StatusCode, pReasonPhrase); } HttpResponseHeaders.IsReadOnly = true; foreach (var headerPair in HttpResponseHeaders) { var headerValues = headerPair.Value; var knownHeaderIndex = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key); if (knownHeaderIndex == -1) { var headerNameBytes = Encoding.UTF8.GetBytes(headerPair.Key); for (var i = 0; i < headerValues.Count; i++) { var headerValueBytes = Encoding.UTF8.GetBytes(headerValues[i]); fixed (byte* pHeaderName = headerNameBytes) { fixed (byte* pHeaderValue = headerValueBytes) { NativeMethods.http_response_set_unknown_header(_pInProcessHandler, pHeaderName, pHeaderValue, (ushort)headerValueBytes.Length, fReplace: false); } } } } else { for (var i = 0; i < headerValues.Count; i++) { var headerValueBytes = Encoding.UTF8.GetBytes(headerValues[i]); fixed (byte* pHeaderValue = headerValueBytes) { NativeMethods.http_response_set_known_header(_pInProcessHandler, knownHeaderIndex, pHeaderValue, (ushort)headerValueBytes.Length, fReplace: false); } } } } } public void Abort() { // TODO } public abstract Task ProcessRequestAsync(); public void OnStarting(Func callback, object state) { lock (_onStartingSync) { if (_hasResponseStarted) { throw new InvalidOperationException("Response already started"); } if (_onStarting == null) { _onStarting = new Stack, object>>(); } _onStarting.Push(new KeyValuePair, object>(callback, state)); } } public void OnCompleted(Func callback, object state) { lock (_onCompletedSync) { if (_onCompleted == null) { _onCompleted = new Stack, object>>(); } _onCompleted.Push(new KeyValuePair, object>(callback, state)); } } protected async Task FireOnStarting() { Stack, object>> onStarting = null; lock (_onStartingSync) { onStarting = _onStarting; _onStarting = null; } if (onStarting != null) { try { foreach (var entry in onStarting) { await entry.Key.Invoke(entry.Value); } } catch (Exception ex) { ReportApplicationError(ex); } } } protected async Task FireOnCompleted() { Stack, object>> onCompleted = null; lock (_onCompletedSync) { onCompleted = _onCompleted; _onCompleted = null; } if (onCompleted != null) { foreach (var entry in onCompleted) { try { await entry.Key.Invoke(entry.Value); } catch (Exception ex) { ReportApplicationError(ex); } } } } protected void ReportApplicationError(Exception ex) { if (_applicationException == null) { _applicationException = ex; } else if (_applicationException is AggregateException) { _applicationException = new AggregateException(_applicationException, ex).Flatten(); } else { _applicationException = new AggregateException(_applicationException, ex); } } public void PostCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS requestNotificationStatus) { Debug.Assert(!_operation.HasContinuation, "Pending async operation!"); var hr = NativeMethods.http_set_completion_status(_pInProcessHandler, requestNotificationStatus); if (hr != NativeMethods.S_OK) { throw Marshal.GetExceptionForHR(hr); } hr = NativeMethods.http_post_completion(_pInProcessHandler, 0); if (hr != NativeMethods.S_OK) { throw Marshal.GetExceptionForHR(hr); } } public void IndicateCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS notificationStatus) { NativeMethods.http_indicate_completion(_pInProcessHandler, notificationStatus); } internal void OnAsyncCompletion(int hr, int cbBytes) { // Must acquire the _stateSync here as anytime we call complete, we need to hold the lock // to avoid races with cancellation. Action continuation; lock (_stateSync) { _reading = false; continuation = _operation.GetCompletion(hr, cbBytes); } continuation?.Invoke(); } private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). _thisHandle.Free(); } if (WindowsUser?.Identity is WindowsIdentity wi) { wi.Dispose(); } disposedValue = true; } } // This code added to correctly implement the disposable pattern. public override void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(disposing: true); } private void ThrowResponseAlreadyStartedException(string value) { throw new InvalidOperationException("Response already started"); } private WindowsPrincipal GetWindowsPrincipal() { var hr = NativeMethods.http_get_authentication_information(_pInProcessHandler, out var authenticationType, out var token); if (hr == 0 && token != IntPtr.Zero && authenticationType != null) { if ((authenticationType.Equals(NtlmString, StringComparison.OrdinalIgnoreCase) || authenticationType.Equals(NegotiateString, StringComparison.OrdinalIgnoreCase) || authenticationType.Equals(BasicString, StringComparison.OrdinalIgnoreCase))) { return new WindowsPrincipal(new WindowsIdentity(token, authenticationType)); } } return null; } private enum WebsocketAvailabilityStatus { Uninitialized = 1, Available, NotAvailable } } }