// 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;

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();
        }

        Debug.Assert(HttpApi.ApiVersion == HttpApiTypes.HTTP_API_VERSION.Version20, "Invalid Http api version");

        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)
    {
        HttpApiTypes.HTTP_RESPONSE_V2 httpResponse = new HttpApiTypes.HTTP_RESPONSE_V2();
        httpResponse.Response_V1.Version = new HttpApiTypes.HTTP_VERSION();
        httpResponse.Response_V1.Version.MajorVersion = (ushort)1;
        httpResponse.Response_V1.Version.MinorVersion = (ushort)1;

        using UnmanagedBufferAllocator allocator = new();

        byte* bytes;
        int bytesLength;

        // Copied from the multi-value headers section of SerializeHeaders
        if (authChallenges != null && authChallenges.Count > 0)
        {
            HttpApiTypes.HTTP_RESPONSE_INFO* knownHeaderInfo = allocator.AllocAsPointer<HttpApiTypes.HTTP_RESPONSE_INFO>(1);
            httpResponse.pResponseInfo = knownHeaderInfo;

            knownHeaderInfo[httpResponse.ResponseInfoCount].Type = HttpApiTypes.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
            knownHeaderInfo[httpResponse.ResponseInfoCount].Length =
                (uint)sizeof(HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS);

            HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS* header = allocator.AllocAsPointer<HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS>(1);

            header->HeaderId = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderWwwAuthenticate;
            header->Flags = HttpApiTypes.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // The docs say this is for www-auth only.
            header->KnownHeaderCount = 0;

            HttpApiTypes.HTTP_KNOWN_HEADER* nativeHeaderValues = allocator.AllocAsPointer<HttpApiTypes.HTTP_KNOWN_HEADER>(authChallenges.Count);
            header->KnownHeaders = nativeHeaderValues;

            for (int headerValueIndex = 0; headerValueIndex < authChallenges.Count; headerValueIndex++)
            {
                // Add Value
                string headerValue = authChallenges[headerValueIndex];
                bytes = allocator.GetHeaderEncodedBytes(headerValue, out bytesLength);
                nativeHeaderValues[header->KnownHeaderCount].RawValueLength = checked((ushort)bytesLength);
                nativeHeaderValues[header->KnownHeaderCount].pRawValue = bytes;
                header->KnownHeaderCount++;
            }

            knownHeaderInfo[0].pInfo = header;

            httpResponse.ResponseInfoCount = 1;
        }

        httpResponse.Response_V1.StatusCode = checked((ushort)httpStatusCode);
        string statusDescription = HttpReasonPhrase.Get(httpStatusCode) ?? string.Empty;
        uint dataWritten = 0;
        uint statusCode;

        bytes = allocator.GetHeaderEncodedBytes(statusDescription, out bytesLength);
        httpResponse.Response_V1.pReason = bytes;
        httpResponse.Response_V1.ReasonLength = checked((ushort)bytesLength);

        const int contentLengthLength = 1;
        byte* pContentLength = allocator.AllocAsPointer<byte>(contentLengthLength + 1);
        pContentLength[0] = (byte)'0';
        pContentLength[1] = 0; // null terminator

        (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = pContentLength;
        (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = contentLengthLength;
        httpResponse.Response_V1.Headers.UnknownHeaderCount = 0;

        statusCode =
            HttpApi.HttpSendHttpResponse(
                _requestQueue.Handle,
                requestId,
                0,
                &httpResponse,
                null,
                &dataWritten,
                IntPtr.Zero,
                0,
                SafeNativeOverlapped.Zero,
                IntPtr.Zero);
        if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
        {
            // if we fail to send a 401 something's seriously wrong, abort the request
            HttpApi.HttpCancelHttpRequest(_requestQueue.Handle, requestId, IntPtr.Zero);
        }
    }

    private void CheckDisposed()
    {
        ObjectDisposedException.ThrowIf(_state == State.Disposed, this);
    }
}
