Skip to content

Commit

Permalink
Support named pipe from urls argument
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Nov 13, 2022
1 parent 2803ff7 commit d5911f9
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 9 deletions.
52 changes: 45 additions & 7 deletions src/Http/Http/src/BindingAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -57,6 +58,14 @@ public BindingAddress()
/// </summary>
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>
Expand All @@ -73,6 +82,22 @@ public string UnixPipePath
}
}

/// <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;
Expand All @@ -84,10 +109,12 @@ private static string GetUnixPipePath(string host)
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();
}
Expand Down Expand Up @@ -135,15 +162,11 @@ public static BindingAddress Parse(string address)
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())
Expand All @@ -159,6 +182,16 @@ public static BindingAddress Parse(string address)
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)
{
Expand Down Expand Up @@ -215,6 +248,11 @@ public static BindingAddress Parse(string address)
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] == '/')
{
Expand Down
2 changes: 2 additions & 0 deletions src/Http/Http/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public NamedPipeEndPoint(string pipeName, string serverName)
/// </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/>
Expand Down
4 changes: 4 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ internal static ListenOptions ParseAddress(string address, out bool https)
{
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.
Expand Down
4 changes: 3 additions & 1 deletion src/Servers/Kestrel/Core/src/ListenOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ internal ListenOptions(ulong fileHandle, FileHandleType handleType)
/// <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.
Expand Down Expand Up @@ -137,6 +137,8 @@ internal virtual string GetDisplayName()
{
case UnixDomainSocketEndPoint _:
return $"{Scheme}://unix:{EndPoint}";
case NamedPipeEndPoint namedPipeEndPoint:
return $"{Scheme}://pipe:{namedPipeEndPoint.PipeName}";
case FileHandleEndPoint _:
return $"{Scheme}://<file handle>";
default:
Expand Down
25 changes: 25 additions & 0 deletions src/Servers/Kestrel/Core/test/AddressBinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,31 @@ public void ParseAddressLocalhost()
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()
Expand Down
51 changes: 51 additions & 0 deletions src/Servers/Kestrel/Transport.NamedPipes/test/WebHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,57 @@ public async Task ListenNamedPipeEndpoint_Tls_ClientSuccess(HttpProtocols protoc
}
}

[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
Expand Down

0 comments on commit d5911f9

Please sign in to comment.