File: System\Net\Http\WinHttpRequestStream.cs
Web Access
Project: src\src\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\System.Net.Http.WinHttpHandler.csproj (System.Net.Http.WinHttpHandler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;

#pragma warning disable CA1844 // lack of WriteAsync(ReadOnlyMemory) override in .NET Standard 2.1 build

namespace System.Net.Http
{
    internal sealed class WinHttpRequestStream : Stream
    {
        private static readonly byte[] s_crLfTerminator = "\r\n"u8.ToArray();
        private static readonly byte[] s_endChunk = "0\r\n\r\n"u8.ToArray();

        private volatile bool _disposed;
        private readonly WinHttpRequestState _state;
        private readonly SafeWinHttpHandle _requestHandle;
        private readonly WinHttpChunkMode _chunkedMode;

        internal WinHttpRequestStream(WinHttpRequestState state, WinHttpChunkMode chunkedMode)
        {
            _state = state;
            _chunkedMode = chunkedMode;

            // Take copy of handle from state.
            // The state's request handle will be set to null once the response stream starts.
            Debug.Assert(_state.RequestHandle != null);
            _requestHandle = _state.RequestHandle;
        }

        public override bool CanRead
        {
            get
            {
                return false;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return false;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return !_disposed;
            }
        }

        public override long Length
        {
            get
            {
                CheckDisposed();
                throw new NotSupportedException();
            }
        }

        public override long Position
        {
            get
            {
                CheckDisposed();
                throw new NotSupportedException();
            }

            set
            {
                CheckDisposed();
                throw new NotSupportedException();
            }
        }

        public override void Flush()
        {
            // Nothing to do.
        }

        public override Task FlushAsync(CancellationToken cancellationToken)
        {
            return cancellationToken.IsCancellationRequested ?
                Task.FromCanceled(cancellationToken) :
                Task.CompletedTask;
        }

        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token)
        {
            ArgumentNullException.ThrowIfNull(buffer);

            if (offset < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(offset));
            }

            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count));
            }

            if (count > buffer.Length - offset)
            {
                throw new ArgumentException(SR.net_http_buffer_insufficient_length, nameof(buffer));
            }

            if (token.IsCancellationRequested)
            {
                var tcs = new TaskCompletionSource<int>();
                tcs.TrySetCanceled(token);
                return tcs.Task;
            }

            CheckDisposed();

            if (_state.TcsInternalWriteDataToRequestStream != null &&
                !_state.TcsInternalWriteDataToRequestStream.Task.IsCompleted)
            {
                throw new InvalidOperationException(SR.net_http_no_concurrent_io_allowed);
            }

            return InternalWriteAsync(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
        }

        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? asyncCallback, object? asyncState) =>
            TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), asyncCallback, asyncState);

        public override void EndWrite(IAsyncResult asyncResult) =>
            TaskToAsyncResult.End(asyncResult);

        public override long Seek(long offset, SeekOrigin origin)
        {
            CheckDisposed();
            throw new NotSupportedException();
        }

        public override void SetLength(long value)
        {
            CheckDisposed();
            throw new NotSupportedException();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            CheckDisposed();
            throw new NotSupportedException();
        }

        internal async Task EndUploadAsync()
        {
            switch (_chunkedMode)
            {
                case WinHttpChunkMode.Manual:
                    await InternalWriteDataAsync(s_endChunk, 0, s_endChunk.Length).ConfigureAwait(false);
                    break;
                case WinHttpChunkMode.Automatic:
                    // Send empty DATA frame with END_STREAM flag.
                    await InternalWriteEndDataAsync().ConfigureAwait(false);
                    break;
            }
        }

        protected override void Dispose(bool disposing)
        {
            _disposed = true;
            base.Dispose(disposing);
        }

        private void CheckDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(this.GetType().FullName);
            }
        }

        private Task InternalWriteAsync(byte[] buffer, int offset, int count)
        {
            if (count == 0)
            {
                return Task.CompletedTask;
            }

            return _chunkedMode == WinHttpChunkMode.Manual ?
                InternalWriteChunkedModeAsync(buffer, offset, count) :
                InternalWriteDataAsync(buffer, offset, count);
        }

        private async Task InternalWriteChunkedModeAsync(byte[] buffer, int offset, int count)
        {
            // WinHTTP does not fully support chunked uploads. It simply allows one to omit the 'Content-Length' header
            // and instead use the 'Transfer-Encoding: chunked' header. The caller is still required to encode the
            // request body according to chunking rules.
            Debug.Assert(_chunkedMode == WinHttpChunkMode.Manual);
            Debug.Assert(count > 0);

            byte[] chunkSize = Encoding.UTF8.GetBytes($"{count:x}\r\n");

            await InternalWriteDataAsync(chunkSize, 0, chunkSize.Length).ConfigureAwait(false);

            await InternalWriteDataAsync(buffer, offset, count).ConfigureAwait(false);
            await InternalWriteDataAsync(s_crLfTerminator, 0, s_crLfTerminator.Length).ConfigureAwait(false);
        }

        private Task<bool> InternalWriteDataAsync(byte[] buffer, int offset, int count)
        {
            Debug.Assert(count > 0);

            _state.PinSendBuffer(buffer);
            _state.TcsInternalWriteDataToRequestStream =
                new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

            lock (_state.Lock)
            {
                if (!Interop.WinHttp.WinHttpWriteData(
                    _requestHandle,
                    Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset),
                    (uint)count,
                    IntPtr.Zero))
                {
                    _state.TcsInternalWriteDataToRequestStream.TrySetException(
                        new IOException(SR.net_http_io_write, WinHttpException.CreateExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpWriteData))));
                }
            }

            return _state.TcsInternalWriteDataToRequestStream.Task;
        }

        private Task<bool> InternalWriteEndDataAsync()
        {
            _state.TcsInternalWriteDataToRequestStream =
                new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

            lock (_state.Lock)
            {
                if (!Interop.WinHttp.WinHttpWriteData(
                    _requestHandle,
                    IntPtr.Zero,
                    0,
                    IntPtr.Zero))
                {
                    _state.TcsInternalWriteDataToRequestStream.TrySetException(
                        new IOException(SR.net_http_io_write, WinHttpException.CreateExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpWriteData))));
                }
            }

            return _state.TcsInternalWriteDataToRequestStream.Task;
        }
    }
}