diff options
author | James Newton-King <james@newtonking.com> | 2022-11-13 11:40:43 +0300 |
---|---|---|
committer | James Newton-King <james@newtonking.com> | 2022-11-13 11:40:43 +0300 |
commit | d5911f9918cba5ba3ba9751a8d2b10ad90f00399 (patch) | |
tree | 672f7b050f784153d305322db03904ca8353426e | |
parent | 2803ff75e0c3695ed786f26da306aadcc409e74f (diff) |
Support named pipe from urls argumentjamesnk/namedpipes-transport
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) { @@ -58,6 +59,14 @@ public class BindingAddress public bool IsUnixPipe => Host.StartsWith(UnixPipeHostPrefix, StringComparison.Ordinal); /// <summary> + /// Gets a value that determines if this instance represents a named pipe. + /// <para> + /// Returns <see langword="true"/> if <see cref="Host"/> starts with <c>pipe:</c> prefix. + /// </para> + /// </summary> + public bool IsNamedPipe => Host.StartsWith(NamedPipeHostPrefix, StringComparison.Ordinal); + + /// <summary> /// Gets the unix pipe path if this instance represents a Unix pipe. /// </summary> public string UnixPipePath @@ -73,6 +82,22 @@ public class BindingAddress } } + /// <summary> + /// Gets the named pipe path if this instance represents a named pipe. + /// </summary> + 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); + /// <inheritdoc /> 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 /// </summary> 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}"; } /// <inheritdoc/> 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 /// <remarks> /// Only set if the <see cref="ListenOptions"/> is bound to a <see cref="NamedPipeEndPoint"/>. /// </remarks> - public string? PipeName => (EndPoint as NamedPipeEndPoint)?.ToString(); + public string? PipeName => (EndPoint as NamedPipeEndPoint)?.PipeName.ToString(); /// <summary> /// 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}://<file handle>"; 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<NamedPipeEndPoint>(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<NamedPipeEndPoint>(listenOptions.EndPoint); + Assert.Equal("/tmp/kestrel-test.sock", listenOptions.PipeName); + Assert.False(https); + } + + [Fact] + public void ParseAddressNamedPipe_ErrorFromBackslash() + { + var ex = Assert.Throws<FormatException>(() => 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 |