Skip to content

Commit 3abdcb7

Browse files
committed
Add CreateNamedPipeServerStream to named pipes options
1 parent d301328 commit 3abdcb7

File tree

5 files changed

+227
-25
lines changed

5 files changed

+227
-25
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO.Pipes;
5+
using Microsoft.AspNetCore.Connections;
6+
using PipeOptions = System.IO.Pipes.PipeOptions;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
9+
10+
/// <summary>
11+
/// Provides information about an endpoint when creating a <see cref="NamedPipeServerStream"/>.
12+
/// </summary>
13+
public sealed class CreateNamedPipeServerStreamContext
14+
{
15+
/// <summary>
16+
/// Gets the endpoint.
17+
/// </summary>
18+
public required NamedPipeEndPoint NamedPipeEndPoint { get; init; }
19+
/// <summary>
20+
/// Gets the pipe options.
21+
/// </summary>
22+
public PipeOptions PipeOptions { get; init; }
23+
/// <summary>
24+
/// Gets the default access control and audit security.
25+
/// </summary>
26+
public PipeSecurity? PipeSecurity { get; init; }
27+
}

src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ public NamedPipeServerStreamPoolPolicy(NamedPipeEndPoint endpoint, NamedPipeTran
194194

195195
public NamedPipeServerStream Create()
196196
{
197-
NamedPipeServerStream stream;
198197
var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough;
199198
if (!_hasFirstPipeStarted)
200199
{
@@ -209,30 +208,13 @@ public NamedPipeServerStream Create()
209208
pipeOptions |= NamedPipeOptions.CurrentUserOnly;
210209
}
211210

212-
if (_options.PipeSecurity != null)
211+
var context = new CreateNamedPipeServerStreamContext
213212
{
214-
stream = NamedPipeServerStreamAcl.Create(
215-
_endpoint.PipeName,
216-
PipeDirection.InOut,
217-
NamedPipeServerStream.MaxAllowedServerInstances,
218-
PipeTransmissionMode.Byte,
219-
pipeOptions,
220-
inBufferSize: 0, // Buffer in System.IO.Pipelines
221-
outBufferSize: 0, // Buffer in System.IO.Pipelines
222-
_options.PipeSecurity);
223-
}
224-
else
225-
{
226-
stream = new NamedPipeServerStream(
227-
_endpoint.PipeName,
228-
PipeDirection.InOut,
229-
NamedPipeServerStream.MaxAllowedServerInstances,
230-
PipeTransmissionMode.Byte,
231-
pipeOptions,
232-
inBufferSize: 0,
233-
outBufferSize: 0);
234-
}
235-
return stream;
213+
NamedPipeEndPoint = _endpoint,
214+
PipeOptions = pipeOptions,
215+
PipeSecurity = _options.PipeSecurity
216+
};
217+
return _options.CreateNamedPipeServerStream(context);
236218
}
237219

238220
public bool Return(NamedPipeServerStream obj) => !obj.IsConnected;

src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,63 @@ public sealed class NamedPipeTransportOptions
5656
public bool CurrentUserOnly { get; set; } = true;
5757

5858
/// <summary>
59-
/// Gets or sets the security information that determines the access control and audit security for pipes.
59+
/// Gets or sets the security information that determines the default access control and audit security for pipes.
6060
/// </summary>
61+
/// <remarks>
62+
/// <para>
63+
/// Defaults to <c>null</c>, which is no pipe security.
64+
/// </para>
65+
/// <para>
66+
/// Configuring <see cref="PipeSecurity"/> sets the default access control and audit security for pipes.
67+
/// If per-endpoint security is needed then <see cref="CreateNamedPipeServerStream"/> can be configured
68+
/// to create streams with different security settings.</para>
69+
/// </remarks>
6170
public PipeSecurity? PipeSecurity { get; set; }
6271

