Skip to content

Commit 141777e

Browse files
committed
Update
1 parent 5d8d0c7 commit 141777e

File tree

9 files changed

+243
-71
lines changed

9 files changed

+243
-71
lines changed

src/Framework/test/TestData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ static TestData()
9090
"Microsoft.AspNetCore.Server.Kestrel",
9191
"Microsoft.AspNetCore.Server.Kestrel.Core",
9292
"Microsoft.AspNetCore.Server.Kestrel.Transport.Quic",
93+
"Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes",
9394
"Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets",
9495
"Microsoft.AspNetCore.Session",
9596
"Microsoft.AspNetCore.SignalR",
@@ -228,6 +229,7 @@ static TestData()
228229
{ "Microsoft.AspNetCore.Server.Kestrel.Core" },
229230
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" },
230231
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" },
232+
{ "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" },
231233
{ "Microsoft.AspNetCore.Server.Kestrel" },
232234
{ "Microsoft.AspNetCore.Session" },
233235
{ "Microsoft.AspNetCore.SignalR.Common" },

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

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers;
5+
using System.Diagnostics;
56
using System.IO.Pipelines;
67
using System.IO.Pipes;
78
using System.Net;
89
using System.Threading.Channels;
910
using Microsoft.AspNetCore.Connections;
1011
using Microsoft.Extensions.Logging;
12+
using NamedPipeOptions = System.IO.Pipes.PipeOptions;
1113
using PipeOptions = System.IO.Pipelines.PipeOptions;
1214

1315
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal;
@@ -20,21 +22,24 @@ internal sealed class NamedPipeConnectionListener : IConnectionListener
2022
private readonly CancellationTokenSource _listeningTokenSource = new CancellationTokenSource();
2123
private readonly CancellationToken _listeningToken;
2224
private readonly Channel<ConnectionContext> _acceptedQueue;
23-
private readonly Task _listeningTask;
2425
private readonly MemoryPool<byte> _memoryPool;
2526
private readonly PipeOptions _inputOptions;
2627
private readonly PipeOptions _outputOptions;
28+
private readonly Mutex _mutex;
29+
private Task? _listeningTask;
2730
private int _disposed;
2831

2932
public NamedPipeConnectionListener(
3033
NamedPipeEndPoint endpoint,
3134
NamedPipeTransportOptions options,
32-
ILoggerFactory loggerFactory)
35+
ILoggerFactory loggerFactory,
36+
Mutex mutex)
3337
{
3438
_log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes");
3539
_endpoint = endpoint;
3640
_options = options;
37-
_acceptedQueue = Channel.CreateBounded<ConnectionContext>(new BoundedChannelOptions(options.Backlog));
41+
_mutex = mutex;
42+
_acceptedQueue = Channel.CreateBounded<ConnectionContext>(new BoundedChannelOptions(options.Backlog) { SingleWriter = true });
3843
_memoryPool = options.MemoryPoolFactory();
3944
_listeningToken = _listeningTokenSource.Token;
4045

@@ -43,50 +48,57 @@ public NamedPipeConnectionListener(
4348

4449
_inputOptions = new PipeOptions(_memoryPool, PipeScheduler.ThreadPool, PipeScheduler.Inline, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false);
4550
_outputOptions = new PipeOptions(_memoryPool, PipeScheduler.Inline, PipeScheduler.ThreadPool, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false);
51+
}
52+
53+
public void Start()
54+
{
55+
Debug.Assert(_listeningTask == null, "Already started");
4656

47-
// Start after all fields are initialized.
48-
_listeningTask = StartAsync();
57+
// Start first stream inline to catch creation errors.
58+
var initialStream = CreateServerStream();
59+
60+
_listeningTask = StartAsync(initialStream);
4961
}
5062

5163
public EndPoint EndPoint => _endpoint;
5264

53-
private async Task StartAsync()
65+
private async Task StartAsync(NamedPipeServerStream nextStream)
5466
{
5567
try
5668
{
5769
while (true)
5870
{
59-
NamedPipeServerStream stream;
60-
6171
try
6272
{
63-
_listeningToken.ThrowIfCancellationRequested();
64-
65-
stream = NamedPipeServerStreamAcl.Create(
66-
_endpoint.PipeName,
67-
PipeDirection.InOut,
68-
NamedPipeServerStream.MaxAllowedServerInstances,
69-
PipeTransmissionMode.Byte,
70-
_endpoint.PipeOptions,
71-
inBufferSize: 0, // Buffer in System.IO.Pipelines
72-
outBufferSize: 0, // Buffer in System.IO.Pipelines
73-
_options.PipeSecurity);
73+
var stream = nextStream;
7474

7575
await stream.WaitForConnectionAsync(_listeningToken);
76+
77+
var connection = new NamedPipeConnection(stream, _endpoint, _log, _memoryPool, _inputOptions, _outputOptions);
78+
connection.Start();
79+
80+
// Create the next stream before writing connected stream to the channel.
81+
// This ensures there is always a created stream and another process can't
82+
// create a stream with the same name with different a access policy.
83+
nextStream = CreateServerStream();
84+
85+
while (!_acceptedQueue.Writer.TryWrite(connection))
86+
{
87+
if (!await _acceptedQueue.Writer.WaitToWriteAsync(_listeningToken))
88+
{
89+
throw new InvalidOperationException("Accept queue writer was unexpectedly closed.");
90+
}
91+
}
7692
}
7793
catch (OperationCanceledException ex) when (_listeningToken.IsCancellationRequested)
7894
{
7995
// Cancelled the current token
8096
NamedPipeLog.ConnectionListenerAborted(_log, ex);
8197
break;
8298
}
83-
84-
var connection = new NamedPipeConnection(stream, _endpoint, _log, _memoryPool, _inputOptions, _outputOptions);
85-
connection.Start();
86-
87-
_acceptedQueue.Writer.TryWrite(connection);
8899
}
89100

101+
nextStream.Dispose();
90102
_acceptedQueue.Writer.TryComplete();
91103
}
92104
catch (Exception ex)
@@ -95,6 +107,41 @@ private async Task StartAsync()
95107
}
96108
}
97109

