Skip to content

Commit d5911f9

Browse files
committed
Support named pipe from urls argument
1 parent 2803ff7 commit d5911f9

File tree

7 files changed

+132
-9
lines changed

7 files changed

+132
-9
lines changed

src/Http/Http/src/BindingAddress.cs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Http;
1111
public class BindingAddress
1212
{
1313
private const string UnixPipeHostPrefix = "unix:/";
14+
private const string NamedPipeHostPrefix = "pipe:";
1415

1516
private BindingAddress(string host, string pathBase, int port, string scheme)
1617
{
@@ -57,6 +58,14 @@ public BindingAddress()
5758
/// </summary>
5859
public bool IsUnixPipe => Host.StartsWith(UnixPipeHostPrefix, StringComparison.Ordinal);
5960

61+
/// <summary>
62+
/// Gets a value that determines if this instance represents a named pipe.
63+
/// <para>
64+
/// Returns <see langword="true"/> if <see cref="Host"/> starts with <c>pipe:</c> prefix.
65+
/// </para>
66+
/// </summary>
67+
public bool IsNamedPipe => Host.StartsWith(NamedPipeHostPrefix, StringComparison.Ordinal);
68+
6069
/// <summary>
6170
/// Gets the unix pipe path if this instance represents a Unix pipe.
6271
/// </summary>
@@ -73,6 +82,22 @@ public string UnixPipePath
7382
}
7483
}
7584

85+
/// <summary>
86+
/// Gets the named pipe path if this instance represents a named pipe.
87+
/// </summary>
88+
public string NamedPipePath
89+
{
90+
get
91+
{
92+
if (!IsNamedPipe)
93+
{
94+
throw new InvalidOperationException("Binding address is not a named pipe.");
95+
}
96+
97+
return GetNamedPipePath(Host);
98+
}
99+
}
100+
76101
private static string GetUnixPipePath(string host)
77102
{
78103
var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length;
@@ -84,10 +109,12 @@ private static string GetUnixPipePath(string host)
84109
return host.Substring(unixPipeHostPrefixLength);
85110
}
86111

112+
private static string GetNamedPipePath(string host) => host.Substring(NamedPipeHostPrefix.Length);
113+
87114
/// <inheritdoc />
88115
public override string ToString()
89116
{
90-
if (IsUnixPipe)
117+
if (IsUnixPipe || IsNamedPipe)
91118
{
92119
return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant();
93120
}
@@ -135,15 +162,11 @@ public static BindingAddress Parse(string address)
135162
var schemeDelimiterEnd = schemeDelimiterStart + Uri.SchemeDelimiter.Length;
136163

137164
var isUnixPipe = address.IndexOf(UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd;
165+
var isNamedPipe = address.IndexOf(NamedPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd;
138166

139167
int pathDelimiterStart;
140168
int pathDelimiterEnd;
141-
if (!isUnixPipe)
142-
{
143-
pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal);
144-
pathDelimiterEnd = pathDelimiterStart;
145-
}
146-
else
169+
if (isUnixPipe)
147170
{
148171
var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length;
149172
if (OperatingSystem.IsWindows())
@@ -159,6 +182,16 @@ public static BindingAddress Parse(string address)
159182
pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + unixPipeHostPrefixLength, StringComparison.Ordinal);
160183
pathDelimiterEnd = pathDelimiterStart + ":".Length;
161184
}
185+
else if (isNamedPipe)
186+
{
187+
pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + NamedPipeHostPrefix.Length, StringComparison.Ordinal);
188+
pathDelimiterEnd = pathDelimiterStart + ":".Length;
189+
}
190+
else
191+
{
192+
pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal);
193+
pathDelimiterEnd = pathDelimiterStart;
194+
}
162195

163196
if (pathDelimiterStart < 0)
164197
{
@@ -215,6 +248,11 @@ public static BindingAddress Parse(string address)
215248
throw new FormatException($"Invalid url, unix socket path must be absolute: '{address}'");
216249
}
217250

251+
if (isNamedPipe && GetNamedPipePath(host).Contains('\\'))
252+
{
253+
throw new FormatException($"Invalid url, pipe name must not contain backslashes: '{address}'");
254+
}
255+
218256
string pathBase;
219257
if (address[address.Length - 1] == '/')
220258
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
22
*REMOVED*Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature! priorFeature) -> void
3+
Microsoft.AspNetCore.Http.BindingAddress.IsNamedPipe.get -> bool
4+
Microsoft.AspNetCore.Http.BindingAddress.NamedPipePath.get -> string!
35
Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature? priorFeature) -> void

src/Servers/Connections.Abstractions/src/NamedPipeEndPoint.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public NamedPipeEndPoint(string pipeName, string serverName)
4747
/// </summary>
4848
public override string ToString()
4949
{
50-
return $"pipe:{ServerName}/{PipeName}";
50+
// Based on format at https://learn.microsoft.com/windows/win32/ipc/pipe-names
51+
return $@"\\{ServerName}\pipe\{PipeName}";
5152
}
5253

5354
/// <inheritdoc/>

