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)]