File: HttpSysListener.cs
Web Access
Project: src\src\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj (Microsoft.AspNetCore.Server.HttpSys)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.Extensions.Logging;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Networking.HttpServer;
 
namespace Microsoft.AspNetCore.Server.HttpSys;
 
/// <summary>
/// An HTTP server wrapping the Http.Sys APIs that accepts requests.
/// </summary>
internal sealed partial class HttpSysListener : IDisposable
{
    // Win8# 559317 fixed a bug in Http.sys's HttpReceiveClientCertificate method.
    // Without this fix IOCP callbacks were not being called although ERROR_IO_PENDING was
    // returned from HttpReceiveClientCertificate when using the
    // FileCompletionNotificationModes.SkipCompletionPortOnSuccess flag.
    // This bug was only hit when the buffer passed into HttpReceiveClientCertificate
    // (1500 bytes initially) is too small for the certificate.
    // Due to this bug in downlevel operating systems the FileCompletionNotificationModes.SkipCompletionPortOnSuccess
    // flag is only used on Win8 and later.
    internal static readonly bool SkipIOCPCallbackOnSuccess = ComNetOS.IsWin8orLater;
 
    // Mitigate potential DOS attacks by limiting the number of unknown headers we accept.  Numerous header names
    // with hash collisions will cause the server to consume excess CPU.  1000 headers limits CPU time to under
    // 0.5 seconds per request.  Respond with a 400 Bad Request.
    private const int UnknownHeaderLimit = 1000;
 
    internal MemoryPool<byte> MemoryPool { get; } = PinnedBlockMemoryPoolFactory.Create();
 
    private volatile State _state; // m_State is set only within lock blocks, but often read outside locks.
 
    private readonly ServerSession _serverSession;
    private readonly UrlGroup _urlGroup;
    private readonly RequestQueue _requestQueue;
    private readonly DisconnectListener _disconnectListener;
 
    private readonly object _internalLock;
 
    public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(loggerFactory);
 
        if (!HttpApi.Supported)
        {
            throw new PlatformNotSupportedException();
        }
 
        Options = options;
 
        Logger = loggerFactory.CreateLogger<HttpSysListener>();
 
        _state = State.Stopped;
        _internalLock = new object();
 
        // V2 initialization sequence:
        // 1. Create server session
        // 2. Create url group
        // 3. Create request queue
        // 4. Add urls to url group - Done in Start()
        // 5. Attach request queue to url group - Done in Start()
 
        try
        {
            _serverSession = new ServerSession();
 
            _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger);
 
