File: Core\IISHttpContext.FeatureCollection.cs
Web Access
Project: src\src\Servers\IIS\IIS\src\Microsoft.AspNetCore.Server.IIS.csproj (Microsoft.AspNetCore.Server.IIS)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.AspNetCore.Server.IIS.Core.IO;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Server.IIS.Core;
 
internal partial class IISHttpContext : IFeatureCollection,
                                        IHttpRequestFeature,
                                        IHttpRequestBodyDetectionFeature,
                                        IHttpResponseFeature,
                                        IHttpResponseBodyFeature,
                                        IHttpUpgradeFeature,
                                        IHttpRequestLifetimeFeature,
                                        IHttpAuthenticationFeature,
                                        IServerVariablesFeature,
                                        ITlsConnectionFeature,
                                        ITlsHandshakeFeature,
                                        IHttpBodyControlFeature,
                                        IHttpMaxRequestBodySizeFeature,
                                        IHttpResponseTrailersFeature,
                                        IHttpResetFeature,
                                        IConnectionLifetimeNotificationFeature,
                                        IHttpSysRequestInfoFeature,
                                        IHttpSysRequestTimingFeature
{
    private int _featureRevision;
    private string? _httpProtocolVersion;
    private X509Certificate2? _certificate;
 
    private List<KeyValuePair<Type, object>>? MaybeExtra;
 
    public void ResetFeatureCollection()
    {
        Initialize();
        MaybeExtra?.Clear();
        _featureRevision++;
    }
 
    private object? ExtraFeatureGet(Type key)
    {
        if (MaybeExtra == null)
        {
            return null;
        }
        for (var i = 0; i < MaybeExtra.Count; i++)
        {
            var kv = MaybeExtra[i];
            if (kv.Key == key)
            {
                return kv.Value;
            }
        }
        return null;
    }
 
    private void ExtraFeatureSet(Type key, object? value)
    {
        if (value == null)
        {
            if (MaybeExtra == null)
            {
                return;
            }
            for (var i = 0; i < MaybeExtra.Count; i++)
            {
                if (MaybeExtra[i].Key == key)
                {
                    MaybeExtra.RemoveAt(i);
                    return;
                }
            }
        }
        else
        {
            if (MaybeExtra == null)
            {
                MaybeExtra = new List<KeyValuePair<Type, object>>(2);
            }
            for (var i = 0; i < MaybeExtra.Count; i++)
            {
                if (MaybeExtra[i].Key == key)
                {
                    MaybeExtra[i] = new KeyValuePair<Type, object>(key, value);
                    return;
                }
            }
            MaybeExtra.Add(new KeyValuePair<Type, object>(key, value));
        }
    }
 
    string IHttpRequestFeature.Protocol
    {
        get => _httpProtocolVersion ??= HttpProtocol.GetHttpProtocol(HttpVersion);
        set => _httpProtocolVersion = value;
    }
 
    string IHttpRequestFeature.Scheme
    {
        get => Scheme;
        set => Scheme = value;
    }
 
    string IHttpRequestFeature.Method
    {
        get => Method;
        set => Method = value;
    }
 
    string IHttpRequestFeature.PathBase
    {
        get => PathBase;
        set => PathBase = value;
    }
 
    string IHttpRequestFeature.Path
    {
        get => Path;
        set => Path = value;
    }
 
    string IHttpRequestFeature.QueryString
    {
        get => QueryString;
        set => QueryString = value;
    }
 
    string IHttpRequestFeature.RawTarget
    {
        get => RawTarget;
        set => RawTarget = value;
    }
 
    IHeaderDictionary IHttpRequestFeature.Headers
    {
        get => RequestHeaders;
        set => RequestHeaders = value;
    }
 
    Stream IHttpRequestFeature.Body
    {
        get => RequestBody;
        set => RequestBody = value;
    }
 
    bool IHttpRequestBodyDetectionFeature.CanHaveBody => RequestCanHaveBody;
 
    int IHttpResponseFeature.StatusCode
    {
        get => StatusCode;
        set => StatusCode = value;
    }
 
    string? IHttpResponseFeature.ReasonPhrase
    {
        get => ReasonPhrase;
        set => ReasonPhrase = value;
    }
 
    IHeaderDictionary IHttpResponseFeature.Headers
    {
        get => ResponseHeaders;
        set => ResponseHeaders = value;
    }
 
    Stream IHttpResponseFeature.Body
    {
        get => ResponseBody;
        set => ResponseBody = value;
    }
 
    bool IHttpResponseFeature.HasStarted => HasResponseStarted;
 
    Stream IHttpResponseBodyFeature.Stream => ResponseBody;
 
    PipeWriter IHttpResponseBodyFeature.Writer
    {
        get
        {
            if (ResponsePipeWrapper == null)
            {
                ResponsePipeWrapper = PipeWriter.Create(ResponseBody, new StreamPipeWriterOptions(leaveOpen: true));
            }
 
            return ResponsePipeWrapper;
        }
    }
 
    Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
    {
        if (!HasResponseStarted)
        {
            return InitializeResponse(flushHeaders: false);
        }
 
        return Task.CompletedTask;
    }
 
    Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
        => SendFileFallback.SendFileAsync(ResponseBody, path, offset, count, cancellation);
 
    // TODO: In the future this could complete the body all the way down to the server. For now it just ensures
    // any unflushed data gets flushed.
    Task IHttpResponseBodyFeature.CompleteAsync()
    {
        if (ResponsePipeWrapper != null)
        {
            var completeAsyncValueTask = ResponsePipeWrapper.CompleteAsync();
            if (!completeAsyncValueTask.IsCompletedSuccessfully)
            {
                return CompleteResponseBodyAwaited(completeAsyncValueTask);
            }
            completeAsyncValueTask.GetAwaiter().GetResult();
        }
 
        if (!HasResponseStarted)
        {
            var initializeTask = InitializeResponse(flushHeaders: false);
            if (!initializeTask.IsCompletedSuccessfully)
            {
                return CompleteInitializeResponseAwaited(initializeTask);
            }
        }
 
        // Completing the body output will trigger a final flush to IIS.
        // We'd rather not bypass the bodyoutput to flush, to guarantee we avoid
        // calling flush twice at the same time.
        // awaiting the writeBodyTask guarantees the response has finished the final flush.
        _bodyOutput.Complete();
        return _writeBodyTask!;
    }
 
    private async Task CompleteResponseBodyAwaited(ValueTask completeAsyncTask)
    {
        await completeAsyncTask;
 
        if (!HasResponseStarted)
        {
            await InitializeResponse(flushHeaders: false);
        }
 
        _bodyOutput.Complete();
        await _writeBodyTask!;
    }
 
    private async Task CompleteInitializeResponseAwaited(Task initializeTask)
    {
        await initializeTask;
 
        _bodyOutput.Complete();
        await _writeBodyTask!;
    }
 
    // Http/2 does not support the upgrade mechanic.
    // Http/1.1 upgrade requests may have a request body, but that's not allowed in our main scenario (WebSockets) and much
    // more complicated to support. See https://tools.ietf.org/html/rfc7230#section-6.7, https://tools.ietf.org/html/rfc7540#section-3.2
    bool IHttpUpgradeFeature.IsUpgradableRequest => !RequestCanHaveBody && HttpVersion == System.Net.HttpVersion.Version11;
 
    bool IFeatureCollection.IsReadOnly => false;
 
    int IFeatureCollection.Revision => _featureRevision;
 
    ClaimsPrincipal? IHttpAuthenticationFeature.User
    {
        get => User;
        set => User = value;
    }
 
    string? IServerVariablesFeature.this[string variableName]
    {
        get
        {
            ArgumentException.ThrowIfNullOrEmpty(variableName);
 
            // Synchronize access to native methods that might run in parallel with IO loops
            lock (_contextLock)
            {
                return NativeMethods.HttpTryGetServerVariable(_requestNativeHandle, variableName, out var value) ? value : null;
            }
        }
        set
        {
            ArgumentException.ThrowIfNullOrEmpty(variableName);
 
            ArgumentNullException.ThrowIfNull(value);
 
            // Synchronize access to native methods that might run in parallel with IO loops
            lock (_contextLock)
            {
                NativeMethods.HttpSetServerVariable(_requestNativeHandle, variableName, value);
            }
        }
    }
 
    object? IFeatureCollection.this[Type key]
    {
        get => FastFeatureGet(key);
        set => FastFeatureSet(key, value);
    }
 
    TFeature? IFeatureCollection.Get<TFeature>() where TFeature : default
    {
        return (TFeature?)FastFeatureGet(typeof(TFeature));
    }
 
    void IFeatureCollection.Set<TFeature>(TFeature? instance) where TFeature : default
    {
        FastFeatureSet(typeof(TFeature), instance);
    }
 
    void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
    {
        OnStarting(callback, state);
    }
 
    void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state)
    {
        OnCompleted(callback, state);
    }
 
    async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
    {
        if (!((IHttpUpgradeFeature)this).IsUpgradableRequest)
        {
            if (HttpVersion != System.Net.HttpVersion.Version11)
            {
                throw new InvalidOperationException(CoreStrings.UpgradeWithWrongProtocolVersion);
            }
            throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest);
        }
 
        if (_wasUpgraded)
        {
            throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes);
        }
        if (HasResponseStarted)
        {
            throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes);
        }
 
        MaxRequestBodySize = null;
        _wasUpgraded = true;
 
        StatusCode = StatusCodes.Status101SwitchingProtocols;
        ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
 
        // If we started reading before calling Upgrade Task should be completed at this point
        // because read would return 0 synchronously
        Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted);
 
        // Reset reading status to allow restarting with new IO
        HasStartedConsumingRequestBody = false;
 
        // Upgrade async will cause the stream processing to go into duplex mode
        AsyncIO = new WebSocketsAsyncIOEngine(this, _requestNativeHandle);
 
        await InitializeResponse(flushHeaders: true);
 
        return _streams.Upgrade();
    }
 
    Task<X509Certificate2?> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(((ITlsConnectionFeature)this).ClientCertificate);
    }
 
    unsafe X509Certificate2? ITlsConnectionFeature.ClientCertificate
    {
        get
        {
            if (_certificate == null &&
                NativeRequest->pSslInfo != null &&
                NativeRequest->pSslInfo->pClientCertInfo != null &&
                NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded != null &&
                NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize != 0)
            {
                // Based off of from https://referencesource.microsoft.com/#system/net/System/Net/HttpListenerRequest.cs,1037c8ec82879ba0,references
                var rawCertificateCopy = new byte[NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize];
                Marshal.Copy((IntPtr)NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded, rawCertificateCopy, 0, rawCertificateCopy.Length);
                _certificate = new X509Certificate2(rawCertificateCopy);
            }
 
            return _certificate;
        }
        set
        {
            _certificate = value;
        }
    }
 
    SslProtocols ITlsHandshakeFeature.Protocol => Protocol;
 
    TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => NegotiatedCipherSuite;
 
    string ITlsHandshakeFeature.HostName => SniHostName;
 
    CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => CipherAlgorithm;
 
    int ITlsHandshakeFeature.CipherStrength => CipherStrength;
 
    HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => HashAlgorithm;
 
    int ITlsHandshakeFeature.HashStrength => HashStrength;
 
    ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => KeyExchangeAlgorithm;
 
    int ITlsHandshakeFeature.KeyExchangeStrength => KeyExchangeStrength;
 
    IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
 
    IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
 
    bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; }
 
    bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded;
 
    long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize
    {
        get => MaxRequestBodySize;
        set
        {
            if (HasStartedConsumingRequestBody)
            {
                throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead);
            }
            if (_wasUpgraded)
            {
                throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests);
            }
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
            }
 
            if (value > _options.IisMaxRequestSizeLimit)
            {
                _logger.LogWarning(CoreStrings.MaxRequestLimitWarning);
            }
 
            MaxRequestBodySize = value;
        }
    }
 
    internal IHttpResponseTrailersFeature? GetResponseTrailersFeature()
    {
        return AdvancedHttp2FeaturesSupported() ? this : null;
    }
 
    internal ITlsHandshakeFeature? GetTlsHandshakeFeature()
    {
        return IsHttps ? this : null;
    }
 
    IHeaderDictionary IHttpResponseTrailersFeature.Trailers
    {
        get => ResponseTrailers ??= HttpResponseTrailers;
        set => ResponseTrailers = value;
    }
 
    CancellationToken IConnectionLifetimeNotificationFeature.ConnectionClosedRequested { get; set; }
 
    internal IHttpResetFeature? GetResetFeature()
    {
        return AdvancedHttp2FeaturesSupported() ? this : null;
    }
 
    void IHttpResetFeature.Reset(int errorCode)
    {
        if (errorCode < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(errorCode), "'errorCode' cannot be negative");
        }
 
        SetResetCode(errorCode);
        AbortIO(clientDisconnect: false);
    }
 
    internal unsafe void SetResetCode(int errorCode)
    {
        NativeMethods.HttpResetStream(_requestNativeHandle, (ulong)errorCode);
    }
 
    void IHttpResponseBodyFeature.DisableBuffering()
    {
        NativeMethods.HttpDisableBuffering(_requestNativeHandle);
        DisableCompression();
    }
 
    private void DisableCompression()
    {
        var serverVariableFeature = (IServerVariablesFeature)this;
        serverVariableFeature["IIS_EnableDynamicCompression"] = "0";
    }
 
    void IConnectionLifetimeNotificationFeature.RequestClose()
    {
        // Set the connection close feature if the response hasn't sent headers as yet
        if (!HasResponseStarted)
        {
            ResponseHeaders.Connection = ConnectionClose;
        }
    }
}