File: NativeInterop\UrlGroup.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.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Networking.HttpServer;
 
namespace Microsoft.AspNetCore.Server.HttpSys;
 
internal sealed partial class UrlGroup : IDisposable
{
    private static readonly int BindingInfoSize =
        Marshal.SizeOf<HTTP_BINDING_INFO>();
    private static readonly int QosInfoSize =
        Marshal.SizeOf<HTTP_QOS_SETTING_INFO>();
    private static readonly int RequestPropertyInfoSize =
        Marshal.SizeOf<HTTP_BINDING_INFO>();
 
    private readonly ILogger _logger;
 
    private readonly ServerSession? _serverSession;
    private readonly RequestQueue _requestQueue;
    private bool _disposed;
    private readonly bool _created;
 
    internal unsafe UrlGroup(ServerSession serverSession, RequestQueue requestQueue, ILogger logger)
    {
        _serverSession = serverSession;
        _requestQueue = requestQueue;
        _logger = logger;
 
        _created = true;
        var statusCode = PInvoke.HttpCreateUrlGroup(_serverSession.Id.DangerousGetServerSessionId(), out var urlGroupId);
 
        if (statusCode != ErrorCodes.ERROR_SUCCESS)
        {
            throw new HttpSysException((int)statusCode);
        }
 
        Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup");
        Id = urlGroupId;
    }
 
    internal ulong Id { get; private set; }
 
    internal unsafe void SetMaxConnections(long maxConnections)
    {
        var connectionLimit = new HTTP_CONNECTION_LIMIT_INFO
        {
            Flags = HttpApi.HTTP_PROPERTY_FLAGS_PRESENT,
            MaxConnections = (uint)maxConnections
        };
 
        var qosSettings = new HTTP_QOS_SETTING_INFO
        {
            QosType = HTTP_QOS_SETTING_TYPE.HttpQosSettingTypeConnectionLimit,
            QosSetting = &connectionLimit
        };
 
        SetProperty(HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize);
    }
 
    internal unsafe void SetDelegationProperty(RequestQueue destination)
    {
        var propertyInfo = new HTTP_BINDING_INFO
        {
            Flags = HttpApi.HTTP_PROPERTY_FLAGS_PRESENT,
            RequestQueueHandle = (HANDLE)destination.Handle.DangerousGetHandle()
        };
 
        SetProperty(HTTP_SERVER_PROPERTY.HttpServerDelegationProperty, new IntPtr(&propertyInfo), (uint)RequestPropertyInfoSize);
    }
 
    internal unsafe void UnSetDelegationProperty(RequestQueue destination, bool throwOnError = true)
    {
        var propertyInfo = new HTTP_BINDING_INFO
        {
            RequestQueueHandle = (HANDLE)destination.Handle.DangerousGetHandle()
        };
 
        SetProperty(HTTP_SERVER_PROPERTY.HttpServerDelegationProperty, new IntPtr(&propertyInfo), (uint)RequestPropertyInfoSize, throwOnError);
    }
 
    internal unsafe void SetProperty(HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true)
    {
        Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer");
        CheckDisposed();
 
        var statusCode = PInvoke.HttpSetUrlGroupProperty(Id, property, info.ToPointer(), infosize);
 
        if (statusCode != ErrorCodes.ERROR_SUCCESS)
        {
            var exception = new HttpSysException((int)statusCode);
            Log.SetUrlPropertyError(_logger, exception);
            if (throwOnError)
            {
                throw exception;
            }
        }
    }
 
    internal unsafe void AttachToQueue()
    {
        CheckDisposed();
        // Set the association between request queue and url group. After this, requests for registered urls will
        // get delivered to this request queue.
 
        var info = new HTTP_BINDING_INFO
        {
            Flags = HttpApi.HTTP_PROPERTY_FLAGS_PRESENT,
            RequestQueueHandle = (HANDLE)_requestQueue.Handle.DangerousGetHandle()
        };
 
        var infoptr = new IntPtr(&info);
 
        SetProperty(HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
            infoptr, (uint)BindingInfoSize);
    }
 
    internal unsafe void DetachFromQueue()
    {
        CheckDisposed();
        // Break the association between request queue and url group. After this, requests for registered urls
        // will get 503s.
        // Note that this method may be called multiple times (Stop() and then Abort()). This
        // is fine since http.sys allows to set HttpServerBindingProperty multiple times for valid
        // Url groups.
 
        var info = new HTTP_BINDING_INFO();
        var infoptr = new IntPtr(&info);
 
        SetProperty(HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
            infoptr, (uint)BindingInfoSize, throwOnError: false);
    }
 
    internal void RegisterPrefix(string uriPrefix, int contextId)
    {
        Log.RegisteringPrefix(_logger, uriPrefix);
        CheckDisposed();
        var statusCode = PInvoke.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId);
        if (statusCode != ErrorCodes.ERROR_SUCCESS)
        {
            if (statusCode == ErrorCodes.ERROR_ALREADY_EXISTS)
            {
                // If we didn't create the queue and the uriPrefix already exists, confirm it exists for the
                // queue we attached to, if so we are all good, otherwise throw an already registered error.
                if (!_requestQueue.Created)
                {
                    unsafe
                    {
                        var findUrlStatusCode = PInvoke.HttpFindUrlGroupId(uriPrefix, _requestQueue.Handle, out var _);
                        if (findUrlStatusCode == ErrorCodes.ERROR_SUCCESS)
                        {
                            // Already registered for the desired queue, all good
                            return;
                        }
                    }
                }
 
                throw new HttpSysException((int)statusCode, Resources.FormatException_PrefixAlreadyRegistered(uriPrefix));
            }
            if (statusCode == ErrorCodes.ERROR_ACCESS_DENIED)
            {
                throw new HttpSysException((int)statusCode, Resources.FormatException_AccessDenied(uriPrefix, Environment.UserDomainName + @"\" + Environment.UserName));
            }
            throw new HttpSysException((int)statusCode);
        }
    }
 
    internal void UnregisterPrefix(string uriPrefix)
    {
        Log.UnregisteringPrefix(_logger, uriPrefix);
        CheckDisposed();
 
        PInvoke.HttpRemoveUrlFromUrlGroup(Id, uriPrefix, 0);
    }
 
    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }
 
        _disposed = true;
 
        if (_created)
        {
 
            Debug.Assert(Id != 0, "HttpCloseUrlGroup called with invalid url group id");
 
            var statusCode = PInvoke.HttpCloseUrlGroup(Id);
 
            if (statusCode != ErrorCodes.ERROR_SUCCESS)
            {
                Log.CloseUrlGroupError(_logger, statusCode);
            }
 
        }
 
        Id = 0;
    }
 
    private void CheckDisposed()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
    }
}