            _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger);
 
            _disconnectListener = new DisconnectListener(_requestQueue, Logger);
        }
        catch (Exception exception)
        {
            // If Url group or request queue creation failed, close server session before throwing.
            _requestQueue?.Dispose();
            _urlGroup?.Dispose();
            _serverSession?.Dispose();
            Log.HttpSysListenerCtorError(Logger, exception);
            throw;
        }
    }
 
    internal enum State
    {
        Stopped,
        Started,
        Disposed,
    }
 
    internal ILogger Logger { get; private set; }
 
    internal UrlGroup UrlGroup
    {
        get { return _urlGroup; }
    }
 
    internal RequestQueue RequestQueue
    {
        get { return _requestQueue; }
    }
 
    internal DisconnectListener DisconnectListener
    {
        get { return _disconnectListener; }
    }
 
    public HttpSysOptions Options { get; }
 
    public bool IsListening
    {
        get { return _state == State.Started; }
    }
 
    /// <summary>
    /// Start accepting incoming requests.
    /// </summary>
    public void Start()
    {
        CheckDisposed();
 
        Log.ListenerStarting(Logger);
 
        // Make sure there are no race conditions between Start/Stop/Abort/Close/Dispose.
        // Start needs to setup all resources. Abort/Stop must not interfere while Start is
        // allocating those resources.
        lock (_internalLock)
        {
            try
            {
                CheckDisposed();
                if (_state == State.Started)
                {
                    return;
                }
 
                // Always configure the UrlGroup if the intent was to create, only configure the queue if we actually created it
                if (Options.RequestQueueMode == RequestQueueMode.Create || Options.RequestQueueMode == RequestQueueMode.CreateOrAttach)
                {
                    Options.Apply(UrlGroup, _requestQueue.Created ? RequestQueue : null);
 
                    UrlGroup.AttachToQueue();
 
                    // All resources are set up correctly. Now add all prefixes.
                    try
                    {
                        Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup);
                    }
                    catch (HttpSysException)
                    {
                        // If an error occurred while adding prefixes, free all resources allocated by previous steps.
                        UrlGroup.DetachFromQueue();
                        throw;
                    }
                }
 
                _state = State.Started;
            }
            catch (Exception exception)
            {
                // Make sure the HttpListener instance can't be used if Start() failed.
                _state = State.Disposed;
                DisposeInternal();
                Log.ListenerStartError(Logger, exception);
                throw;
            }
        }
    }
 
    private void Stop()
    {
        try
        {
            lock (_internalLock)
            {
                CheckDisposed();
                if (_state == State.Stopped)
                {
                    return;
                }
 
                Log.ListenerStopping(Logger);
 
                // If this instance registered URL prefixes then remove them before shutting down.
                if (Options.RequestQueueMode == RequestQueueMode.Create || Options.RequestQueueMode == RequestQueueMode.CreateOrAttach)
                {
                    Options.UrlPrefixes.UnregisterAllPrefixes();
                    UrlGroup.DetachFromQueue();
                }
 
                _state = State.Stopped;
 
            }
        }
        catch (Exception exception)
        {
            Log.ListenerStopError(Logger, exception);
            throw;
        }
    }
 
    /// <summary>
    /// Stop the server and clean up.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
    }
 
    private void Dispose(bool disposing)
    {
        if (!disposing)
        {
            return;
        }
 
        lock (_internalLock)
        {
            try
            {
                if (_state == State.Disposed)
                {
                    return;
                }
                Log.ListenerDisposing(Logger);
 
                Stop();
                DisposeInternal();
            }
            catch (Exception exception)
            {
                Log.ListenerDisposeError(Logger, exception);
                throw;
            }
            finally
            {
                _state = State.Disposed;
            }
        }
    }
 
    private void DisposeInternal()
    {
        // V2 stopping sequence:
        // 1. Detach request queue from url group - Done in Stop()/Abort()
        // 2. Remove urls from url group - Done in Stop()
        // 3. Close request queue - Done in Stop()/Abort()
        // 4. Close Url group.
        // 5. Close server session.
 
        _requestQueue.Dispose();
 
        _urlGroup.Dispose();
 
        Debug.Assert(_serverSession != null, "ServerSessionHandle is null in CloseV2Config");
        Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config");
 
        _serverSession.Dispose();
    }
 
    /// <summary>
    /// Accept a request from the incoming request queue.
    /// </summary>
    internal ValueTask<RequestContext> AcceptAsync(AsyncAcceptContext acceptContext)
    {
        CheckDisposed();
        Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
 
        return acceptContext.AcceptAsync();
    }
 
    internal bool ValidateRequest(NativeRequestContext requestMemory)
    {
        try
        {
            // Block potential DOS attacks
            if (requestMemory.UnknownHeaderCount > UnknownHeaderLimit)
            {
                SendError(requestMemory.RequestId, StatusCodes.Status400BadRequest, authChallenges: null);
                return false;
            }
 
            if (!Options.Authentication.AllowAnonymous && !requestMemory.CheckAuthenticated())
            {
                SendError(requestMemory.RequestId, StatusCodes.Status401Unauthorized,
                    AuthenticationManager.GenerateChallenges(Options.Authentication.Schemes));
                return false;
            }
        }
        catch (Exception ex)
        {
            Log.RequestValidationFailed(Logger, ex, requestMemory.RequestId);
            return false;
        }
 
        return true;
    }
 
    internal unsafe void SendError(ulong requestId, int httpStatusCode, IList<string>? authChallenges = null)
    {
        var httpResponse = new HTTP_RESPONSE_V2();
        httpResponse.Base.Version = new()
        {
            MajorVersion = 1,
            MinorVersion = 1
        };
 
        using UnmanagedBufferAllocator allocator = new();
 
        byte* bytes;
        int bytesLength;
 
        // Copied from the multi-value headers section of SerializeHeaders
        if (authChallenges != null && authChallenges.Count > 0)
        {
            var knownHeaderInfo = allocator.AllocAsPointer<HTTP_RESPONSE_INFO>(1);
            httpResponse.pResponseInfo = knownHeaderInfo;
 
            knownHeaderInfo[httpResponse.ResponseInfoCount].Type = HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
            knownHeaderInfo[httpResponse.ResponseInfoCount].Length =
                (uint)sizeof(HTTP_MULTIPLE_KNOWN_HEADERS);
 
            var header = allocator.AllocAsPointer<HTTP_MULTIPLE_KNOWN_HEADERS>(1);
 
            header->HeaderId = HTTP_HEADER_ID.HttpHeaderWwwAuthenticate;
            header->Flags = PInvoke.HTTP_RESPONSE_INFO_FLAGS_PRESERVE_ORDER; // The docs say this is for www-auth only.
            header->KnownHeaderCount = 0;
 
            var nativeHeaderValues = allocator.AllocAsPointer<HTTP_KNOWN_HEADER>(authChallenges.Count);
            header->KnownHeaders = nativeHeaderValues;
 
            for (var headerValueIndex = 0; headerValueIndex < authChallenges.Count; headerValueIndex++)
            {
                // Add Value
                var headerValue = authChallenges[headerValueIndex];
                bytes = allocator.GetHeaderEncodedBytes(headerValue, out bytesLength);
                nativeHeaderValues[header->KnownHeaderCount].RawValueLength = checked((ushort)bytesLength);
                nativeHeaderValues[header->KnownHeaderCount].pRawValue = (PCSTR)bytes;
                header->KnownHeaderCount++;
            }
 
            knownHeaderInfo[0].pInfo = header;
 
            httpResponse.ResponseInfoCount = 1;
        }
 
        httpResponse.Base.StatusCode = checked((ushort)httpStatusCode);
        var statusDescription = HttpReasonPhrase.Get(httpStatusCode) ?? string.Empty;
        uint dataWritten = 0;
        uint statusCode;
 
        bytes = allocator.GetHeaderEncodedBytes(statusDescription, out bytesLength);
        httpResponse.Base.pReason = (PCSTR)bytes;
        httpResponse.Base.ReasonLength = checked((ushort)bytesLength);
 
        const int contentLengthLength = 1;
        var pContentLength = allocator.AllocAsPointer<byte>(contentLengthLength + 1);
        pContentLength[0] = (byte)'0';
        pContentLength[1] = 0; // null terminator
 
        var knownHeaders = httpResponse.Base.Headers.KnownHeaders.AsSpan();
        knownHeaders[(int)HttpSysResponseHeader.ContentLength].pRawValue = (PCSTR)pContentLength;
        knownHeaders[(int)HttpSysResponseHeader.ContentLength].RawValueLength = contentLengthLength;
        httpResponse.Base.Headers.UnknownHeaderCount = 0;
 
        statusCode = PInvoke.HttpSendHttpResponse(
            _requestQueue.Handle,
            requestId,
            0,
            httpResponse,
            null,
            &dataWritten,
            null,
            null);
        if (statusCode != ErrorCodes.ERROR_SUCCESS)
        {
            // if we fail to send a 401 something's seriously wrong, abort the request
            PInvoke.HttpCancelHttpRequest(_requestQueue.Handle, requestId, default);
        }
    }
 
    private void CheckDisposed()
    {
        ObjectDisposedException.ThrowIf(_state == State.Disposed, this);
    }
}