// 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.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.ExceptionServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using System.Web.Configuration; using System.Web.Http.Controllers; using System.Web.Http.ExceptionHandling; using System.Web.Http.Hosting; using System.Web.Http.Routing; using System.Web.Http.WebHost.Properties; using System.Web.Http.WebHost.Routing; using System.Web.Routing; namespace System.Web.Http.WebHost { /// /// An that uses an to process ASP.NET requests asynchronously. /// [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This class is a coordinator, so this coupling is expected.")] [SuppressMessage("Microsoft.Design", "CA1001:Implement IDisposable", Justification = "HttpMessageInvoker doesn’t have any resources of its own to dispose.")] public class HttpControllerHandler : HttpTaskAsyncHandler { // See Microsoft.Owin.Host.SystemWeb. internal static readonly string OwinEnvironmentHttpContextKey = "owin.Environment"; internal static readonly string OwinEnvironmentKey = "MS_OwinEnvironment"; private static readonly Lazy> _suppressRedirectAction = new Lazy>( () => { // If the behavior is explicitly disabled, do nothing if (!SuppressFormsAuthRedirectHelper.GetEnabled(WebConfigurationManager.AppSettings)) { return httpContext => { }; } return httpContext => httpContext.Response.SuppressFormsAuthenticationRedirect = true; }); private static readonly Lazy _bufferPolicySelector = new Lazy(() => GlobalConfiguration.Configuration.Services.GetHostBufferPolicySelector()); private static readonly Lazy _exceptionHandler = new Lazy(() => ExceptionServices.GetHandler(GlobalConfiguration.Configuration)); private static readonly Lazy _exceptionLogger = new Lazy(() => ExceptionServices.GetLogger(GlobalConfiguration.Configuration)); private static readonly Func _retrieveClientCertificate = new Func(RetrieveClientCertificate); private readonly IHttpRouteData _routeData; private readonly HttpMessageInvoker _server; /// /// Initializes a new instance of the class. /// /// The route data. public HttpControllerHandler(RouteData routeData) : this(routeData, GlobalConfiguration.DefaultServer) { } /// /// Initializes a new instance of the class. /// /// The route data. /// The message handler to dispatch requests to. public HttpControllerHandler(RouteData routeData, HttpMessageHandler handler) { if (routeData == null) { throw Error.ArgumentNull("routeData"); } if (handler == null) { throw Error.ArgumentNull("handler"); } _routeData = new HostedHttpRouteData(routeData); _server = new HttpMessageInvoker(handler); } public override Task ProcessRequestAsync(HttpContext context) { return ProcessRequestAsyncCore(new HttpContextWrapper(context)); } internal async Task ProcessRequestAsyncCore(HttpContextBase contextBase) { HttpRequestMessage request = contextBase.GetHttpRequestMessage() ?? ConvertRequest(contextBase); // Add route data request.SetRouteData(_routeData); CancellationToken cancellationToken = contextBase.Response.GetClientDisconnectedTokenWhenFixed(); HttpResponseMessage response = null; try { response = await _server.SendAsync(request, cancellationToken); await CopyResponseAsync(contextBase, request, response, _exceptionLogger.Value, _exceptionHandler.Value, cancellationToken); } catch (OperationCanceledException) { // HttpTaskAsyncHandler treats a canceled task as an unhandled exception (logged to Application event // log). Instead of returning a canceled task, abort the request and return a completed task. contextBase.Request.Abort(); } finally { // The other HttpTaskAsyncHandler is HttpRouteExceptionHandler; it has similar cleanup logic. request.DisposeRequestResources(); request.Dispose(); if (response != null) { response.Dispose(); } } } private static void CopyHeaders(HttpHeaders from, HttpContextBase to) { Contract.Assert(from != null); Contract.Assert(to != null); foreach (var header in from) { string name = header.Key; foreach (var value in header.Value) { to.Response.AppendHeader(name, value); } } } private static void AddHeaderToHttpRequestMessage(HttpRequestMessage httpRequestMessage, string headerName, string[] headerValues) { Contract.Assert(httpRequestMessage != null); Contract.Assert(headerName != null); Contract.Assert(headerValues != null); if (!httpRequestMessage.Headers.TryAddWithoutValidation(headerName, headerValues)) { httpRequestMessage.Content.Headers.TryAddWithoutValidation(headerName, headerValues); } } internal static async Task CopyResponseAsync(HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, IExceptionHandler exceptionHandler, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); Contract.Assert(request != null); // A null response creates a 500 with no content if (response == null) { SetEmptyErrorResponse(httpContextBase.Response); return; } if (!await CopyResponseStatusAndHeadersAsync(httpContextBase, request, response, exceptionLogger, cancellationToken)) { return; } // TODO 335085: Consider this when coming up with our caching story if (response.Headers.CacheControl == null) { // DevDiv2 #332323. ASP.NET by default always emits a cache-control: private header. // However, we don't want requests to be cached by default. // If nobody set an explicit CacheControl then explicitly set to no-cache to override the // default behavior. This will cause the following response headers to be emitted: // Cache-Control: no-cache // Pragma: no-cache // Expires: -1 httpContextBase.Response.Cache.SetCacheability(HttpCacheability.NoCache); } // Asynchronously write the response body. If there is no body, we use // a completed task to share the Finally() below. // The response-writing task will not fault -- it handles errors internally. if (response.Content != null) { await WriteResponseContentAsync(httpContextBase, request, response, exceptionLogger, exceptionHandler, cancellationToken); } } internal static HttpRequestMessage ConvertRequest(HttpContextBase httpContextBase) { return ConvertRequest(httpContextBase, _bufferPolicySelector.Value); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner")] internal static HttpRequestMessage ConvertRequest(HttpContextBase httpContextBase, IHostBufferPolicySelector policySelector) { Contract.Assert(httpContextBase != null); HttpRequestBase requestBase = httpContextBase.Request; HttpMethod method = HttpMethodHelper.GetHttpMethod(requestBase.HttpMethod); Uri uri = requestBase.Url; HttpRequestMessage request = new HttpRequestMessage(method, uri); // Choose a buffered or bufferless input stream based on user's policy bool bufferInput = policySelector == null ? true : policySelector.UseBufferedInputStream(httpContextBase); request.Content = GetStreamContent(requestBase, bufferInput); foreach (string headerName in requestBase.Headers) { string[] values = requestBase.Headers.GetValues(headerName); AddHeaderToHttpRequestMessage(request, headerName, values); } // Add context to enable route lookup later on request.SetHttpContext(httpContextBase); HttpRequestContext requestContext = new WebHostHttpRequestContext(httpContextBase, requestBase, request); request.SetRequestContext(requestContext); IDictionary httpContextItems = httpContextBase.Items; // Add the OWIN environment, when available (such as when using the OWIN integrated pipeline HTTP module). if (httpContextItems != null && httpContextItems.Contains(OwinEnvironmentHttpContextKey)) { request.Properties.Add(OwinEnvironmentKey, httpContextItems[OwinEnvironmentHttpContextKey]); } // The following three properties are set for backwards compatibility only. The request context controls // the behavior for all cases except when accessing the property directly by key. // Add the retrieve client certificate delegate to the property bag to enable lookup later on request.Properties.Add(HttpPropertyKeys.RetrieveClientCertificateDelegateKey, _retrieveClientCertificate); // Add information about whether the request is local or not request.Properties.Add(HttpPropertyKeys.IsLocalKey, new Lazy(() => requestBase.IsLocal)); // Add information about whether custom errors are enabled for this request or not request.Properties.Add(HttpPropertyKeys.IncludeErrorDetailKey, new Lazy(() => !httpContextBase.IsCustomErrorEnabled)); return request; } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner")] private static HttpContent GetStreamContent(HttpRequestBase requestBase, bool bufferInput) { if (bufferInput) { return new LazyStreamContent(() => { if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.None) { return new SeekableBufferedRequestStream(requestBase); } else if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Classic) { requestBase.InputStream.Position = 0; return requestBase.InputStream; } else if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Buffered) { if (requestBase.GetBufferedInputStream().Position > 0) { // If GetBufferedInputStream() was completely read, we can continue accessing it via Request.InputStream. // If it was partially read, accessing InputStream will throw, but at that point we have no // way of recovering. requestBase.InputStream.Position = 0; return requestBase.InputStream; } return new SeekableBufferedRequestStream(requestBase); } else { Contract.Assert(requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Bufferless); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, SRResources.RequestBodyAlreadyReadInMode, ReadEntityBodyMode.Bufferless)); } }); } else { return new LazyStreamContent(() => { if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.None) { return requestBase.GetBufferlessInputStream(); } else if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Classic) { // The user intended that the request be read in a bufferless manner, but we are starting with a buffered stream. // To maintain compatibility with legacy behavior, we'll throw in this case. throw new InvalidOperationException(SRResources.RequestStreamCannotBeReadBufferless); } else if (requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Bufferless) { Stream bufferlessInputStream = requestBase.GetBufferlessInputStream(); if (bufferlessInputStream.Position > 0) { throw new InvalidOperationException(SRResources.RequestBodyAlreadyRead); } return bufferlessInputStream; } else { Contract.Assert(requestBase.ReadEntityBodyMode == ReadEntityBodyMode.Buffered); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, SRResources.RequestBodyAlreadyReadInMode, ReadEntityBodyMode.Buffered)); } }); } } /// /// Prevents the from altering a 401 response to 302 by /// setting to true if available. /// /// The HTTP context base. internal static void EnsureSuppressFormsAuthenticationRedirect(HttpContextBase httpContextBase) { Contract.Assert(httpContextBase != null); // Only if the response is status code is 401 if (httpContextBase.Response.StatusCode == (int)HttpStatusCode.Unauthorized) { _suppressRedirectAction.Value(httpContextBase); } } [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "unused", Justification = "unused variable necessary to call getter")] private static Task WriteResponseContentAsync(HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, IExceptionHandler exceptionHandler, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); Contract.Assert(response != null); Contract.Assert(request != null); Contract.Assert(response.Content != null); HttpResponseBase httpResponseBase = httpContextBase.Response; HttpContent responseContent = response.Content; CopyHeaders(responseContent.Headers, httpContextBase); // PrepareHeadersAsync already evaluated the buffer policy. bool isBuffered = httpResponseBase.BufferOutput; return isBuffered ? WriteBufferedResponseContentAsync(httpContextBase, request, response, exceptionLogger, exceptionHandler, cancellationToken) : WriteStreamedResponseContentAsync(httpContextBase, request, response, exceptionLogger, cancellationToken); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "All exceptions caught here become error responses")] internal static async Task WriteStreamedResponseContentAsync(HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); Contract.Assert(httpContextBase.Response != null); Contract.Assert(request != null); Contract.Assert(response != null); Contract.Assert(response.Content != null); Exception exception = null; cancellationToken.ThrowIfCancellationRequested(); try { // Copy the HttpContent into the output stream asynchronously. await response.Content.CopyToAsync(httpContextBase.Response.OutputStream); return; } catch (OperationCanceledException) { // Propogate the canceled task without calling exception loggers. throw; } catch (Exception ex) { exception = ex; } Contract.Assert(exception != null); ExceptionContextCatchBlock catchBlock = WebHostExceptionCatchBlocks.HttpControllerHandlerStreamContent; ExceptionContext exceptionContext = new ExceptionContext(exception, catchBlock, request, response); await exceptionLogger.LogAsync(exceptionContext, cancellationToken); // Streamed content may have been written and cannot be recalled. // Our only choice is to abort the connection. httpContextBase.Request.Abort(); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "continuation task owned by caller")] [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "All exceptions caught here become error responses")] internal static async Task WriteBufferedResponseContentAsync(HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, IExceptionHandler exceptionHandler, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); Contract.Assert(httpContextBase.Response != null); Contract.Assert(request != null); Contract.Assert(response != null); Contract.Assert(response.Content != null); HttpResponseBase httpResponseBase = httpContextBase.Response; // Return a task that writes the response body asynchronously. // We guarantee we will handle all error responses internally // and always return a non-faulted task, except for custom error handlers that choose to propagate these // exceptions. ExceptionDispatchInfo exceptionInfo; cancellationToken.ThrowIfCancellationRequested(); try { // Copy the HttpContent into the output stream asynchronously. await response.Content.CopyToAsync(httpResponseBase.OutputStream); return; } catch (OperationCanceledException) { // Propogate the canceled task without calling exception loggers or handlers. throw; } catch (Exception exception) { // Can't use await inside a catch block exceptionInfo = ExceptionDispatchInfo.Capture(exception); } Debug.Assert(exceptionInfo.SourceException != null); // If we were using a buffered stream, we can still set the headers and status code, and we can create an // error response with the exception. // We create a continuation task to write an error response that will run after returning from this Catch() // but before other continuations the caller appends to this task. // The error response writing task handles errors internally and will not show as faulted, except for // custom error handlers that choose to propagate these exceptions. ExceptionContextCatchBlock catchBlock = WebHostExceptionCatchBlocks.HttpControllerHandlerBufferContent; if (!await CopyErrorResponseAsync(catchBlock, httpContextBase, request, response, exceptionInfo.SourceException, exceptionLogger, exceptionHandler, cancellationToken)) { exceptionInfo.Throw(); } } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "All exceptions caught here become error responses")] [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "errorResponse gets disposed in the async continuation")] internal static async Task CopyErrorResponseAsync(ExceptionContextCatchBlock catchBlock, HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, Exception exception, IExceptionLogger exceptionLogger, IExceptionHandler exceptionHandler, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); Contract.Assert(httpContextBase.Response != null); Contract.Assert(request != null); Contract.Assert(exception != null); Contract.Assert(catchBlock != null); Contract.Assert(catchBlock.CallsHandler); HttpResponseBase httpResponseBase = httpContextBase.Response; HttpResponseMessage errorResponse = null; HttpResponseException responseException = exception as HttpResponseException; // Ensure all headers and content are cleared to eliminate any partial results. ClearContentAndHeaders(httpResponseBase); // If the exception we are handling is HttpResponseException, // that becomes the error response. if (responseException != null) { errorResponse = responseException.Response; } else { ExceptionContext exceptionContext = new ExceptionContext(exception, catchBlock, request) { Response = response }; await exceptionLogger.LogAsync(exceptionContext, cancellationToken); errorResponse = await exceptionHandler.HandleAsync(exceptionContext, cancellationToken); if (errorResponse == null) { return false; } } Contract.Assert(errorResponse != null); if (!await CopyResponseStatusAndHeadersAsync(httpContextBase, request, errorResponse, exceptionLogger, cancellationToken)) { // Don't rethrow the original exception unless explicitly requested to do so. In this case, the // exception handler indicated it wanted to handle the exception; it simply failed create a stable // response to send. return true; } // The error response may return a null content if content negotiation // fails to find a formatter, or this may be an HttpResponseException without // content. In either case, cleanup and return a completed task. if (errorResponse.Content == null) { errorResponse.Dispose(); return true; } CopyHeaders(errorResponse.Content.Headers, httpContextBase); await WriteErrorResponseContentAsync(httpResponseBase, request, errorResponse, cancellationToken, exceptionLogger); return true; } private static async Task WriteErrorResponseContentAsync(HttpResponseBase httpResponseBase, HttpRequestMessage request, HttpResponseMessage errorResponse, CancellationToken cancellationToken, IExceptionLogger exceptionLogger) { HttpRequestMessage ignoreUnused = request; try { Exception exception = null; cancellationToken.ThrowIfCancellationRequested(); try { // Asynchronously write the content of the new error HttpResponseMessage await errorResponse.Content.CopyToAsync(httpResponseBase.OutputStream); return; } catch (OperationCanceledException) { // Propogate the canceled task without calling exception loggers. throw; } catch (Exception ex) { exception = ex; } Contract.Assert(exception != null); ExceptionContext exceptionContext = new ExceptionContext(exception, WebHostExceptionCatchBlocks.HttpControllerHandlerBufferError, request, errorResponse); await exceptionLogger.LogAsync(exceptionContext, cancellationToken); // Failure writing the error response. Likely cause is a formatter // serialization exception. Create empty error response and // return a non-faulted task. SetEmptyErrorResponse(httpResponseBase); } finally { // Dispose the temporary HttpResponseMessage carrying the error response errorResponse.Dispose(); } } private static async Task CopyResponseStatusAndHeadersAsync(HttpContextBase httpContextBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, CancellationToken cancellationToken) { Contract.Assert(httpContextBase != null); HttpResponseBase httpResponseBase = httpContextBase.Response; httpResponseBase.StatusCode = (int)response.StatusCode; httpResponseBase.StatusDescription = response.ReasonPhrase; httpResponseBase.TrySkipIisCustomErrors = true; EnsureSuppressFormsAuthenticationRedirect(httpContextBase); if (!await PrepareHeadersAsync(httpResponseBase, request, response, exceptionLogger, cancellationToken)) { return false; } CopyHeaders(response.Headers, httpContextBase); return true; } // Prepares Content-Length and Transfer-Encoding headers. [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "unused", Justification = "unused variable necessary to call getter")] internal static async Task PrepareHeadersAsync(HttpResponseBase responseBase, HttpRequestMessage request, HttpResponseMessage response, IExceptionLogger exceptionLogger, CancellationToken cancellationToken) { Contract.Assert(response != null); HttpResponseHeaders responseHeaders = response.Headers; Contract.Assert(responseHeaders != null); HttpContent content = response.Content; bool isTransferEncodingChunked = responseHeaders.TransferEncodingChunked == true; HttpHeaderValueCollection transferEncoding = responseHeaders.TransferEncoding; if (content != null) { HttpContentHeaders contentHeaders = content.Headers; Contract.Assert(contentHeaders != null); if (isTransferEncodingChunked) { // According to section 4.4 of the HTTP 1.1 spec, HTTP responses that use chunked transfer // encoding must not have a content length set. Chunked should take precedence over content // length in this case because chunked is always set explicitly by users while the Content-Length // header can be added implicitly by System.Net.Http. contentHeaders.ContentLength = null; } else { Exception exception = null; // Copy the response content headers only after ensuring they are complete. // We ask for Content-Length first because HttpContent lazily computes this // and only afterwards writes the value into the content headers. try { var unused = contentHeaders.ContentLength; } catch (Exception ex) { exception = ex; } if (exception != null) { ExceptionContext exceptionContext = new ExceptionContext(exception, WebHostExceptionCatchBlocks.HttpControllerHandlerComputeContentLength, request, response); await exceptionLogger.LogAsync(exceptionContext, cancellationToken); SetEmptyErrorResponse(responseBase); return false; } } // Select output buffering based on the user-controlled buffering policy bool isBuffered = _bufferPolicySelector.Value != null ? _bufferPolicySelector.Value.UseBufferedOutputStream(response) : true; responseBase.BufferOutput = isBuffered; } // Ignore the Transfer-Encoding header if it is just "chunked"; the host will provide it when no // Content-Length is present and BufferOutput is disabled (and this method guarantees those conditions). // HttpClient sets this header when it receives chunked content, but HttpContent does not include the // frames. The ASP.NET contract is to set this header only when writing chunked frames to the stream. // A Web API caller who desires custom framing would need to do a different Transfer-Encoding (such as // "identity, chunked"). if (isTransferEncodingChunked && transferEncoding.Count == 1) { transferEncoding.Clear(); // In the case of a conflict between a Transfer-Encoding: chunked header and the output buffering // policy, honor the Transnfer-Encoding: chunked header and ignore the buffer policy. // If output buffering is not disabled, ASP.NET will not write the TransferEncoding: chunked header. responseBase.BufferOutput = false; } return true; } private static void ClearContentAndHeaders(HttpResponseBase httpResponseBase) { httpResponseBase.Clear(); // Despite what the documentation indicates, calling Clear on its own doesn't fully clear the headers. httpResponseBase.ClearHeaders(); } private static void SetEmptyErrorResponse(HttpResponseBase httpResponseBase) { ClearContentAndHeaders(httpResponseBase); httpResponseBase.StatusCode = (int)HttpStatusCode.InternalServerError; httpResponseBase.SuppressContent = true; } private static X509Certificate2 RetrieveClientCertificate(HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } X509Certificate2 result = null; HttpContextBase httpContextBase = request.GetHttpContext(); if (httpContextBase != null) { if (httpContextBase.Request.ClientCertificate.Certificate != null && httpContextBase.Request.ClientCertificate.Certificate.Length > 0) { result = new X509Certificate2(httpContextBase.Request.ClientCertificate.Certificate); } } return result; } private class DelegatingStreamContent : StreamContent { public DelegatingStreamContent(Stream stream) : base(stream) { } public Task WriteToStreamAsync(Stream stream, TransportContext context) { return SerializeToStreamAsync(stream, context); } public Task GetContentReadStreamAsync() { return CreateContentReadStreamAsync(); } protected override bool TryComputeLength(out long length) { // Do not attempt to calculate length because SeekableBufferedRequestStream (for example) // may report 0 until the underlying Stream has been read to end. length = 0L; return false; } } private class LazyStreamContent : HttpContent { private readonly Func _getStream; private DelegatingStreamContent _streamContent; public LazyStreamContent(Func getStream) { _getStream = getStream; } private DelegatingStreamContent StreamContent { get { if (_streamContent == null) { _streamContent = new DelegatingStreamContent(_getStream()); } return _streamContent; } } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { return StreamContent.WriteToStreamAsync(stream, context); } protected override Task CreateContentReadStreamAsync() { return StreamContent.GetContentReadStreamAsync(); } protected override bool TryComputeLength(out long length) { // Do not attempt to calculate length because SeekableBufferedRequestStream (for example) // may report 0 until the underlying Stream has been read to end. length = 0L; return false; } } } }