Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Oct 13, 2022
1 parent 5d8d0c7 commit 141777e
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 71 deletions.
2 changes: 2 additions & 0 deletions src/Framework/test/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ static TestData()
"Microsoft.AspNetCore.Server.Kestrel",
"Microsoft.AspNetCore.Server.Kestrel.Core",
"Microsoft.AspNetCore.Server.Kestrel.Transport.Quic",
"Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes",
"Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets",
"Microsoft.AspNetCore.Session",
"Microsoft.AspNetCore.SignalR",
Expand Down Expand Up @@ -228,6 +229,7 @@ static TestData()
{ "Microsoft.AspNetCore.Server.Kestrel.Core" },
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" },
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" },
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" },
{ "Microsoft.AspNetCore.Server.Kestrel" },
{ "Microsoft.AspNetCore.Session" },
{ "Microsoft.AspNetCore.SignalR.Common" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.IO.Pipelines;
using System.IO.Pipes;
using System.Net;
using System.Threading.Channels;
using Microsoft.AspNetCore.Connections;
using Microsoft.Extensions.Logging;
using NamedPipeOptions = System.IO.Pipes.PipeOptions;
using PipeOptions = System.IO.Pipelines.PipeOptions;

namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal;
Expand All @@ -20,21 +22,24 @@ internal sealed class NamedPipeConnectionListener : IConnectionListener
private readonly CancellationTokenSource _listeningTokenSource = new CancellationTokenSource();
private readonly CancellationToken _listeningToken;
private readonly Channel<ConnectionContext> _acceptedQueue;
private readonly Task _listeningTask;
private readonly MemoryPool<byte> _memoryPool;
private readonly PipeOptions _inputOptions;
private readonly PipeOptions _outputOptions;
private readonly Mutex _mutex;
private Task? _listeningTask;
private int _disposed;

public NamedPipeConnectionListener(
NamedPipeEndPoint endpoint,
NamedPipeTransportOptions options,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
Mutex mutex)
{
_log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes");
_endpoint = endpoint;
_options = options;
_acceptedQueue = Channel.CreateBounded<ConnectionContext>(new BoundedChannelOptions(options.Backlog));
_mutex = mutex;
_acceptedQueue = Channel.CreateBounded<ConnectionContext>(new BoundedChannelOptions(options.Backlog) { SingleWriter = true });
_memoryPool = options.MemoryPoolFactory();
_listeningToken = _listeningTokenSource.Token;

Expand All @@ -43,50 +48,57 @@ public NamedPipeConnectionListener(

_inputOptions = new PipeOptions(_memoryPool, PipeScheduler.ThreadPool, PipeScheduler.Inline, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false);
_outputOptions = new PipeOptions(_memoryPool, PipeScheduler.Inline, PipeScheduler.ThreadPool, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false);
}

public void Start()
{
Debug.Assert(_listeningTask == null, "Already started");

// Start after all fields are initialized.
_listeningTask = StartAsync();
// Start first stream inline to catch creation errors.
var initialStream = CreateServerStream();

_listeningTask = StartAsync(initialStream);
}

public EndPoint EndPoint => _endpoint;

private async Task StartAsync()
private async Task StartAsync(NamedPipeServerStream nextStream)
{
try
{
while (true)
{
NamedPipeServerStream stream;

try
{
_listeningToken.ThrowIfCancellationRequested();

stream = NamedPipeServerStreamAcl.Create(
_endpoint.PipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
_endpoint.PipeOptions,
inBufferSize: 0, // Buffer in System.IO.Pipelines
outBufferSize: 0, // Buffer in System.IO.Pipelines
_options.PipeSecurity);
var stream = nextStream;

await stream.WaitForConnectionAsync(_listeningToken);

var connection = new NamedPipeConnection(stream, _endpoint, _log, _memoryPool, _inputOptions, _outputOptions);
connection.Start();

// Create the next stream before writing connected stream to the channel.
// This ensures there is always a created stream and another process can't
// create a stream with the same name with different a access policy.
nextStream = CreateServerStream();

while (!_acceptedQueue.Writer.TryWrite(connection))
{
if (!await _acceptedQueue.Writer.WaitToWriteAsync(_listeningToken))
{
throw new InvalidOperationException("Accept queue writer was unexpectedly closed.");
}
}
}
catch (OperationCanceledException ex) when (_listeningToken.IsCancellationRequested)
{
// Cancelled the current token
NamedPipeLog.ConnectionListenerAborted(_log, ex);
break;
}

var connection = new NamedPipeConnection(stream, _endpoint, _log, _memoryPool, _inputOptions, _outputOptions);
connection.Start();

_acceptedQueue.Writer.TryWrite(connection);
}

nextStream.Dispose();
_acceptedQueue.Writer.TryComplete();
}
catch (Exception ex)
Expand All @@ -95,6 +107,41 @@ private async Task StartAsync()
}
}

private NamedPipeServerStream CreateServerStream()
{
NamedPipeServerStream stream;
var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough;
if (_options.CurrentUserOnly)
{
pipeOptions |= NamedPipeOptions.CurrentUserOnly;
}

if (_options.PipeSecurity != null)
{
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;
}

public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
{
while (await _acceptedQueue.Reader.WaitToReadAsync(cancellationToken))
Expand All @@ -109,6 +156,8 @@ private async Task StartAsync()
return null;
}

public ValueTask UnbindAsync(CancellationToken cancellationToken = default) => DisposeAsync();

public async ValueTask DisposeAsync()
{
// A stream may be waiting on WaitForConnectionAsync when dispose happens.
Expand All @@ -119,12 +168,10 @@ public async ValueTask DisposeAsync()
}

_listeningTokenSource.Dispose();
await _listeningTask;
}

public async ValueTask UnbindAsync(CancellationToken cancellationToken = default)
{
_listeningTokenSource.Cancel();
await _listeningTask;
_mutex.Dispose();
if (_listeningTask != null)
{
await _listeningTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,31 @@ public NamedPipeTransportFactory(

public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
{
if (endpoint is not NamedPipeEndPoint np)
ArgumentNullException.ThrowIfNull(endpoint);

if (endpoint is not NamedPipeEndPoint namedPipeEndPoint)
{
throw new NotSupportedException($"{endpoint.GetType()} is not supported.");
}
if (namedPipeEndPoint.ServerName != NamedPipeEndPoint.LocalComputerServerName)
{
throw new NotSupportedException($@"Server name '{namedPipeEndPoint.ServerName}' is invalid. The server name must be ""."".");
}

// Creating a named pipe server with an name isn't exclusive. Create a mutex with the pipe name to prevent multiple endpoints
// accidently sharing the same pipe name. Will detect across Kestrel processes.
// Note that this doesn't prevent other applications from using the pipe name.
var mutexName = "Kestrel-NamedPipe-" + namedPipeEndPoint.PipeName;
var mutex = new Mutex(false, mutexName, out var createdNew);
if (!createdNew)
{
mutex.Dispose();
throw new AddressInUseException($"Named pipe '{namedPipeEndPoint.PipeName}' is already in use by Kestrel.");
}

var listener = new NamedPipeConnectionListener(np, _options, _loggerFactory);
var listener = new NamedPipeConnectionListener(namedPipeEndPoint, _options, _loggerFactory, mutex);
listener.Start();

return new ValueTask<IConnectionListener>(listener);
}
}
28 changes: 18 additions & 10 deletions src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeEndPoint.cs
Original file line number Diff line number Diff line change
@@ -1,7 +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.IO.Pipes;
using System.Diagnostics.CodeAnalysis;
using System.Net;

namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
Expand All @@ -11,37 +11,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
/// </summary>
public sealed class NamedPipeEndPoint : EndPoint
{
internal const string LocalComputerServerName = ".";

/// <summary>
/// Initializes a new instance of the <see cref="NamedPipeEndPoint"/> class.
/// </summary>
/// <param name="pipeName">The name of the pipe.</param>
/// <param name="serverName">The name of the remote computer to connect to, or "." to specify the local computer.</param>
/// <param name="pipeOptions">One of the enumeration values that determines how to open or create the pipe.</param>
public NamedPipeEndPoint(string pipeName, string serverName = ".", PipeOptions pipeOptions = PipeOptions.Asynchronous)
public NamedPipeEndPoint(string pipeName, string serverName = LocalComputerServerName)
{
ServerName = serverName;
PipeName = pipeName;
PipeOptions = pipeOptions;
}

/// <summary>
/// Gets the name of the remote computer to connect to.
/// Gets the name of the remote computer. The server name must be ".", the local computer, when creating a server.
/// </summary>
public string ServerName { get; }
/// <summary>
/// Gets the name of the pipe.
/// </summary>
public string PipeName { get; }
/// <summary>
/// Gets the pipe options.
/// </summary>
public PipeOptions PipeOptions { get; set; }

/// <summary>
/// Gets the pipe name represented by this <see cref="NamedPipeEndPoint"/> instance.
/// </summary>
public override string ToString()
{
return PipeName;
return $"pipe:{ServerName}/{PipeName}";
}

/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is NamedPipeEndPoint other && other.ServerName == ServerName && other.PipeName == PipeName;
}

/// <inheritdoc/>
public override int GetHashCode()
{
return ServerName.GetHashCode() ^ PipeName.GetHashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ public sealed class NamedPipeTransportOptions
/// </remarks>
public long? MaxWriteBufferSize { get; set; } = 64 * 1024;

/// <summary>
/// Gets or sets a value that indicates that the pipe can only be connected to by a client created by
/// the same user account.
/// <para>
/// On Windows, a value of true verifies both the user account and elevation level.
/// </para>
/// </summary>
/// <remarks>
/// Defaults to true.
/// </remarks>
public bool CurrentUserOnly { get; set; } = true;

/// <summary>
/// Gets or sets the security information that determines the access control and audit security for pipes.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature
Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream!
Microsoft.AspNetCore.Hosting.WebHostBuilderNamedPipeExtensions
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName = ".", System.IO.Pipes.PipeOptions pipeOptions = System.IO.Pipes.PipeOptions.Asynchronous) -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName = ".") -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeName.get -> string!
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeOptions.get -> System.IO.Pipes.PipeOptions
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeOptions.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.ServerName.get -> string!
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.Backlog.get -> int
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.Backlog.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CurrentUserOnly.get -> bool
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CurrentUserOnly.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxReadBufferSize.get -> long?
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxReadBufferSize.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxWriteBufferSize.get -> long?
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxWriteBufferSize.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.NamedPipeTransportOptions() -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.PipeSecurity.get -> System.IO.Pipes.PipeSecurity?
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.PipeSecurity.set -> void
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.Equals(object? obj) -> bool
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.GetHashCode() -> int
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.ToString() -> string!
static Microsoft.AspNetCore.Hosting.WebHostBuilderNamedPipeExtensions.UseNamedPipes(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Microsoft.AspNetCore.Hosting.WebHostBuilderNamedPipeExtensions.UseNamedPipes(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action<Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions!>! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
Loading

0 comments on commit 141777e

Please sign in to comment.