File: NativeInterop\UrlGroup.cs
Web Access
Project: src\aspnetcore\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
{
    // See https://learn.microsoft.com/windows/win32/api/http/ns-http-http_channel_bind_info
    private const uint HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN = 0x00000008;
 
    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 static readonly int ChannelBindInfoSize = Marshal.SizeOf<HTTP_CHANNEL_BIND_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;
    }
 
    // Sets HttpServerChannelBindProperty with the requested hardening level and the HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN flag
    // so the per-request CBT is delivered to the app.
    internal unsafe void SetChannelBindingProperty(HttpAuthenticationHardeningLevel level)
    {
        var info = new HTTP_CHANNEL_BIND_INFO
        {
            Hardening = level switch
            {
                HttpAuthenticationHardeningLevel.Strict => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningStrict,
                HttpAuthenticationHardeningLevel.Legacy => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningLegacy,
                HttpAuthenticationHardeningLevel.Medium or _ => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningMedium,
            },
            ServiceNames = (HTTP_SERVICE_BINDING_BASE**)IntPtr.Zero,
            NumberOfServiceNames = 0,
 
            // optimize: Flags control if CBT is included in NativeRequest, and if legacy level is used, no point to load the CBT data.
            Flags = level == HttpAuthenticationHardeningLevel.Legacy
                ? default
                : HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN,
        };
 
        SetProperty(
            HTTP_SERVER_PROPERTY.HttpServerChannelBindProperty,
            new IntPtr(&info),
            (uint)ChannelBindInfoSize,
            throwOnError: level == HttpAuthenticationHardeningLevel.Strict);
    }
 
    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);
    }
}