From 2a461901ece83a62987da4e8c61858482102a1e1 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 2 Jul 2024 11:21:26 +0800 Subject: [PATCH 1/6] Add CreateNamedPipeServerStream to named pipes options --- .../src/CreateNamedPipeServerStreamContext.cs | 27 ++++ .../Internal/NamedPipeConnectionListener.cs | 30 +--- .../src/NamedPipeTransportOptions.cs | 56 +++++++- .../src/PublicAPI.Unshipped.txt | 11 ++ .../Transport.NamedPipes/test/WebHostTests.cs | 128 ++++++++++++++++++ 5 files changed, 227 insertions(+), 25 deletions(-) create mode 100644 src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs 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..aa002a386fc1 --- /dev/null +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs @@ -0,0 +1,27 @@ +// 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; +using PipeOptions = System.IO.Pipes.PipeOptions; + +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 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..cf70b48fedbe 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -56,9 +56,63 @@ 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. + /// + /// An . + /// + /// A instance. + /// + public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNamedPipeServerStreamContext 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..bec87f27aa05 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs @@ -19,6 +19,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Reflection.Metadata; +using System.Globalization; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests; @@ -268,6 +270,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)] From 856e6f63e60158588a187fff500ff155f6cec507 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 2 Jul 2024 11:22:29 +0800 Subject: [PATCH 2/6] Clean up --- .../Transport.NamedPipes/src/NamedPipeTransportOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index cf70b48fedbe..6c59aa5aea87 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -83,7 +83,7 @@ public sealed class NamedPipeTransportOptions /// that can be used by a connection listener /// to listen for inbound requests. /// - /// An . + /// A . /// /// A instance. /// From 8f93c767ae89312e7d65c169c9ce6539e72d1438 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 2 Jul 2024 11:23:06 +0800 Subject: [PATCH 3/6] Clean up --- .../Kestrel/Transport.NamedPipes/test/WebHostTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs index bec87f27aa05..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,13 +15,11 @@ 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; -using System.Reflection.Metadata; -using System.Globalization; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests; From c1c3a92bd811304f897af894bbdc96c486e9b7b0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 2 Jul 2024 11:27:40 +0800 Subject: [PATCH 4/6] Clean up --- .../src/CreateNamedPipeServerStreamContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs index aa002a386fc1..0351b1645516 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs @@ -1,9 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; -using PipeOptions = System.IO.Pipes.PipeOptions; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; From 5adbdc89397456ec47d721866ccadbda4660bcdb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 4 Jul 2024 08:50:31 +0800 Subject: [PATCH 5/6] Feedback --- .../src/CreateNamedPipeServerStreamContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs index 0351b1645516..76f228df9808 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/CreateNamedPipeServerStreamContext.cs @@ -18,7 +18,7 @@ public sealed class CreateNamedPipeServerStreamContext /// /// Gets the pipe options. /// - public PipeOptions PipeOptions { get; init; } + public required PipeOptions PipeOptions { get; init; } /// /// Gets the default access control and audit security. /// From e3d60a79ccae536e49deaf202a7cd446ecc9d676 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 9 Jul 2024 08:30:31 +0800 Subject: [PATCH 6/6] Null check --- .../Transport.NamedPipes/src/NamedPipeTransportOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index 6c59aa5aea87..a1d7d47f4854 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -89,6 +89,8 @@ public sealed class NamedPipeTransportOptions /// public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNamedPipeServerStreamContext context) { + ArgumentNullException.ThrowIfNull(context); + if (context.PipeSecurity != null) { return NamedPipeServerStreamAcl.Create(