diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs new file mode 100644 index 000000000000..76f228df9808 --- /dev/null +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; + +/// +/// Provides information about an endpoint when creating a . +/// +public sealed class CreateNamedPipeServerStreamContext +{ + /// + /// Gets the endpoint. + /// + public required NamedPipeEndPoint NamedPipeEndPoint { get; init; } + /// + /// Gets the pipe options. + /// + public required PipeOptions PipeOptions { get; init; } + /// + /// Gets the default access control and audit security. + /// + public PipeSecurity? PipeSecurity { get; init; } +} diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index cb3f09432d85..ab172bc28476 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -194,7 +194,6 @@ public NamedPipeServerStreamPoolPolicy(NamedPipeEndPoint endpoint, NamedPipeTran public NamedPipeServerStream Create() { - NamedPipeServerStream stream; var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough; if (!_hasFirstPipeStarted) { @@ -209,30 +208,13 @@ public NamedPipeServerStream Create() pipeOptions |= NamedPipeOptions.CurrentUserOnly; } - if (_options.PipeSecurity != null) + var context = new CreateNamedPipeServerStreamContext { - stream = NamedPipeServerStreamAcl.Create( - _endpoint.PipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, - pipeOptions, - inBufferSize: 0, // Buffer in System.IO.Pipelines - outBufferSize: 0, // Buffer in System.IO.Pipelines - _options.PipeSecurity); - } - else - { - stream = new NamedPipeServerStream( - _endpoint.PipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, - pipeOptions, - inBufferSize: 0, - outBufferSize: 0); - } - return stream; + NamedPipeEndPoint = _endpoint, + PipeOptions = pipeOptions, + PipeSecurity = _options.PipeSecurity + }; + return _options.CreateNamedPipeServerStream(context); } public bool Return(NamedPipeServerStream obj) => !obj.IsConnected; diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index 612ef6256b68..a1d7d47f4854 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -56,9 +56,65 @@ public sealed class NamedPipeTransportOptions public bool CurrentUserOnly { get; set; } = true; /// - /// Gets or sets the security information that determines the access control and audit security for pipes. + /// Gets or sets the security information that determines the default access control and audit security for pipes. /// + /// + /// + /// Defaults to null, which is no pipe security. + /// + /// + /// Configuring sets the default access control and audit security for pipes. + /// If per-endpoint security is needed then can be configured + /// to create streams with different security settings. + /// public PipeSecurity? PipeSecurity { get; set; } + /// + /// A function used to create a new to listen with. If + /// not set, is used. + /// + /// + /// Defaults to . + /// + public Func CreateNamedPipeServerStream { get; set; } = CreateDefaultNamedPipeServerStream; + + /// + /// Creates a default instance of for the given + /// that can be used by a connection listener + /// to listen for inbound requests. + /// + /// A . + /// + /// A instance. + /// + public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNamedPipeServerStreamContext context) + { + ArgumentNullException.ThrowIfNull(context); + + if (context.PipeSecurity != null) + { + return NamedPipeServerStreamAcl.Create( + context.NamedPipeEndPoint.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + context.PipeOptions, + inBufferSize: 0, // Buffer in System.IO.Pipelines + outBufferSize: 0, // Buffer in System.IO.Pipelines + context.PipeSecurity); + } + else + { + return new NamedPipeServerStream( + context.NamedPipeEndPoint.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + context.PipeOptions, + inBufferSize: 0, + outBufferSize: 0); + } + } + internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.NamedPipes/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..e5e7753bb42a 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/PublicAPI.Unshipped.txt @@ -1 +1,12 @@ #nullable enable +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.CreateNamedPipeServerStreamContext() -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.NamedPipeEndPoint.get -> Microsoft.AspNetCore.Connections.NamedPipeEndPoint! +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.NamedPipeEndPoint.init -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeOptions.get -> System.IO.Pipes.PipeOptions +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeOptions.init -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeSecurity.get -> System.IO.Pipes.PipeSecurity? +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeSecurity.init -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateNamedPipeServerStream.get -> System.Func! +Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateNamedPipeServerStream.set -> void +static Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateDefaultNamedPipeServerStream(Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext! context) -> System.IO.Pipes.NamedPipeServerStream! diff --git a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs index 591ea9cf7324..effc95f1ef5e 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using System.IO.Pipes; using System.Net; using System.Net.Http; @@ -14,8 +15,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -268,6 +269,132 @@ public async Task ListenNamedPipeEndpoint_Impersonation_ClientSuccess() } } + [ConditionalFact] + [NamedPipesSupported] + public async Task ListenNamedPipeEndpoint_Security_PerEndpointSecuritySettings() + { + AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal); + + // Arrange + using var httpEventSource = new HttpEventSourceListener(LoggerFactory); + var defaultSecurityPipeName = NamedPipeTestHelpers.GetUniquePipeName(); + var customSecurityPipeName = NamedPipeTestHelpers.GetUniquePipeName(); + + var builder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel(o => + { + o.ListenNamedPipe(defaultSecurityPipeName, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1; + }); + o.ListenNamedPipe(customSecurityPipeName, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1; + }); + }) + .UseNamedPipes(options => + { + var defaultSecurity = new PipeSecurity(); + defaultSecurity.AddAccessRule(new PipeAccessRule("Users", PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); + + options.PipeSecurity = defaultSecurity; + options.CurrentUserOnly = false; + options.CreateNamedPipeServerStream = (context) => + { + if (context.NamedPipeEndPoint.PipeName == defaultSecurityPipeName) + { + return NamedPipeTransportOptions.CreateDefaultNamedPipeServerStream(context); + } + + var allowSecurity = new PipeSecurity(); + allowSecurity.AddAccessRule(new PipeAccessRule("Users", PipeAccessRights.FullControl, AccessControlType.Allow)); + + return NamedPipeServerStreamAcl.Create( + context.NamedPipeEndPoint.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + context.PipeOptions, + inBufferSize: 0, // Buffer in System.IO.Pipelines + outBufferSize: 0, // Buffer in System.IO.Pipelines + allowSecurity); + }; + }) + .Configure(app => + { + app.Run(async context => + { + var serverName = Thread.CurrentPrincipal.Identity.Name; + + var namedPipeStream = context.Features.Get().NamedPipe; + + var security = namedPipeStream.GetAccessControl(); + var rules = security.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier)); + + context.Response.Headers.Add("X-PipeAccessRights", ((int)rules.OfType().Single().PipeAccessRights).ToString(CultureInfo.InvariantCulture)); + + await context.Response.WriteAsync("hello, world"); + }); + }); + }) + .ConfigureServices(AddTestLogging); + + using (var host = builder.Build()) + { + await host.StartAsync().DefaultTimeout(); + + using (var client = CreateClient(defaultSecurityPipeName)) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1/") + { + Version = HttpVersion.Version11, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + // Act + var response = await client.SendAsync(request).DefaultTimeout(); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(HttpVersion.Version11, response.Version); + var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout(); + Assert.Equal("hello, world", responseText); + + var pipeAccessRights = (PipeAccessRights)Convert.ToInt32(string.Join(",", response.Headers.GetValues("X-PipeAccessRights")), CultureInfo.InvariantCulture); + + Assert.Equal(PipeAccessRights.ReadWrite, pipeAccessRights & PipeAccessRights.ReadWrite); + Assert.Equal(PipeAccessRights.CreateNewInstance, pipeAccessRights & PipeAccessRights.CreateNewInstance); + } + + using (var client = CreateClient(customSecurityPipeName)) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1/") + { + Version = HttpVersion.Version11, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + // Act + var response = await client.SendAsync(request).DefaultTimeout(); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(HttpVersion.Version11, response.Version); + var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout(); + Assert.Equal("hello, world", responseText); + + var pipeAccessRights = (PipeAccessRights)Convert.ToInt32(string.Join(",", response.Headers.GetValues("X-PipeAccessRights")), CultureInfo.InvariantCulture); + + Assert.Equal(PipeAccessRights.FullControl, pipeAccessRights & PipeAccessRights.FullControl); + } + + await host.StopAsync().DefaultTimeout(); + } + } + [ConditionalTheory] [NamedPipesSupported] [InlineData(HttpProtocols.Http1)]