diff --git a/src/Http/Http/src/BindingAddress.cs b/src/Http/Http/src/BindingAddress.cs index 67a62e0c42..38e552b381 100644 --- a/src/Http/Http/src/BindingAddress.cs +++ b/src/Http/Http/src/BindingAddress.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Http { @@ -32,10 +34,17 @@ namespace Microsoft.AspNetCore.Http throw new InvalidOperationException("Binding address is not a unix pipe."); } - return Host.Substring(UnixPipeHostPrefix.Length - 1); + var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // "/" character in unix refers to root. Windows has drive letters and volume separator (c:) + unixPipeHostPrefixLength--; + } + return Host.Substring(unixPipeHostPrefixLength); } } + public override string ToString() { if (IsUnixPipe) @@ -88,7 +97,18 @@ namespace Microsoft.AspNetCore.Http } else { - pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + UnixPipeHostPrefix.Length, StringComparison.Ordinal); + var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Windows has drive letters and volume separator (c:) + unixPipeHostPrefixLength += 2; + if (schemeDelimiterEnd + unixPipeHostPrefixLength > address.Length) + { + throw new FormatException($"Invalid url: '{address}'"); + } + } + + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + unixPipeHostPrefixLength, StringComparison.Ordinal); pathDelimiterEnd = pathDelimiterStart + ":".Length; } @@ -141,6 +161,11 @@ namespace Microsoft.AspNetCore.Http throw new FormatException($"Invalid url: '{address}'"); } + if (isUnixPipe && !Path.IsPathRooted(serverAddress.UnixPipePath)) + { + throw new FormatException($"Invalid url, unix socket path must be absolute: '{address}'"); + } + if (address[address.Length - 1] == '/') { serverAddress.PathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); diff --git a/src/Http/Http/test/BindingAddressTests.cs b/src/Http/Http/test/BindingAddressTests.cs index 3e56f5a993..858aafeefb 100644 --- a/src/Http/Http/test/BindingAddressTests.cs +++ b/src/Http/Http/test/BindingAddressTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.AspNetCore.Http.Tests { @@ -32,6 +34,16 @@ namespace Microsoft.AspNetCore.Http.Tests Assert.Throws(() => BindingAddress.Parse(url)); } + [ConditionalTheory] + [InlineData("http://unix:/")] + [InlineData("http://unix:/c")] + [InlineData("http://unix:/wrong.path")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] + public void FromUriThrowsForUrlsWithWrongFilePathOnWindows(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } + [Theory] [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] [InlineData("http://+", "http", "+", 80, "", "http://+:80")] @@ -50,11 +62,6 @@ namespace Microsoft.AspNetCore.Http.Tests [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] - [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] - [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] - [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] - [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] - [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) { var serverAddress = BindingAddress.Parse(url); @@ -66,5 +73,43 @@ namespace Microsoft.AspNetCore.Http.Tests Assert.Equal(toString ?? url, serverAddress.ToString()); } + + [ConditionalTheory] + [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] + [OSSkipCondition(OperatingSystems.Windows)] + public void UnixSocketUrlsAreParsedCorrectlyOnUnix(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); + + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + + Assert.Equal(toString ?? url, serverAddress.ToString()); + } + + [ConditionalTheory] + [InlineData("http://unix:/c:/foo/bar/pipe.socket", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", null)] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:/", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:5000/doesn't/matter", "http", "unix:/c:/foo/bar/pipe.socket", 0, "5000/doesn't/matter", "http://unix:/c:/foo/bar/pipe.socket")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] + public void UnixSocketUrlsAreParsedCorrectlyOnWindows(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); + + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + + Assert.Equal(toString ?? url, serverAddress.ToString()); + } + } } diff --git a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index b5305f5c1d..d4aaee32b7 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_RS4)] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "tmp/kestrel-test.sock is not valid for windows. Unix socket path must be absolute.")] public void ParseAddressUnixPipe() { var listenOptions = AddressBinder.ParseAddress("http://unix:/tmp/kestrel-test.sock", out var https); @@ -86,6 +86,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.False(https); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_RS4)] + public void ParseAddressUnixPipeOnWindows() + { + var listenOptions = AddressBinder.ParseAddress(@"http://unix:/c:/foo/bar/pipe.socket", out var https); + Assert.IsType(listenOptions.EndPoint); + Assert.Equal("c:/foo/bar/pipe.socket", listenOptions.SocketPath); + Assert.False(https); + } + [Theory] [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000, false)] [InlineData("http://[::1]:5000", "::1", 5000, false)] diff --git a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs index 12fe59d9e8..a6a49d8b3b 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs @@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -96,7 +98,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var read = 0; while (read < data.Length) { - read += await socket.ReceiveAsync(buffer.AsMemory(read, buffer.Length - read), SocketFlags.None).DefaultTimeout(); + var bytesReceived = await socket.ReceiveAsync(buffer.AsMemory(read, buffer.Length - read), SocketFlags.None).DefaultTimeout(); + read += bytesReceived; + if (bytesReceived <= 0) break; } Assert.Equal(data, buffer); @@ -114,6 +118,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } +#if LIBUV + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "Libuv does not support unix domain sockets on Windows.")] +#else + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_RS4)] +#endif + [ConditionalFact] + [CollectDump] + public async Task TestUnixDomainSocketWithUrl() + { + var path = Path.GetTempFileName(); + var url = $"http://unix:/{path}"; + + Delete(path); + + try + { + var hostBuilder = TransportSelector.GetWebHostBuilder() + .UseUrls(url) + .UseKestrel() + .ConfigureServices(AddTestLogging) + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello World"); + }); + }); + + using (var host = hostBuilder.Build()) + { + await host.StartAsync().DefaultTimeout(); + + // https://github.com/dotnet/corefx/issues/5999 + // .NET Core HttpClient does not support unix sockets, it's difficult to parse raw response data. below is a little hacky way. + using (var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified)) + { + await socket.ConnectAsync(new UnixDomainSocketEndPoint(path)).DefaultTimeout(); + + var httpRequest = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\nConnection: close\r\n\r\n"); + await socket.SendAsync(httpRequest, SocketFlags.None).DefaultTimeout(); + + var readBuffer = new byte[512]; + var read = 0; + while (true) + { + var bytesReceived = await socket.ReceiveAsync(readBuffer.AsMemory(read), SocketFlags.None).DefaultTimeout(); + read += bytesReceived; + if (bytesReceived <= 0) break; + } + + var httpResponse = Encoding.ASCII.GetString(readBuffer, 0, read); + int httpStatusStart = httpResponse.IndexOf(' ') + 1; + int httpStatusEnd = httpResponse.IndexOf(' ', httpStatusStart); + + var httpStatus = int.Parse(httpResponse.Substring(httpStatusStart, httpStatusEnd - httpStatusStart)); + Assert.Equal(httpStatus, StatusCodes.Status200OK); + + } + await host.StopAsync().DefaultTimeout(); + } + } + finally + { + Delete(path); + } + } + private static void Delete(string path) { try