diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs index 97cca8e51e..a405f0be87 100644 --- a/samples/SampleApp/Startup.cs +++ b/samples/SampleApp/Startup.cs @@ -48,6 +48,10 @@ namespace SampleApp context.Request.PathBase, context.Request.Path, context.Request.QueryString); + Console.WriteLine($"Method: {context.Request.Method}"); + Console.WriteLine($"PathBase: {context.Request.PathBase}"); + Console.WriteLine($"Path: {context.Request.Path}"); + Console.WriteLine($"QueryString: {context.Request.QueryString}"); var connectionFeature = context.Connection; Console.WriteLine($"Peer: {connectionFeature.RemoteIpAddress?.ToString()} {connectionFeature.RemotePort}"); @@ -60,4 +64,4 @@ namespace SampleApp }); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.FeatureCollection.cs index f0406a7aa5..2de39f8aee 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.FeatureCollection.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.FeatureCollection.cs @@ -27,7 +27,6 @@ namespace Microsoft.AspNet.Server.Kestrel.Http // then the list of `implementedFeatures` in the generated code project MUST also be updated. // See also: tools/Microsoft.AspNet.Server.Kestrel.GeneratedCode/FrameFeatureCollection.cs - private string _pathBase; private int _featureRevision; private List> MaybeExtra; @@ -118,12 +117,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { get { - return _pathBase ?? ""; + return PathBase ?? ""; } set { - _pathBase = value; + PathBase = value; } } diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs index a738cb0c84..13b16b5634 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; -using Microsoft.AspNet.Server.Kestrel.Filter; using Microsoft.AspNet.Server.Kestrel.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -70,6 +69,8 @@ namespace Microsoft.AspNet.Server.Kestrel.Http private readonly IPEndPoint _remoteEndPoint; private readonly Action _prepareRequest; + private readonly string _pathBase; + public Frame(ConnectionContext context) : this(context, remoteEndPoint: null, localEndPoint: null, prepareRequest: null) { @@ -84,6 +85,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http _remoteEndPoint = remoteEndPoint; _localEndPoint = localEndPoint; _prepareRequest = prepareRequest; + _pathBase = context.ServerAddress.PathBase; FrameControl = this; Reset(); @@ -92,6 +94,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public string Scheme { get; set; } public string Method { get; set; } public string RequestUri { get; set; } + public string PathBase { get; set; } public string Path { get; set; } public string QueryString { get; set; } public string HttpVersion @@ -198,6 +201,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http Scheme = null; Method = null; RequestUri = null; + PathBase = null; Path = null; QueryString = null; _httpVersion = HttpVersionType.Unknown; @@ -809,7 +813,21 @@ namespace Microsoft.AspNet.Server.Kestrel.Http RequestUri = requestUrlPath; QueryString = queryString; HttpVersion = httpVersion; - Path = RequestUri; + + bool caseMatches; + + if (!string.IsNullOrEmpty(_pathBase) && + (requestUrlPath.Length == _pathBase.Length || (requestUrlPath.Length > _pathBase.Length && requestUrlPath[_pathBase.Length] == '/')) && + RequestUrlStartsWithPathBase(requestUrlPath, out caseMatches)) + { + PathBase = caseMatches ? _pathBase : requestUrlPath.Substring(0, _pathBase.Length); + Path = requestUrlPath.Substring(_pathBase.Length); + } + else + { + Path = requestUrlPath; + } + return true; } finally @@ -818,6 +836,28 @@ namespace Microsoft.AspNet.Server.Kestrel.Http } } + private bool RequestUrlStartsWithPathBase(string requestUrl, out bool caseMatches) + { + caseMatches = true; + + for (var i = 0; i < _pathBase.Length; i++) + { + if (requestUrl[i] != _pathBase[i]) + { + if (char.ToLowerInvariant(requestUrl[i]) == char.ToLowerInvariant(_pathBase[i])) + { + caseMatches = false; + } + else + { + return false; + } + } + } + + return true; + } + public static bool TakeMessageHeaders(SocketInput input, FrameRequestHeaders requestHeaders) { var scan = input.ConsumingStart(); diff --git a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extenstions.cs b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extensions.cs similarity index 99% rename from src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extenstions.cs rename to src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extensions.cs index d46eee028f..d1f83c3d6e 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extenstions.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2Extensions.cs @@ -6,7 +6,7 @@ using System.Text; namespace Microsoft.AspNet.Server.Kestrel.Infrastructure { - public static class MemoryPoolIterator2Extenstions + public static class MemoryPoolIterator2Extensions { private const int _maxStackAllocBytes = 16384; diff --git a/src/Microsoft.AspNet.Server.Kestrel/ServerAddress.cs b/src/Microsoft.AspNet.Server.Kestrel/ServerAddress.cs index 7f910ce959..1c2e2d00e6 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/ServerAddress.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/ServerAddress.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Server.Kestrel public class ServerAddress { public string Host { get; private set; } - public string Path { get; private set; } + public string PathBase { get; private set; } public int Port { get; private set; } public string Scheme { get; private set; } @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Server.Kestrel public override string ToString() { - return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + Path.ToLowerInvariant(); + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToLowerInvariant(); } public override int GetHashCode() @@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Server.Kestrel return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) && Port == other.Port - && string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase); + && string.Equals(PathBase, other.PathBase, StringComparison.OrdinalIgnoreCase); } public static ServerAddress FromUrl(string url) @@ -71,7 +71,7 @@ namespace Microsoft.AspNet.Server.Kestrel Scheme = "http", Host = "+", Port = port, - Path = "/" + PathBase = "/" }; } return null; @@ -137,7 +137,15 @@ namespace Microsoft.AspNet.Server.Kestrel serverAddress.Host = url.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); } - serverAddress.Path = url.Substring(pathDelimiterEnd); + // Path should not end with a / since it will be used as PathBase later + if (url[url.Length - 1] == '/') + { + serverAddress.PathBase = url.Substring(pathDelimiterEnd, url.Length - pathDelimiterEnd - 1); + } + else + { + serverAddress.PathBase = url.Substring(pathDelimiterEnd); + } return serverAddress; } diff --git a/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/PathBaseTests.cs b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/PathBaseTests.cs new file mode 100644 index 0000000000..95df0477ab --- /dev/null +++ b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/PathBaseTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Testing.xunit; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNet.Server.Kestrel.FunctionalTests +{ + public class PathBaseTests + { + [ConditionalTheory] + [InlineData("http://localhost:8791/base", "http://localhost:8791/base", "/base", "")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/base/", "/base", "/")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/base/something", "/base", "/something")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/base/something/", "/base", "/something/")] + [InlineData("http://localhost:8791/base/more", "http://localhost:8791/base/more", "/base/more", "")] + [InlineData("http://localhost:8791/base/more", "http://localhost:8791/base/more/something", "/base/more", "/something")] + [InlineData("http://localhost:8791/base/more", "http://localhost:8791/base/more/something/", "/base/more", "/something/")] + [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] + public Task RequestPathBaseIsServerPathBase(string registerAddress, string requestAddress, string expectedPathBase, string expectedPath) + { + return TestPathBase(registerAddress, requestAddress, expectedPathBase, expectedPath); + } + + [ConditionalTheory] + [InlineData("http://localhost:8791", "http://localhost:8791/", "", "/")] + [InlineData("http://localhost:8791", "http://localhost:8791/something", "", "/something")] + [InlineData("http://localhost:8791/", "http://localhost:8791/", "", "/")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/", "", "/")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/something", "", "/something")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/baseandsomething", "", "/baseandsomething")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/ba", "", "/ba")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/ba/se", "", "/ba/se")] + [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] + public Task DefaultPathBaseIsEmpty(string registerAddress, string requestAddress, string expectedPathBase, string expectedPath) + { + return TestPathBase(registerAddress, requestAddress, expectedPathBase, expectedPath); + } + + [ConditionalTheory] + [InlineData("http://localhost:8791", "http://localhost:8791/", "", "/")] + [InlineData("http://localhost:8791/", "http://localhost:8791/", "", "/")] + [InlineData("http://localhost:8791/base", "http://localhost:8791/base/", "/base", "/")] + [InlineData("http://localhost:8791/base/", "http://localhost:8791/base", "/base", "")] + [InlineData("http://localhost:8791/base/", "http://localhost:8791/base/", "/base", "/")] + [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] + public Task PathBaseNeverEndsWithSlash(string registerAddress, string requestAddress, string expectedPathBase, string expectedPath) + { + return TestPathBase(registerAddress, requestAddress, expectedPathBase, expectedPath); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] + public Task PathBaseAndPathPreserveRequestCasing() + { + return TestPathBase("http://localhost:8791/base", "http://localhost:8791/Base/Something", "/Base", "/Something"); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] + public Task PathBaseCanHaveUTF8Characters() + { + return TestPathBase("http://localhost:8791/b♫se", "http://localhost:8791/b♫se/something", "/b♫se", "/something"); + } + + private async Task TestPathBase(string registerAddress, string requestAddress, string expectedPathBase, string expectedPath) + { + var config = new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary { + { "server.urls", registerAddress } + }).Build(); + + var builder = new WebHostBuilder(config) + .UseServerFactory("Microsoft.AspNet.Server.Kestrel") + .UseStartup(app => + { + app.Run(async context => + { + await context.Response.WriteAsync(JsonConvert.SerializeObject(new + { + PathBase = context.Request.PathBase.Value, + Path = context.Request.Path.Value + })); + }); + }); + + using (var app = builder.Build().Start()) + { + using (var client = new HttpClient()) + { + var response = await client.GetAsync(requestAddress); + response.EnsureSuccessStatusCode(); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(responseText); + + var pathFacts = JsonConvert.DeserializeObject(responseText); + Assert.Equal(expectedPathBase, pathFacts["PathBase"].Value()); + Assert.Equal(expectedPath, pathFacts["Path"].Value()); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Server.KestrelTests/FrameFacts.cs b/test/Microsoft.AspNet.Server.KestrelTests/FrameFacts.cs index d76c015fae..9e3419797f 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/FrameFacts.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/FrameFacts.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Server.Kestrel; using Microsoft.AspNet.Server.Kestrel.Http; using Xunit; @@ -13,7 +14,12 @@ namespace Microsoft.AspNet.Server.KestrelTests public void ResetResetsScheme() { // Arrange - var frame = new Frame(new ConnectionContext() { DateHeaderValueManager = new DateHeaderValueManager() }); + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + }; + var frame = new Frame(connectionContext); frame.Scheme = "https"; // Act diff --git a/test/Microsoft.AspNet.Server.KestrelTests/FrameResponseHeadersTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/FrameResponseHeadersTests.cs index d0e129c5f3..8082e62ed7 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/FrameResponseHeadersTests.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/FrameResponseHeadersTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Server.Kestrel; using Microsoft.AspNet.Server.Kestrel.Http; using Microsoft.Extensions.Primitives; using Xunit; @@ -14,7 +15,12 @@ namespace Microsoft.AspNet.Server.KestrelTests [Fact] public void InitialDictionaryContainsServerAndDate() { - var frame = new Frame(new ConnectionContext { DateHeaderValueManager = new DateHeaderValueManager() }); + var connectionContext = new ConnectionContext + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + }; + var frame = new Frame(connectionContext); IDictionary headers = frame.ResponseHeaders; Assert.Equal(2, headers.Count); @@ -37,7 +43,12 @@ namespace Microsoft.AspNet.Server.KestrelTests [Fact] public void InitialEntriesCanBeCleared() { - var frame = new Frame(new ConnectionContext { DateHeaderValueManager = new DateHeaderValueManager() }); + var connectionContext = new ConnectionContext + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + }; + var frame = new Frame(connectionContext); Assert.True(frame.ResponseHeaders.Count > 0); diff --git a/test/Microsoft.AspNet.Server.KestrelTests/ServerAddressFacts.cs b/test/Microsoft.AspNet.Server.KestrelTests/ServerAddressFacts.cs index 5b280c5e68..1a77598a83 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/ServerAddressFacts.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/ServerAddressFacts.cs @@ -18,30 +18,30 @@ namespace Microsoft.AspNet.Server.KestrelTests [InlineData("http://localhost", "http", "localhost", 80, "")] [InlineData("http://www.example.com", "http", "www.example.com", 80, "")] [InlineData("https://www.example.com", "https", "www.example.com", 443, "")] - [InlineData("http://www.example.com/", "http", "www.example.com", 80, "/")] + [InlineData("http://www.example.com/", "http", "www.example.com", 80, "")] [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz")] [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "")] [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "")] - [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "/")] + [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "")] [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "")] [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "")] - [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "/")] + [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "")] [InlineData("http://foo:/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")] [InlineData("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, "")] [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "")] [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "")] - [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "/")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "")] [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter")] - public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string path) + public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase) { var serverAddress = ServerAddress.FromUrl(url); Assert.Equal(scheme, serverAddress.Scheme); Assert.Equal(host, serverAddress.Host); Assert.Equal(port, serverAddress.Port); - Assert.Equal(path, serverAddress.Path); + Assert.Equal(pathBase, serverAddress.PathBase); } } }