72+
/// <summary>
73+
/// A function used to create a new <see cref="NamedPipeServerStream"/> to listen with. If
74+
/// not set, <see cref="CreateDefaultNamedPipeServerStream" /> is used.
75+
/// </summary>
76+
/// <remarks>
77+
/// Defaults to <see cref="CreateDefaultNamedPipeServerStream"/>.
78+
/// </remarks>
79+
public Func<CreateNamedPipeServerStreamContext, NamedPipeServerStream> CreateNamedPipeServerStream { get; set; } = CreateDefaultNamedPipeServerStream;
80+
81+
/// <summary>
82+
/// Creates a default instance of <see cref="NamedPipeServerStream"/> for the given
83+
/// <see cref="CreateNamedPipeServerStreamContext"/> that can be used by a connection listener
84+
/// to listen for inbound requests.
85+
/// </summary>
86+
/// <param name="context">An <see cref="CreateNamedPipeServerStreamContext"/>.</param>
87+
/// <returns>
88+
/// A <see cref="NamedPipeServerStream"/> instance.
89+
/// </returns>
90+
public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNamedPipeServerStreamContext context)
91+
{
92+
if (context.PipeSecurity != null)
93+
{
94+
return NamedPipeServerStreamAcl.Create(
95+
context.NamedPipeEndPoint.PipeName,
96+
PipeDirection.InOut,
97+
NamedPipeServerStream.MaxAllowedServerInstances,
98+
PipeTransmissionMode.Byte,
99+
context.PipeOptions,
100+
inBufferSize: 0, // Buffer in System.IO.Pipelines
101+
outBufferSize: 0, // Buffer in System.IO.Pipelines
102+
context.PipeSecurity);
103+
}
104+
else
105+
{
106+
return new NamedPipeServerStream(
107+
context.NamedPipeEndPoint.PipeName,
108+
PipeDirection.InOut,
109+
NamedPipeServerStream.MaxAllowedServerInstances,
110+
PipeTransmissionMode.Byte,
111+
context.PipeOptions,
112+
inBufferSize: 0,
113+
outBufferSize: 0);
114+
}
115+
}
116+
63117
internal Func<MemoryPool<byte>> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create;
64118
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext
3+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.CreateNamedPipeServerStreamContext() -> void
4+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.NamedPipeEndPoint.get -> Microsoft.AspNetCore.Connections.NamedPipeEndPoint!
5+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.NamedPipeEndPoint.init -> void
6+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeOptions.get -> System.IO.Pipes.PipeOptions
7+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeOptions.init -> void
8+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeSecurity.get -> System.IO.Pipes.PipeSecurity?
9+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext.PipeSecurity.init -> void
10+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateNamedPipeServerStream.get -> System.Func<Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext!, System.IO.Pipes.NamedPipeServerStream!>!
11+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateNamedPipeServerStream.set -> void
12+
static Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CreateDefaultNamedPipeServerStream(Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.CreateNamedPipeServerStreamContext! context) -> System.IO.Pipes.NamedPipeServerStream!

src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
using Microsoft.Extensions.DependencyInjection;
2020
using Microsoft.Extensions.Hosting;
2121
using Microsoft.Extensions.Logging;
22+
using System.Reflection.Metadata;
23+
using System.Globalization;
2224

2325
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests;
2426

@@ -268,6 +270,132 @@ public async Task ListenNamedPipeEndpoint_Impersonation_ClientSuccess()
268270
}
269271
}
270272

