From d5911f9918cba5ba3ba9751a8d2b10ad90f00399 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 13 Nov 2022 16:40:43 +0800 Subject: Support named pipe from urls argument --- src/Http/Http/src/BindingAddress.cs | 52 +++++++++++++++++++--- src/Http/Http/src/PublicAPI.Unshipped.txt | 2 + .../src/NamedPipeEndPoint.cs | 3 +- .../Kestrel/Core/src/Internal/AddressBinder.cs | 4 ++ src/Servers/Kestrel/Core/src/ListenOptions.cs | 4 +- .../Kestrel/Core/test/AddressBinderTests.cs | 25 +++++++++++ .../Transport.NamedPipes/test/WebHostTests.cs | 51 +++++++++++++++++++++ 7 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/Http/Http/src/BindingAddress.cs b/src/Http/Http/src/BindingAddress.cs index 398489c84d..6e6e12b46a 100644 --- a/src/Http/Http/src/BindingAddress.cs +++ b/src/Http/Http/src/BindingAddress.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Http; public class BindingAddress { private const string UnixPipeHostPrefix = "unix:/"; + private const string NamedPipeHostPrefix = "pipe:"; private BindingAddress(string host, string pathBase, int port, string scheme) { @@ -57,6 +58,14 @@ public class BindingAddress /// public bool IsUnixPipe => Host.StartsWith(UnixPipeHostPrefix, StringComparison.Ordinal); + /// + /// Gets a value that determines if this instance represents a named pipe. + /// + /// Returns if starts with pipe: prefix. + /// + /// + public bool IsNamedPipe => Host.StartsWith(NamedPipeHostPrefix, StringComparison.Ordinal); + /// /// Gets the unix pipe path if this instance represents a Unix pipe. /// @@ -73,6 +82,22 @@ public class BindingAddress } } + /// + /// Gets the named pipe path if this instance represents a named pipe. + /// + public string NamedPipePath + { + get + { + if (!IsNamedPipe) + { + throw new InvalidOperationException("Binding address is not a named pipe."); + } + + return GetNamedPipePath(Host); + } + } + private static string GetUnixPipePath(string host) { var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; @@ -84,10 +109,12 @@ public class BindingAddress return host.Substring(unixPipeHostPrefixLength); } + private static string GetNamedPipePath(string host) => host.Substring(NamedPipeHostPrefix.Length); + /// public override string ToString() { - if (IsUnixPipe) + if (IsUnixPipe || IsNamedPipe) { return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant(); } @@ -135,15 +162,11 @@ public class BindingAddress var schemeDelimiterEnd = schemeDelimiterStart + Uri.SchemeDelimiter.Length; var isUnixPipe = address.IndexOf(UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + var isNamedPipe = address.IndexOf(NamedPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; int pathDelimiterStart; int pathDelimiterEnd; - if (!isUnixPipe) - { - pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); - pathDelimiterEnd = pathDelimiterStart; - } - else + if (isUnixPipe) { var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; if (OperatingSystem.IsWindows()) @@ -159,6 +182,16 @@ public class BindingAddress pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + unixPipeHostPrefixLength, StringComparison.Ordinal); pathDelimiterEnd = pathDelimiterStart + ":".Length; } + else if (isNamedPipe) + { + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + NamedPipeHostPrefix.Length, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart + ":".Length; + } + else + { + pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart; + } if (pathDelimiterStart < 0) { @@ -215,6 +248,11 @@ public class BindingAddress throw new FormatException($"Invalid url, unix socket path must be absolute: '{address}'"); } + if (isNamedPipe && GetNamedPipePath(host).Contains('\\')) + { + throw new FormatException($"Invalid url, pipe name must not contain backslashes: '{address}'"); + } + string pathBase; if (address[address.Length - 1] == '/') { diff --git a/src/Http/Http/src/PublicAPI.Unshipped.txt b/src/Http/Http/src/PublicAPI.Unshipped.txt index a158cc48ca..82a37c76f8 100644 --- a/src/Http/Http/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature! priorFeature) -> void +Microsoft.AspNetCore.Http.BindingAddress.IsNamedPipe.get -> bool +Microsoft.AspNetCore.Http.BindingAddress.NamedPipePath.get -> string! Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature? priorFeature) -> void diff --git a/src/Servers/Connections.Abstractions/src/NamedPipeEndPoint.cs b/src/Servers/Connections.Abstractions/src/NamedPipeEndPoint.cs index 31ef3cffc0..52d2d803e8 100644 --- a/src/Servers/Connections.Abstractions/src/NamedPipeEndPoint.cs +++ b/src/Servers/Connections.Abstractions/src/NamedPipeEndPoint.cs @@ -47,7 +47,8 @@ public sealed class NamedPipeEndPoint : EndPoint /// public override string ToString() { - return $"pipe:{ServerName}/{PipeName}"; + // Based on format at https://learn.microsoft.com/windows/win32/ipc/pipe-names + return $@"\\{ServerName}\pipe\{PipeName}"; } /// diff --git a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs index e8f8bbee90..448f845821 100644 --- a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs +++ b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs @@ -120,6 +120,10 @@ internal sealed class AddressBinder { options = new ListenOptions(parsedAddress.UnixPipePath); } + else if (parsedAddress.IsNamedPipe) + { + options = new ListenOptions(new NamedPipeEndPoint(parsedAddress.NamedPipePath)); + } else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) { // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index 2a028a4954..d3b023e489 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -72,7 +72,7 @@ public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder /// /// Only set if the is bound to a . /// - public string? PipeName => (EndPoint as NamedPipeEndPoint)?.ToString(); + public string? PipeName => (EndPoint as NamedPipeEndPoint)?.PipeName.ToString(); /// /// Gets the bound file descriptor to a socket. @@ -137,6 +137,8 @@ public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder { case UnixDomainSocketEndPoint _: return $"{Scheme}://unix:{EndPoint}"; + case NamedPipeEndPoint namedPipeEndPoint: + return $"{Scheme}://pipe:{namedPipeEndPoint.PipeName}"; case FileHandleEndPoint _: return $"{Scheme}://"; default: diff --git a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index c63d207f8a..0a4bfb51e0 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -78,6 +78,31 @@ public class AddressBinderTests Assert.False(https); } + [Fact] + public void ParseAddressNamedPipe() + { + var listenOptions = AddressBinder.ParseAddress("http://pipe:HelloWorld", out var https); + Assert.IsType(listenOptions.EndPoint); + Assert.Equal("HelloWorld", listenOptions.PipeName); + Assert.False(https); + } + + [Fact] + public void ParseAddressNamedPipe_ForwardSlashes() + { + var listenOptions = AddressBinder.ParseAddress("http://pipe:/tmp/kestrel-test.sock", out var https); + Assert.IsType(listenOptions.EndPoint); + Assert.Equal("/tmp/kestrel-test.sock", listenOptions.PipeName); + Assert.False(https); + } + + [Fact] + public void ParseAddressNamedPipe_ErrorFromBackslash() + { + var ex = Assert.Throws(() => AddressBinder.ParseAddress(@"http://pipe:this\is\invalid", out var https)); + Assert.Equal(@"Invalid url, pipe name must not contain backslashes: 'http://pipe:this\is\invalid'", ex.Message); + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, SkipReason = "tmp/kestrel-test.sock is not valid for windows. Unix socket path must be absolute.")] public void ParseAddressUnixPipe() diff --git a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs index c4ee759c79..5f330673ae 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs @@ -281,6 +281,57 @@ public class WebHostTests : LoggedTest } } + [Fact] + public async Task ListenNamedPipeEndpoint_FromUrl_HelloWorld_ClientSuccess() + { + // Arrange + using var httpEventSource = new HttpEventSourceListener(LoggerFactory); + var pipeName = NamedPipeTestHelpers.GetUniquePipeName(); + var url = $"http://pipe:{pipeName}"; + + var builder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseUrls(url) + .UseKestrel() + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("hello, world"); + }); + }); + }) + .ConfigureServices(AddTestLogging); + + using (var host = builder.Build()) + using (var client = CreateClient(pipeName)) + { + await host.StartAsync().DefaultTimeout(); + + 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); + + await host.StopAsync().DefaultTimeout(); + } + + var listeningOn = TestSink.Writes.Single(m => m.EventId.Name == "ListeningOnAddress"); + Assert.Equal($"Now listening on: {url}", listeningOn.Message); + } + private static HttpClient CreateClient(string pipeName, TokenImpersonationLevel? impersonationLevel = null) { var httpHandler = new SocketsHttpHandler -- cgit v1.2.3