File: DelegateTests.cs
Web Access
Project: src\src\Servers\HttpSys\test\FunctionalTests\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj (Microsoft.AspNetCore.Server.HttpSys.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
 
namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests;
 
public class DelegateTests : LoggedTest
{
    private static readonly string _expectedResponseString = "Hello from delegatee";
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public void IServerDelegationFeature_IsAvailableFromServices()
    {
        var builder = new HostBuilder();
        builder.ConfigureWebHost(webHost =>
        {
            webHost.UseHttpSys();
        });
        using var host = builder.Build();
        var server = host.Services.GetRequiredService<IServer>();
        var delegationFeature = host.Services.GetRequiredService<IServerDelegationFeature>();
        Assert.Same(server, delegationFeature);
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task DelegateRequestTest()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
        options =>
        {
            options.RequestQueueName = queueName;
        }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            delegateFeature.DelegateRequest(destination);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        var responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task DelegateAfterWriteToResponseBodyShouldThrowTest()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, httpContext =>
        {
            httpContext.Response.StatusCode = StatusCodes.Status418ImATeapot;
            return Task.CompletedTask;
        },
        options =>
        {
            options.RequestQueueName = queueName;
        }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            Assert.False(delegateFeature.CanDelegate);
            Assert.Throws<InvalidOperationException>(() => delegateFeature.DelegateRequest(destination));
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        var responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task WriteToBodyAfterDelegateShouldNoOp()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
        options =>
        {
            options.RequestQueueName = queueName;
        }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            delegateFeature.DelegateRequest(destination);
            Assert.False(delegateFeature.CanDelegate);
            httpContext.Response.WriteAsync(_expectedResponseString);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        var responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task DelegateAfterRequestBodyReadShouldThrow()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, httpContext =>
        {
            httpContext.Response.StatusCode = StatusCodes.Status418ImATeapot;
            return Task.CompletedTask;
        },
       options =>
       {
           options.RequestQueueName = queueName;
       }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, async httpContext =>
        {
            var memoryStream = new MemoryStream();
            await httpContext.Request.Body.CopyToAsync(memoryStream);
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            Assert.Throws<InvalidOperationException>(() => delegateFeature.DelegateRequest(destination));
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        _ = await SendRequestWithBodyAsync(delegatorAddress);
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(false)]
    public async Task DelegationFeaturesAreNull()
    {
        // Testing the DelegateSupportedCondition
        Assert.True(Environment.OSVersion.Version < new Version(10, 0, 22000), "This should be supported on Win 11.");
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            Assert.Null(delegateFeature);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        Assert.Null(delegationProperty);
 
        _ = await SendRequestAsync(delegatorAddress);
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task UpdateDelegationRuleTest()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
       options =>
       {
           options.RequestQueueName = queueName;
       }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            delegateFeature.DelegateRequest(destination);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
        // Send a request to ensure the rule is fully active
        var responseString = await SendRequestAsync(delegatorAddress);
        destination?.Dispose();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
        responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/60141")]
    public async Task DelegateAfterReceiverRestart()
    {
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
        options =>
        {
            options.RequestQueueName = queueName;
        }, LoggerFactory);
 
        DelegationRule destination = default;
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            delegateFeature.DelegateRequest(destination);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        var responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
 
        // Stop the receiver
        receiver?.Dispose();
 
        // Start the receiver again but this time we need to use CreateOrAttach to attach to the existing queue and setup the UrlPrefixes
        using var receiverRestarted = (MessagePump)Utilities.CreateHttpServer(out receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
        options =>
        {
            options.RequestQueueName = queueName;
            options.RequestQueueMode = RequestQueueMode.CreateOrAttach;
            options.UrlPrefixes.Clear();
            options.UrlPrefixes.Add(receiverAddress);
        }, LoggerFactory);
 
        responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
 
        destination?.Dispose();
    }
 
    [ConditionalFact]
    [DelegateSupportedCondition(true)]
    public async Task DelegateRequestTestCanSetSecurityDescriptor()
    {
        // Create a new security descriptor
        CommonSecurityDescriptor securityDescriptor = new CommonSecurityDescriptor(false, false, string.Empty);
 
        // Create a discretionary access control list (DACL)
        DiscretionaryAcl dacl = new DiscretionaryAcl(false, false, 2);
        dacl.AddAccess(AccessControlType.Allow, new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), -1, InheritanceFlags.None, PropagationFlags.None);
        dacl.AddAccess(AccessControlType.Deny, new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null), -1, InheritanceFlags.None, PropagationFlags.None);
 
        // Assign the DACL to the security descriptor
        securityDescriptor.DiscretionaryAcl = dacl;
 
        var queueName = Guid.NewGuid().ToString();
        using var receiver = Utilities.CreateHttpServer(out var receiverAddress, async httpContext =>
        {
            await httpContext.Response.WriteAsync(_expectedResponseString);
        },
        options =>
        {
            options.RequestQueueName = queueName;
            options.RequestQueueSecurityDescriptor = securityDescriptor;
        }, LoggerFactory);
 
        DelegationRule destination = default;
 
        using var delegator = Utilities.CreateHttpServer(out var delegatorAddress, httpContext =>
        {
            var delegateFeature = httpContext.Features.Get<IHttpSysRequestDelegationFeature>();
            delegateFeature.DelegateRequest(destination);
            return Task.CompletedTask;
        }, LoggerFactory);
 
        var delegationProperty = delegator.Features.Get<IServerDelegationFeature>();
        destination = delegationProperty.CreateDelegationRule(queueName, receiverAddress);
 
        AssertPermissions(destination.Queue.Handle);
        unsafe void AssertPermissions(SafeHandle handle)
        {
            PSECURITY_DESCRIPTOR pSecurityDescriptor = new();
 
            WIN32_ERROR result = PInvoke.GetSecurityInfo(
                handle,
                Windows.Win32.Security.Authorization.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
                4, // DACL_SECURITY_INFORMATION
                null,
                null,
                null,
                null,
                &pSecurityDescriptor);
 
            var length = (int)PInvoke.GetSecurityDescriptorLength(pSecurityDescriptor);
 
            // Copy the security descriptor to a managed byte array
            byte[] securityDescriptorBytes = new byte[length];
            Marshal.Copy(new IntPtr(pSecurityDescriptor.Value), securityDescriptorBytes, 0, length);
 
            // Convert the byte array to a RawSecurityDescriptor
            var securityDescriptor = new RawSecurityDescriptor(securityDescriptorBytes, 0);
 
            var checkedAllowUser = false;
            var checkedDenyGuest = false;
 
            foreach (CommonAce ace in securityDescriptor.DiscretionaryAcl)
            {
                if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinGuestsSid))
                {
                    Assert.Equal(AceType.AccessDenied, ace.AceType);
                    checkedDenyGuest = true;
                }
                else if (ace.SecurityIdentifier.IsWellKnown(WellKnownSidType.BuiltinUsersSid))
                {
                    Assert.Equal(AceType.AccessAllowed, ace.AceType);
                    checkedAllowUser = true;
                }
            }
 
            PInvoke.LocalFree((HLOCAL)pSecurityDescriptor.Value);
 
            Assert.True(checkedDenyGuest && checkedAllowUser, "DACL does not contain the expected ACEs");
        }
 
        var responseString = await SendRequestAsync(delegatorAddress);
        Assert.Equal(_expectedResponseString, responseString);
        destination?.Dispose();
    }
 
    private async Task<string> SendRequestAsync(string uri)
    {
        using var client = new HttpClient();
        return await client.GetStringAsync(uri);
    }
 
    private async Task<string> SendRequestWithBodyAsync(string uri)
    {
        using var client = new HttpClient();
        var content = new StringContent("Sample request body");
        var response = await client.PostAsync(uri, content);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}