273+
[ConditionalFact]
274+
[NamedPipesSupported]
275+
public async Task ListenNamedPipeEndpoint_Security_PerEndpointSecuritySettings()
276+
{
277+
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
278+
279+
// Arrange
280+
using var httpEventSource = new HttpEventSourceListener(LoggerFactory);
281+
var defaultSecurityPipeName = NamedPipeTestHelpers.GetUniquePipeName();
282+
var customSecurityPipeName = NamedPipeTestHelpers.GetUniquePipeName();
283+
284+
var builder = new HostBuilder()
285+
.ConfigureWebHost(webHostBuilder =>
286+
{
287+
webHostBuilder
288+
.UseKestrel(o =>
289+
{
290+
o.ListenNamedPipe(defaultSecurityPipeName, listenOptions =>
291+
{
292+
listenOptions.Protocols = HttpProtocols.Http1;
293+
});
294+
o.ListenNamedPipe(customSecurityPipeName, listenOptions =>
295+
{
296+
listenOptions.Protocols = HttpProtocols.Http1;
297+
});
298+
})
299+
.UseNamedPipes(options =>
300+
{
301+
var defaultSecurity = new PipeSecurity();
302+
defaultSecurity.AddAccessRule(new PipeAccessRule("Users", PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow));
303+
304+
options.PipeSecurity = defaultSecurity;
305+
options.CurrentUserOnly = false;
306+
options.CreateNamedPipeServerStream = (context) =>
307+
{
308+
if (context.NamedPipeEndPoint.PipeName == defaultSecurityPipeName)
309+
{
310+
return NamedPipeTransportOptions.CreateDefaultNamedPipeServerStream(context);
311+
}
312+
313+
var allowSecurity = new PipeSecurity();
314+
allowSecurity.AddAccessRule(new PipeAccessRule("Users", PipeAccessRights.FullControl, AccessControlType.Allow));
315+
316+
return NamedPipeServerStreamAcl.Create(
317+
context.NamedPipeEndPoint.PipeName,
318+
PipeDirection.InOut,
319+
NamedPipeServerStream.MaxAllowedServerInstances,
320+
PipeTransmissionMode.Byte,
321+
context.PipeOptions,
322+
inBufferSize: 0, // Buffer in System.IO.Pipelines
323+
outBufferSize: 0, // Buffer in System.IO.Pipelines
324+
allowSecurity);
325+
};
326+
})
327+
.Configure(app =>
328+
{
329+
app.Run(async context =>
330+
{
331+
var serverName = Thread.CurrentPrincipal.Identity.Name;
332+
333+
var namedPipeStream = context.Features.Get<IConnectionNamedPipeFeature>().NamedPipe;
334+
335+
var security = namedPipeStream.GetAccessControl();
336+
var rules = security.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier));
337+
338+
context.Response.Headers.Add("X-PipeAccessRights", ((int)rules.OfType<PipeAccessRule>().Single().PipeAccessRights).ToString(CultureInfo.InvariantCulture));
339+
340+
await context.Response.WriteAsync("hello, world");
341+
});
342+
});
343+
})
344+
.ConfigureServices(AddTestLogging);
345+
346+
using (var host = builder.Build())
347+
{
348+
await host.StartAsync().DefaultTimeout();
349+
350+
using (var client = CreateClient(defaultSecurityPipeName))
351+
{
352+
var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1/")
353+
{
354+
Version = HttpVersion.Version11,
355+
VersionPolicy = HttpVersionPolicy.RequestVersionExact
356+
};
357+
358+
// Act
359+
var response = await client.SendAsync(request).DefaultTimeout();
360+
361+
// Assert
362+
response.EnsureSuccessStatusCode();
363+
Assert.Equal(HttpVersion.Version11, response.Version);
364+
var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout();
365+
Assert.Equal("hello, world", responseText);
366+
367+
var pipeAccessRights = (PipeAccessRights)Convert.ToInt32(string.Join(",", response.Headers.GetValues("X-PipeAccessRights")), CultureInfo.InvariantCulture);
368+
369+
Assert.Equal(PipeAccessRights.ReadWrite, pipeAccessRights & PipeAccessRights.ReadWrite);
370+
Assert.Equal(PipeAccessRights.CreateNewInstance, pipeAccessRights & PipeAccessRights.CreateNewInstance);
371+
}
372+
373+
using (var client = CreateClient(customSecurityPipeName))
374+
{
375+
var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1/")
376+
{
377+
Version = HttpVersion.Version11,
378+
VersionPolicy = HttpVersionPolicy.RequestVersionExact
379+
};
380+
381+
// Act
382+
var response = await client.SendAsync(request).DefaultTimeout();
383+
384+
// Assert
385+
response.EnsureSuccessStatusCode();
386+
Assert.Equal(HttpVersion.Version11, response.Version);
387+
var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout();
388+
Assert.Equal("hello, world", responseText);
389+
390+
var pipeAccessRights = (PipeAccessRights)Convert.ToInt32(string.Join(",", response.Headers.GetValues("X-PipeAccessRights")), CultureInfo.InvariantCulture);
391+
392+
Assert.Equal(PipeAccessRights.FullControl, pipeAccessRights & PipeAccessRights.FullControl);
393+
}
394+
395+
await host.StopAsync().DefaultTimeout();
396+
}
397+
}
398+
271399
[ConditionalTheory]
272400
[NamedPipesSupported]
273401
[InlineData(HttpProtocols.Http1)]

0 commit comments

Comments
 (0)