110+
private NamedPipeServerStream CreateServerStream()
111+
{
112+
NamedPipeServerStream stream;
113+
var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough;
114+
if (_options.CurrentUserOnly)
115+
{
116+
pipeOptions |= NamedPipeOptions.CurrentUserOnly;
117+
}
118+
119+
if (_options.PipeSecurity != null)
120+
{
121+
stream = NamedPipeServerStreamAcl.Create(
122+
_endpoint.PipeName,
123+
PipeDirection.InOut,
124+
NamedPipeServerStream.MaxAllowedServerInstances,
125+
PipeTransmissionMode.Byte,
126+
pipeOptions,
127+
inBufferSize: 0, // Buffer in System.IO.Pipelines
128+
outBufferSize: 0, // Buffer in System.IO.Pipelines
129+
_options.PipeSecurity);
130+
}
131+
else
132+
{
133+
stream = new NamedPipeServerStream(
134+
_endpoint.PipeName,
135+
PipeDirection.InOut,
136+
NamedPipeServerStream.MaxAllowedServerInstances,
137+
PipeTransmissionMode.Byte,
138+
pipeOptions,
139+
inBufferSize: 0,
140+
outBufferSize: 0);
141+
}
142+
return stream;
143+
}
144+
98145
public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
99146
{
100147
while (await _acceptedQueue.Reader.WaitToReadAsync(cancellationToken))
@@ -109,6 +156,8 @@ private async Task StartAsync()
109156
return null;
110157
}
111158

159+
public ValueTask UnbindAsync(CancellationToken cancellationToken = default) => DisposeAsync();
160+
112161
public async ValueTask DisposeAsync()
113162
{
114163
// A stream may be waiting on WaitForConnectionAsync when dispose happens.
@@ -119,12 +168,10 @@ public async ValueTask DisposeAsync()
119168
}
120169

121170
_listeningTokenSource.Dispose();
122-
await _listeningTask;
123-
}
124-
125-
public async ValueTask UnbindAsync(CancellationToken cancellationToken = default)
126-
{
127-
_listeningTokenSource.Cancel();
128-
await _listeningTask;
171+
_mutex.Dispose();
172+
if (_listeningTask != null)
173+
{
174+
await _listeningTask;
175+
}
129176
}
130177
}

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,31 @@ public NamedPipeTransportFactory(
2525

2626
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
2727
{
28-
if (endpoint is not NamedPipeEndPoint np)
28+
ArgumentNullException.ThrowIfNull(endpoint);
29+
30+
if (endpoint is not NamedPipeEndPoint namedPipeEndPoint)
2931
{
3032
throw new NotSupportedException($"{endpoint.GetType()} is not supported.");
3133
}
34+
if (namedPipeEndPoint.ServerName != NamedPipeEndPoint.LocalComputerServerName)
35+
{
36+
throw new NotSupportedException($@"Server name '{namedPipeEndPoint.ServerName}' is invalid. The server name must be ""."".");
37+
}
38+
39+
// Creating a named pipe server with an name isn't exclusive. Create a mutex with the pipe name to prevent multiple endpoints
40+
// accidently sharing the same pipe name. Will detect across Kestrel processes.
41+
// Note that this doesn't prevent other applications from using the pipe name.
42+
var mutexName = "Kestrel-NamedPipe-" + namedPipeEndPoint.PipeName;
43+
var mutex = new Mutex(false, mutexName, out var createdNew);
44+
if (!createdNew)
45+
{
46+
mutex.Dispose();
47+
throw new AddressInUseException($"Named pipe '{namedPipeEndPoint.PipeName}' is already in use by Kestrel.");
48+
}
3249

33-
var listener = new NamedPipeConnectionListener(np, _options, _loggerFactory);
50+
var listener = new NamedPipeConnectionListener(namedPipeEndPoint, _options, _loggerFactory, mutex);
51+
listener.Start();
52+
3453
return new ValueTask<IConnectionListener>(listener);
3554
}
3655
}
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +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.IO.Pipes;
4+
using System.Diagnostics.CodeAnalysis;
55
using System.Net;
66