src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ internal static ListenOptions ParseAddress(string address, out bool https)
120120
{
121121
options = new ListenOptions(parsedAddress.UnixPipePath);
122122
}
123+
else if (parsedAddress.IsNamedPipe)
124+
{
125+
options = new ListenOptions(new NamedPipeEndPoint(parsedAddress.NamedPipePath));
126+
}
123127
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
124128
{
125129
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.

src/Servers/Kestrel/Core/src/ListenOptions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal ListenOptions(ulong fileHandle, FileHandleType handleType)
7272
/// <remarks>
7373
/// Only set if the <see cref="ListenOptions"/> is bound to a <see cref="NamedPipeEndPoint"/>.
7474
/// </remarks>
75-
public string? PipeName => (EndPoint as NamedPipeEndPoint)?.ToString();
75+
public string? PipeName => (EndPoint as NamedPipeEndPoint)?.PipeName.ToString();
7676

7777
/// <summary>
7878
/// Gets the bound file descriptor to a socket.
@@ -137,6 +137,8 @@ internal virtual string GetDisplayName()
137137
{
138138
case UnixDomainSocketEndPoint _:
139139
return $"{Scheme}://unix:{EndPoint}";
140+
case NamedPipeEndPoint namedPipeEndPoint:
141+
return $"{Scheme}://pipe:{namedPipeEndPoint.PipeName}";
140142
case FileHandleEndPoint _:
141143
return $"{Scheme}://<file handle>";
142144
default:

src/Servers/Kestrel/Core/test/AddressBinderTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,31 @@ public void ParseAddressLocalhost()
7878
Assert.False(https);
7979
}
8080

81+
[Fact]
82+
public void ParseAddressNamedPipe()
83+
{
84+
var listenOptions = AddressBinder.ParseAddress("http://pipe:HelloWorld", out var https);
85+
Assert.IsType<NamedPipeEndPoint>(listenOptions.EndPoint);
86+
Assert.Equal("HelloWorld", listenOptions.PipeName);
87+
Assert.False(https);
88+
}
89+
90+
[Fact]
91+
public void ParseAddressNamedPipe_ForwardSlashes()
92+
{
93+
var listenOptions = AddressBinder.ParseAddress("http://pipe:/tmp/kestrel-test.sock", out var https);
94+
Assert.IsType<NamedPipeEndPoint>(listenOptions.EndPoint);
95+
Assert.Equal("/tmp/kestrel-test.sock", listenOptions.PipeName);
96+
Assert.False(https);
97+
}
98+
99+
[Fact]
100+
public void ParseAddressNamedPipe_ErrorFromBackslash()
101+
{
102+
var ex = Assert.Throws<FormatException>(() => AddressBinder.ParseAddress(@"http://pipe:this\is\invalid", out var https));
103+
Assert.Equal(@"Invalid url, pipe name must not contain backslashes: 'http://pipe:this\is\invalid'", ex.Message);
104+
}
105+
81106
[ConditionalFact]
82107
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "tmp/kestrel-test.sock is not valid for windows. Unix socket path must be absolute.")]
83108
public void ParseAddressUnixPipe()

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,57 @@ public async Task ListenNamedPipeEndpoint_Tls_ClientSuccess(HttpProtocols protoc
281281
}
282282
}
283283

284+
[Fact]
285+
public async Task ListenNamedPipeEndpoint_FromUrl_HelloWorld_ClientSuccess()
286+
{
287+
// Arrange
288+
using var httpEventSource = new HttpEventSourceListener(LoggerFactory);
289+
var pipeName = NamedPipeTestHelpers.GetUniquePipeName();
290+
var url = $"http://pipe:{pipeName}";
291+
292+
var builder = new HostBuilder()
293+
.ConfigureWebHost(webHostBuilder =>
294+
{
295+
webHostBuilder
296+
.UseUrls(url)
297+
.UseKestrel()
298+
.Configure(app =>
299+
{
300+
app.Run(async context =>
301+
{
302+
await context.Response.WriteAsync("hello, world");
303+
});
304+
});
305+
})
306+
.ConfigureServices(AddTestLogging);
307+
308+
using (var host = builder.Build())
309+
using (var client = CreateClient(pipeName))
310+
{
311+
await host.StartAsync().DefaultTimeout();
312+
313+
var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1/")
314+
{
315+
Version = HttpVersion.Version11,
316+
VersionPolicy = HttpVersionPolicy.RequestVersionExact
317+
};
318+
319+
// Act
320+
var response = await client.SendAsync(request).DefaultTimeout();
321+
322+
// Assert
323+
response.EnsureSuccessStatusCode();
324+
Assert.Equal(HttpVersion.Version11, response.Version);
325+
var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout();
326+
Assert.Equal("hello, world", responseText);
327+
328+
await host.StopAsync().DefaultTimeout();
329+
}
330+
331+
var listeningOn = TestSink.Writes.Single(m => m.EventId.Name == "ListeningOnAddress");
332+
Assert.Equal($"Now listening on: {url}", listeningOn.Message);
333+
}
334+
284335
private static HttpClient CreateClient(string pipeName, TokenImpersonationLevel? impersonationLevel = null)
285336
{
286337
var httpHandler = new SocketsHttpHandler

0 commit comments

Comments
 (0)