Skip to content

Commit

Permalink
Add CreateNamedPipeServerStream to named pipes options (#56567)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Jul 9, 2024
1 parent 1dd3549 commit 3105342
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides information about an endpoint when creating a <see cref="NamedPipeServerStream"/>.
/// </summary>
public sealed class CreateNamedPipeServerStreamContext
{
/// <summary>
/// Gets the endpoint.
/// </summary>
public required NamedPipeEndPoint NamedPipeEndPoint { get; init; }
/// <summary>
/// Gets the pipe options.
/// </summary>
public required PipeOptions PipeOptions { get; init; }
/// <summary>
/// Gets the default access control and audit security.
/// </summary>
public PipeSecurity? PipeSecurity { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ public NamedPipeServerStreamPoolPolicy(NamedPipeEndPoint endpoint, NamedPipeTran

public NamedPipeServerStream Create()
{
NamedPipeServerStream stream;
var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough;
if (!_hasFirstPipeStarted)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,65 @@ public sealed class NamedPipeTransportOptions
public bool CurrentUserOnly { get; set; } = true;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// Defaults to <c>null</c>, which is no pipe security.
/// </para>
/// <para>
/// Configuring <see cref="PipeSecurity"/> sets the default access control and audit security for pipes.
/// If per-endpoint security is needed then <see cref="CreateNamedPipeServerStream"/> can be configured
/// to create streams with different security settings.</para>
/// </remarks>
public PipeSecurity? PipeSecurity { get; set; }

/// <summary>
/// A function used to create a new <see cref="NamedPipeServerStream"/> to listen with. If
/// not set, <see cref="CreateDefaultNamedPipeServerStream" /> is used.
/// </summary>
/// <remarks>
/// Defaults to <see cref="CreateDefaultNamedPipeServerStream"/>.
/// </remarks>
public Func<CreateNamedPipeServerStreamContext, NamedPipeServerStream> CreateNamedPipeServerStream { get; set; } = CreateDefaultNamedPipeServerStream;

/// <summary>
/// Creates a default instance of <see cref="NamedPipeServerStream"/> for the given
/// <see cref="CreateNamedPipeServerStreamContext"/> that can be used by a connection listener
/// to listen for inbound requests.
/// </summary>
/// <param name="context">A <see cref="CreateNamedPipeServerStreamContext"/>.</param>
/// <returns>
/// A <see cref="NamedPipeServerStream"/> instance.
/// </returns>
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<MemoryPool<byte>> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create;
}
Original file line number Diff line number Diff line change
@@ -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.CreateNamedPipeServerStreamContext!, System.IO.Pipes.NamedPipeServerStream!>!
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!
129 changes: 128 additions & 1 deletion src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<IConnectionNamedPipeFeature>().NamedPipe;

var security = namedPipeStream.GetAccessControl();
var rules = security.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier));

context.Response.Headers.Add("X-PipeAccessRights", ((int)rules.OfType<PipeAccessRule>().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)]
Expand Down

0 comments on commit 3105342

Please sign in to comment.