77
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
@@ -11,37 +11,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
1111
/// </summary>
1212
public sealed class NamedPipeEndPoint : EndPoint
1313
{
14+
internal const string LocalComputerServerName = ".";
15+
1416
/// <summary>
1517
/// Initializes a new instance of the <see cref="NamedPipeEndPoint"/> class.
1618
/// </summary>
1719
/// <param name="pipeName">The name of the pipe.</param>
1820
/// <param name="serverName">The name of the remote computer to connect to, or "." to specify the local computer.</param>
19-
/// <param name="pipeOptions">One of the enumeration values that determines how to open or create the pipe.</param>
20-
public NamedPipeEndPoint(string pipeName, string serverName = ".", PipeOptions pipeOptions = PipeOptions.Asynchronous)
21+
public NamedPipeEndPoint(string pipeName, string serverName = LocalComputerServerName)
2122
{
2223
ServerName = serverName;
2324
PipeName = pipeName;
24-
PipeOptions = pipeOptions;
2525
}
2626

2727
/// <summary>
28-
/// Gets the name of the remote computer to connect to.
28+
/// Gets the name of the remote computer. The server name must be ".", the local computer, when creating a server.
2929
/// </summary>
3030
public string ServerName { get; }
3131
/// <summary>
3232
/// Gets the name of the pipe.
3333
/// </summary>
3434
public string PipeName { get; }
35-
/// <summary>
36-
/// Gets the pipe options.
37-
/// </summary>
38-
public PipeOptions PipeOptions { get; set; }
3935

4036
/// <summary>
4137
/// Gets the pipe name represented by this <see cref="NamedPipeEndPoint"/> instance.
4238
/// </summary>
4339
public override string ToString()
4440
{
45-
return PipeName;
41+
return $"pipe:{ServerName}/{PipeName}";
42+
}
43+
44+
/// <inheritdoc/>
45+
public override bool Equals([NotNullWhen(true)] object? obj)
46+
{
47+
return obj is NamedPipeEndPoint other && other.ServerName == ServerName && other.PipeName == PipeName;
48+
}
49+
50+
/// <inheritdoc/>
51+
public override int GetHashCode()
52+
{
53+
return ServerName.GetHashCode() ^ PipeName.GetHashCode();
4654
}
4755
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ public sealed class NamedPipeTransportOptions
4343
/// </remarks>
4444
public long? MaxWriteBufferSize { get; set; } = 64 * 1024;
4545

46+
/// <summary>
47+
/// Gets or sets a value that indicates that the pipe can only be connected to by a client created by
48+
/// the same user account.
49+
/// <para>
50+
/// On Windows, a value of true verifies both the user account and elevation level.
51+
/// </para>
52+
/// </summary>
53+
/// <remarks>
54+
/// Defaults to true.
55+
/// </remarks>
56+
public bool CurrentUserOnly { get; set; } = true;
57+
4658
/// <summary>
4759
/// Gets or sets the security information that determines the access control and audit security for pipes.
4860
/// </summary>

src/Servers/Kestrel/Transport.NamedPipes/src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature
33
Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream!
44
Microsoft.AspNetCore.Hosting.WebHostBuilderNamedPipeExtensions
55
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint
6-
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName = ".", System.IO.Pipes.PipeOptions pipeOptions = System.IO.Pipes.PipeOptions.Asynchronous) -> void
6+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName = ".") -> void
77
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeName.get -> string!
8-
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeOptions.get -> System.IO.Pipes.PipeOptions
9-
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.PipeOptions.set -> void
108
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.ServerName.get -> string!
119
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions
1210
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.Backlog.get -> int
1311
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.Backlog.set -> void
12+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CurrentUserOnly.get -> bool
13+
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.CurrentUserOnly.set -> void
1414
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxReadBufferSize.get -> long?
1515
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxReadBufferSize.set -> void
1616
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxWriteBufferSize.get -> long?
1717
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.MaxWriteBufferSize.set -> void
1818
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.NamedPipeTransportOptions() -> void
1919
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.PipeSecurity.get -> System.IO.Pipes.PipeSecurity?
2020
Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeTransportOptions.PipeSecurity.set -> void
21+
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.Equals(object? obj) -> bool
22+
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.GetHashCode() -> int
2123
override Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.NamedPipeEndPoint.ToString() -> string!
2224
static Microsoft.AspNetCore.Hosting.WebHostBuilderNamedPipeExtensions.UseNamedPipes(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
2325
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!

0 commit comments

Comments
 (0)