// 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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using static Microsoft.AspNetCore.Server.HttpSys.UnsafeNclNativeMethods; namespace Microsoft.AspNetCore.Server.HttpSys { internal class ResponseBody : Stream { private RequestContext _requestContext; private long _leftToWrite = long.MinValue; private bool _skipWrites; private bool _disposed; // The last write needs special handling to cancel. private ResponseStreamAsyncResult _lastWrite; internal ResponseBody(RequestContext requestContext) { _requestContext = requestContext; } internal RequestContext RequestContext { get { return _requestContext; } } private SafeHandle RequestQueueHandle => RequestContext.Server.RequestQueue.Handle; private ulong RequestId => RequestContext.Request.RequestId; private ILogger Logger => RequestContext.Server.Logger; internal bool ThrowWriteExceptions => RequestContext.Server.Options.ThrowWriteExceptions; internal bool IsDisposed => _disposed; public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return true; } } public override bool CanRead { get { return false; } } public override long Length { get { throw new NotSupportedException(Resources.Exception_NoSeek); } } public override long Position { get { throw new NotSupportedException(Resources.Exception_NoSeek); } set { throw new NotSupportedException(Resources.Exception_NoSeek); } } // Send headers public override void Flush() { if (_disposed) { return; } FlushInternal(endOfRequest: false); } // We never expect endOfRequest and data at the same time private unsafe void FlushInternal(bool endOfRequest, ArraySegment data = new ArraySegment()) { Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request."); if (_skipWrites) { return; } var started = _requestContext.Response.HasStarted; if (data.Count == 0 && started && !endOfRequest) { // No data to send and we've already sent the headers return; } // Make sure all validation is performed before this computes the headers var flags = ComputeLeftToWrite(data.Count, endOfRequest); if (endOfRequest && _leftToWrite > 0) { _requestContext.Abort(); // This is logged rather than thrown because it is too late for an exception to be visible in user code. LogHelper.LogError(Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length."); return; } uint statusCode = 0; HttpApi.HTTP_DATA_CHUNK[] dataChunks; var pinnedBuffers = PinDataBuffers(endOfRequest, data, out dataChunks); try { if (!started) { statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false); } else { fixed (HttpApi.HTTP_DATA_CHUNK* pDataChunks = dataChunks) { statusCode = HttpApi.HttpSendResponseEntityBody( RequestQueueHandle, RequestId, (uint)flags, (ushort)dataChunks.Length, pDataChunks, null, SafeLocalFree.Zero, 0, SafeNativeOverlapped.Zero, IntPtr.Zero); } } } finally { FreeDataBuffers(pinnedBuffers); } if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF // Don't throw for disconnects, we were already finished with the response. && (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER))) { if (ThrowWriteExceptions) { var exception = new IOException(string.Empty, new HttpSysException((int)statusCode)); LogHelper.LogException(Logger, "Flush", exception); Abort(); throw exception; } else { // Abort the request but do not close the stream, let future writes complete silently LogHelper.LogDebug(Logger, "Flush", $"Ignored write exception: {statusCode}"); Abort(dispose: false); } } } private List PinDataBuffers(bool endOfRequest, ArraySegment data, out HttpApi.HTTP_DATA_CHUNK[] dataChunks) { var pins = new List(); var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var currentChunk = 0; // Figure out how many data chunks if (chunked && data.Count == 0 && endOfRequest) { dataChunks = new HttpApi.HTTP_DATA_CHUNK[1]; SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment(Helpers.ChunkTerminator)); return pins; } else if (data.Count == 0) { // No data dataChunks = new HttpApi.HTTP_DATA_CHUNK[0]; return pins; } var chunkCount = 1; if (chunked) { // Chunk framing chunkCount += 2; if (endOfRequest) { // Chunk terminator chunkCount += 1; } } dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunkCount]; if (chunked) { var chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count); SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer); } SetDataChunk(dataChunks, ref currentChunk, pins, data); if (chunked) { SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment(Helpers.CRLF)); if (endOfRequest) { SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment(Helpers.ChunkTerminator)); } } return pins; } private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List pins, ArraySegment buffer) { var handle = GCHandle.Alloc(buffer.Array, GCHandleType.Pinned); pins.Add(handle); chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory; chunks[chunkIndex].fromMemory.pBuffer = handle.AddrOfPinnedObject() + buffer.Offset; chunks[chunkIndex].fromMemory.BufferLength = (uint)buffer.Count; chunkIndex++; } private void FreeDataBuffers(List pinnedBuffers) { foreach (var pin in pinnedBuffers) { if (pin.IsAllocated) { pin.Free(); } } } public override Task FlushAsync(CancellationToken cancellationToken) { if (_disposed) { return Helpers.CompletedTask(); } return FlushInternalAsync(new ArraySegment(), cancellationToken); } // Simpler than Flush because it will never be called at the end of the request from Dispose. private unsafe Task FlushInternalAsync(ArraySegment data, CancellationToken cancellationToken) { if (_skipWrites) { return Helpers.CompletedTask(); } var started = _requestContext.Response.HasStarted; if (data.Count == 0 && started) { // No data to send and we've already sent the headers return Helpers.CompletedTask(); } if (cancellationToken.IsCancellationRequested) { Abort(ThrowWriteExceptions); return Helpers.CanceledTask(); } // Make sure all validation is performed before this computes the headers var flags = ComputeLeftToWrite(data.Count); uint statusCode = 0; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var asyncResult = new ResponseStreamAsyncResult(this, data, chunked, cancellationToken); uint bytesSent = 0; try { if (!started) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); bytesSent = asyncResult.BytesSent; } else { statusCode = HttpApi.HttpSendResponseEntityBody( RequestQueueHandle, RequestId, (uint)flags, asyncResult.DataChunkCount, asyncResult.DataChunks, &bytesSent, SafeLocalFree.Zero, 0, asyncResult.NativeOverlapped, IntPtr.Zero); } } catch (Exception e) { LogHelper.LogException(Logger, "FlushAsync", e); asyncResult.Dispose(); Abort(); throw; } if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING) { if (cancellationToken.IsCancellationRequested) { LogHelper.LogDebug(Logger, "FlushAsync", $"Write cancelled with error code: {statusCode}"); asyncResult.Cancel(ThrowWriteExceptions); } else if (ThrowWriteExceptions) { asyncResult.Dispose(); Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode)); LogHelper.LogException(Logger, "FlushAsync", exception); Abort(); throw exception; } else { // Abort the request but do not close the stream, let future writes complete silently LogHelper.LogDebug(Logger, "FlushAsync", $"Ignored write exception: {statusCode}"); asyncResult.FailSilently(); } } if (statusCode == ErrorCodes.ERROR_SUCCESS && HttpSysListener.SkipIOCPCallbackOnSuccess) { // IO operation completed synchronously - callback won't be called to signal completion. asyncResult.IOCompleted(statusCode, bytesSent); } // Last write, cache it for special cancellation handling. if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0) { _lastWrite = asyncResult; } return asyncResult.Task; } #region NotSupported Read/Seek public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(Resources.Exception_NoSeek); } public override void SetLength(long value) { throw new NotSupportedException(Resources.Exception_NoSeek); } public override int Read([In, Out] byte[] buffer, int offset, int count) { throw new InvalidOperationException(Resources.Exception_WriteOnlyStream); } #if !NETSTANDARD1_3 public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { throw new InvalidOperationException(Resources.Exception_WriteOnlyStream); } public override int EndRead(IAsyncResult asyncResult) { throw new InvalidOperationException(Resources.Exception_WriteOnlyStream); } #endif #endregion internal void Abort(bool dispose = true) { if (dispose) { _disposed = true; } else { _skipWrites = true; } _requestContext.Abort(); } private HttpApi.HTTP_FLAGS ComputeLeftToWrite(long writeCount, bool endOfRequest = false) { var flags = HttpApi.HTTP_FLAGS.NONE; if (!_requestContext.Response.HasComputedHeaders) { flags = _requestContext.Response.ComputeHeaders(writeCount, endOfRequest); } if (_leftToWrite == long.MinValue) { if (_requestContext.Request.IsHeadMethod) { _leftToWrite = 0; } else if (_requestContext.Response.BoundaryType == BoundaryType.ContentLength) { _leftToWrite = _requestContext.Response.ExpectedBodyLength; } else { _leftToWrite = -1; // unlimited } } if (endOfRequest && _requestContext.Response.BoundaryType == BoundaryType.Close) { flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT; } else if (!endOfRequest && _leftToWrite != writeCount) { flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; } // Update _leftToWrite now so we can queue up additional async writes. if (_leftToWrite > 0) { // keep track of the data transferred _leftToWrite -= writeCount; } if (_leftToWrite == 0) { // in this case we already passed 0 as the flag, so we don't need to call HttpSendResponseEntityBody() when we Close() _disposed = true; } // else -1 unlimited return flags; } public override void Write(byte[] buffer, int offset, int count) { // Validates for null and bounds. Allows count == 0. // TODO: Verbose log parameters var data = new ArraySegment(buffer, offset, count); CheckDisposed(); CheckWriteCount(count); FlushInternal(endOfRequest: false, data: data); } private void CheckWriteCount(long? count) { var contentLength = _requestContext.Response.ContentLength; // First write with more bytes written than the entire content-length if (!_requestContext.Response.HasComputedHeaders && contentLength < count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } // A write in a response that has already started where the count exceeds the remainder of the content-length else if (_requestContext.Response.HasComputedHeaders && _requestContext.Response.BoundaryType == BoundaryType.ContentLength && _leftToWrite < count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } } #if NETSTANDARD1_3 public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) #else public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) #endif { return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state); } #if NETSTANDARD1_3 public void EndWrite(IAsyncResult asyncResult) #else public override void EndWrite(IAsyncResult asyncResult) #endif { if (asyncResult == null) { throw new ArgumentNullException(nameof(asyncResult)); } ((Task)asyncResult).GetAwaiter().GetResult(); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { // Validates for null and bounds. Allows count == 0. // TODO: Verbose log parameters var data = new ArraySegment(buffer, offset, count); CheckDisposed(); CheckWriteCount(count); return FlushInternalAsync(data, cancellationToken); } internal async Task SendFileAsync(string fileName, long offset, long? count, CancellationToken cancellationToken) { // It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths. // This all happens inside of ResponseStreamAsyncResult. // TODO: Verbose log parameters if (string.IsNullOrWhiteSpace(fileName)) { throw new ArgumentNullException("fileName"); } CheckDisposed(); CheckWriteCount(count); // We can't mix await and unsafe so separate the unsafe code into another method. await SendFileAsyncCore(fileName, offset, count, cancellationToken); } internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken) { if (_skipWrites) { return Helpers.CompletedTask(); } var started = _requestContext.Response.HasStarted; if (count == 0 && started) { // No data to send and we've already sent the headers return Helpers.CompletedTask(); } if (cancellationToken.IsCancellationRequested) { Abort(ThrowWriteExceptions); return Helpers.CanceledTask(); } // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer // It's too expensive to validate anything before opening the file. Open the file and then check the lengths. var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 1, options: FileOptions.Asynchronous | FileOptions.SequentialScan); // Extremely expensive. try { var length = fileStream.Length; // Expensive, only do it once if (!count.HasValue) { count = length - offset; } if (offset < 0 || offset > length) { throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); } if (count < 0 || count > length - offset) { throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); } CheckWriteCount(count); } catch { fileStream.Dispose(); throw; } // Make sure all validation is performed before this computes the headers var flags = ComputeLeftToWrite(count.Value); uint statusCode; uint bytesSent = 0; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var asyncResult = new ResponseStreamAsyncResult(this, fileStream, offset, count.Value, chunked, cancellationToken); try { if (!started) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); bytesSent = asyncResult.BytesSent; } else { // TODO: If opaque then include the buffer data flag. statusCode = HttpApi.HttpSendResponseEntityBody( RequestQueueHandle, RequestId, (uint)flags, asyncResult.DataChunkCount, asyncResult.DataChunks, &bytesSent, SafeLocalFree.Zero, 0, asyncResult.NativeOverlapped, IntPtr.Zero); } } catch (Exception e) { LogHelper.LogException(Logger, "SendFileAsync", e); asyncResult.Dispose(); Abort(); throw; } if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { if (cancellationToken.IsCancellationRequested) { LogHelper.LogDebug(Logger, "SendFileAsync", $"Write cancelled with error code: {statusCode}"); asyncResult.Cancel(ThrowWriteExceptions); } else if (ThrowWriteExceptions) { asyncResult.Dispose(); var exception = new IOException(string.Empty, new HttpSysException((int)statusCode)); LogHelper.LogException(Logger, "SendFileAsync", exception); Abort(); throw exception; } else { // Abort the request but do not close the stream, let future writes complete silently LogHelper.LogDebug(Logger, "SendFileAsync", $"Ignored write exception: {statusCode}"); asyncResult.FailSilently(); } } if (statusCode == ErrorCodes.ERROR_SUCCESS && HttpSysListener.SkipIOCPCallbackOnSuccess) { // IO operation completed synchronously - callback won't be called to signal completion. asyncResult.IOCompleted(statusCode, bytesSent); } // Last write, cache it for special cancellation handling. if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0) { _lastWrite = asyncResult; } return asyncResult.Task; } protected override unsafe void Dispose(bool disposing) { try { if (disposing) { if (_disposed) { return; } _disposed = true; FlushInternal(endOfRequest: true); } } finally { base.Dispose(disposing); } } internal void SwitchToOpaqueMode() { _leftToWrite = -1; } // The final Content-Length async write can only be Canceled by CancelIoEx. // Sync can only be Canceled by CancelSynchronousIo, but we don't attempt this right now. [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification = "It is safe to ignore the return value on a cancel operation because the connection is being closed")] internal unsafe void CancelLastWrite() { ResponseStreamAsyncResult asyncState = _lastWrite; if (asyncState != null && !asyncState.IsCompleted) { UnsafeNclNativeMethods.CancelIoEx(RequestQueueHandle, asyncState.NativeOverlapped); } } private void CheckDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().FullName); } } } }