Skip to content

Commit 3105342

Browse files
authored
Add CreateNamedPipeServerStream to named pipes options (#56567)
1 parent 1dd3549 commit 3105342

File tree

5 files changed

+228
-26
lines changed

5 files changed

+228
-26
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
7+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
8+
9+
/// <summary>
10+
/// Provides information about an endpoint when creating a <see cref="NamedPipeServerStream"/>.
11+
/// </summary>
12+
public sealed class CreateNamedPipeServerStreamContext
13+
{
14+
/// <summary>
15+
/// Gets the endpoint.
16+
/// </summary>
17+
public required NamedPipeEndPoint NamedPipeEndPoint { get; init; }
18+
/// <summary>
19+
/// Gets the pipe options.
20+
/// </summary>
21+
public required PipeOptions PipeOptions { get; init; }
22+
/// <summary>
23+
/// Gets the default access control and audit security.
24+
/// </summary>
25+
public PipeSecurity? PipeSecurity { get; init; }
26+
}

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: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,65 @@ 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">A <see cref="CreateNamedPipeServerStreamContext"/>.</param>
87+
/// <returns>
88+
/// A <see cref="NamedPipeServerStream"/> instance.
89+
/// </returns>
90+
public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNamedPipeServerStreamContext context)
91+
{
92+
ArgumentNullException.ThrowIfNull(context);
93+
94+
if (context.PipeSecurity != null)
95+
{
96+
return NamedPipeServerStreamAcl.Create(
97+
context.NamedPipeEndPoint.PipeName,
98+
PipeDirection.InOut,
99+
NamedPipeServerStream.MaxAllowedServerInstances,
100+
PipeTransmissionMode.Byte,
101+
context.PipeOptions,
102+
inBufferSize: 0, // Buffer in System.IO.Pipelines
103+
outBufferSize: 0, // Buffer in System.IO.Pipelines
104+
context.PipeSecurity);
105+
}
106+
else
107+
{
108+
return new NamedPipeServerStream(
109+
context.NamedPipeEndPoint.PipeName,
110+
PipeDirection.InOut,
111+
NamedPipeServerStream.MaxAllowedServerInstances,
112+
PipeTransmissionMode.Byte,
113+
context.PipeOptions,
114+
inBufferSize: 0,
115+
outBufferSize: 0);
116+
}
117+
}
118+
63119
internal Func<MemoryPool<byte>> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create;
64120
}
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Globalization;
45
using System.IO.Pipes;
56
using System.Net;
67
using System.Net.Http;
@@ -14,8 +15,8 @@
1415
using Microsoft.AspNetCore.Hosting;
1516
using Microsoft.AspNetCore.Http;
1617
using Microsoft.AspNetCore.Internal;
17-
using Microsoft.AspNetCore.Server.Kestrel.Core;
1818
using Microsoft.AspNetCore.InternalTesting;
19+
using Microsoft.AspNetCore.Server.Kestrel.Core;
1920
using Microsoft.Extensions.DependencyInjection;
2021
using Microsoft.Extensions.Hosting;
2122
using Microsoft.Extensions.Logging;
@@ -268,6 +269,132 @@ public async Task ListenNamedPipeEndpoint_Impersonation_ClientSuccess()
268269
}
269270
}
270271

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

0 commit comments

Comments
 (0)