diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index a121986958..50fbf5a748 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -68,8 +68,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http protected HttpVersion _httpVersion; - private readonly string _pathBase; - private int _remainingRequestHeadersBytesAllowed; private int _requestHeadersParsed; @@ -88,7 +86,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http ServerOptions = context.ListenerContext.ServiceContext.ServerOptions; - _pathBase = context.ListenerContext.ListenOptions.PathBase; _parser = context.ListenerContext.ServiceContext.HttpParserFactory(this); FrameControl = this; @@ -1041,38 +1038,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return result; } - private bool RequestUrlStartsWithPathBase(string requestUrl, out bool caseMatches) - { - caseMatches = true; - - if (string.IsNullOrEmpty(_pathBase)) - { - return false; - } - - if (requestUrl.Length < _pathBase.Length || (requestUrl.Length > _pathBase.Length && requestUrl[_pathBase.Length] != '/')) - { - return false; - } - - 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 bool TakeMessageHeaders(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) { // Make sure the buffer is limited @@ -1321,7 +1286,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http QueryString = query.GetAsciiStringNonNullCharacters(); RawTarget = rawTarget; - SetNormalizedPath(requestUrlPath); + Path = PathNormalizer.RemoveDotSegments(requestUrlPath); } private void OnAuthorityFormTarget(HttpMethod method, Span target) @@ -1356,7 +1321,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http // See https://tools.ietf.org/html/rfc3986#section-3.2 RawTarget = target.GetAsciiStringNonNullCharacters(); Path = string.Empty; - PathBase = string.Empty; QueryString = string.Empty; } @@ -1371,7 +1335,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http RawTarget = Asterisk; Path = string.Empty; - PathBase = string.Empty; QueryString = string.Empty; } @@ -1397,25 +1360,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http RejectRequestTarget(target); } - SetNormalizedPath(uri.LocalPath); + Path = PathNormalizer.RemoveDotSegments(uri.LocalPath); // don't use uri.Query because we need the unescaped version QueryString = query.GetAsciiStringNonNullCharacters(); } - private void SetNormalizedPath(string requestPath) - { - var normalizedTarget = PathNormalizer.RemoveDotSegments(requestPath); - if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches)) - { - PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length); - Path = normalizedTarget.Substring(_pathBase.Length); - } - else - { - Path = normalizedTarget; - } - } - private unsafe static string GetUtf8String(Span path) { // .NET 451 doesn't have pointer overloads for Encoding.GetString so we diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs index a36da353f4..654544594b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Net; using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -163,12 +164,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel { var parsedAddress = ServerAddress.FromUrl(address); + if (!string.IsNullOrEmpty(parsedAddress.PathBase)) + { + _logger.LogWarning($"Path base in address {address} is not supported and will be ignored. To specify a path base, use {nameof(IApplicationBuilder)}.UsePathBase()."); + } + if (parsedAddress.IsUnixPipe) { listenOptions.Add(new ListenOptions(parsedAddress.UnixPipePath) { Scheme = parsedAddress.Scheme, - PathBase = parsedAddress.PathBase }); } else @@ -188,7 +193,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel listenOptions.Add(new ListenOptions(CreateIPEndPoint(parsedAddress)) { Scheme = parsedAddress.Scheme, - PathBase = parsedAddress.PathBase }); } } @@ -259,7 +263,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel var ipv4ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, parsedAddress.Port)) { Scheme = parsedAddress.Scheme, - PathBase = parsedAddress.PathBase }; _disposables.Push(engine.CreateServer(ipv4ListenOptions)); @@ -283,7 +286,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel var ipv6ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.IPv6Loopback, parsedAddress.Port)) { Scheme = parsedAddress.Scheme, - PathBase = parsedAddress.PathBase }; _disposables.Push(engine.CreateServer(ipv6ListenOptions)); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs index f1a458776e..45ebe7917b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/ListenOptions.cs @@ -81,9 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel /// public List ConnectionAdapters { get; } = new List(); - // PathBase and Scheme are hopefully only a temporary measure for back compat with IServerAddressesFeature. - // This allows a ListenOptions to describe all the information encoded in IWebHostBuilder.UseUrls. - internal string PathBase { get; set; } + // Scheme is hopefully only a temporary measure for back compat with IServerAddressesFeature. internal string Scheme { get; set; } = "http"; public override string ToString() @@ -93,12 +91,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel switch (Type) { case ListenType.IPEndPoint: - return $"{Scheme}://{IPEndPoint}{PathBase}"; + return $"{Scheme}://{IPEndPoint}"; case ListenType.SocketPath: - // ":" is used by ServerAddress to separate the socket path from PathBase. - return $"{Scheme}://unix:{SocketPath}:{PathBase}"; + return $"{Scheme}://unix:{SocketPath}"; case ListenType.FileHandle: - // This was never supported via --server.urls, so no need to include Scheme or PathBase. + // This was never supported via --server.urls, so no need to include Scheme. return "http://"; default: throw new InvalidOperationException(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs b/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs index 5030e81993..7836c0658e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs @@ -37,18 +37,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel { if (IsUnixPipe) { - if (string.IsNullOrEmpty(PathBase)) - { - return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant(); - } - else - { - return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + PathBase.ToLowerInvariant(); - } + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant(); } else { - return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToLowerInvariant(); + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture); } } @@ -66,8 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel } return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) - && Port == other.Port - && string.Equals(PathBase, other.PathBase, StringComparison.OrdinalIgnoreCase); + && Port == other.Port; } public static ServerAddress FromUrl(string url) @@ -145,7 +137,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel throw new FormatException($"Invalid URL: {url}"); } - // 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); @@ -157,16 +148,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel return serverAddress; } - - internal ServerAddress WithHost(string host) - { - return new ServerAddress - { - Scheme = Scheme, - Host = host, - Port = Port, - PathBase = PathBase - }; - } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/PathBaseTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/PathBaseTests.cs deleted file mode 100644 index 8c0edb6e22..0000000000 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/PathBaseTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// 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; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - public class PathBaseTests - { - [Theory] - [InlineData("/base", "/base", "/base", "")] - [InlineData("/base", "/base/", "/base", "/")] - [InlineData("/base", "/base/something", "/base", "/something")] - [InlineData("/base", "/base/something/", "/base", "/something/")] - [InlineData("/base/more", "/base/more", "/base/more", "")] - [InlineData("/base/more", "/base/more/something", "/base/more", "/something")] - [InlineData("/base/more", "/base/more/something/", "/base/more", "/something/")] - public Task RequestPathBaseIsServerPathBase(string registerPathBase, string requestPath, string expectedPathBase, string expectedPath) - { - return TestPathBase(registerPathBase, requestPath, expectedPathBase, expectedPath); - } - - [Theory] - [InlineData("", "/", "", "/")] - [InlineData("", "/something", "", "/something")] - [InlineData("/", "/", "", "/")] - [InlineData("/base", "/", "", "/")] - [InlineData("/base", "/something", "", "/something")] - [InlineData("/base", "/baseandsomething", "", "/baseandsomething")] - [InlineData("/base", "/ba", "", "/ba")] - [InlineData("/base", "/ba/se", "", "/ba/se")] - public Task DefaultPathBaseIsEmpty(string registerPathBase, string requestPath, string expectedPathBase, string expectedPath) - { - return TestPathBase(registerPathBase, requestPath, expectedPathBase, expectedPath); - } - - [Theory] - [InlineData("", "/", "", "/")] - [InlineData("/", "/", "", "/")] - [InlineData("/base", "/base/", "/base", "/")] - [InlineData("/base/", "/base", "/base", "")] - [InlineData("/base/", "/base/", "/base", "/")] - public Task PathBaseNeverEndsWithSlash(string registerPathBase, string requestPath, string expectedPathBase, string expectedPath) - { - return TestPathBase(registerPathBase, requestPath, expectedPathBase, expectedPath); - } - - [Fact] - public Task PathBaseAndPathPreserveRequestCasing() - { - return TestPathBase("/base", "/Base/Something", "/Base", "/Something"); - } - - [Fact] - public Task PathBaseCanHaveUTF8Characters() - { - return TestPathBase("/b♫se", "/b♫se/something", "/b♫se", "/something"); - } - - private async Task TestPathBase(string registerPathBase, string requestPath, string expectedPathBase, string expectedPath) - { - var builder = new WebHostBuilder() - .UseKestrel() - .UseUrls($"http://127.0.0.1:0{registerPathBase}") - .Configure(app => - { - app.Run(async context => - { - await context.Response.WriteAsync(JsonConvert.SerializeObject(new - { - PathBase = context.Request.PathBase.Value, - Path = context.Request.Path.Value - })); - }); - }); - - using (var host = builder.Build()) - { - host.Start(); - - using (var client = new HttpClient()) - { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}{requestPath}"); - 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.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index 4e5afb14df..9a4ae90d6e 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Net; using System.Net.Http; using System.Net.Sockets; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -18,8 +19,11 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -520,6 +524,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + [Theory] + [InlineData("/base", "/base")] + [InlineData("/base", "/base/")] + [InlineData("/base", "/base/something")] + [InlineData("/base", "/base/something/")] + [InlineData("/base/something", "/base/something")] + [InlineData("/base/something", "/base/something/")] + [InlineData("/base/something", "/base/something/more")] + [InlineData("/base/something", "/base/something/more/")] + public async Task DoesNotSplitPathBase(string registerPathBase, string requestPath) + { + var testLogger = new TestApplicationErrorLogger(); + + string contextPathBase = null; + string contextPath = null; + + var builder = new WebHostBuilder() + .UseKestrel() + .UseUrls($"http://127.0.0.1:0{registerPathBase}") + .ConfigureServices(services => + { + services.AddSingleton(new KestrelTestLoggerFactory(testLogger)); + }) + .Configure(app => + { + app.Run(context => + { + contextPathBase = context.Request.PathBase; + contextPath = context.Request.Path; + + return TaskCache.CompletedTask; + }); + }); + + using (var host = builder.Build()) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.Send($"GET {requestPath} HTTP/1.1\r\n\r\n"); + await connection.Receive("HTTP/1.1 200 OK"); + } + + Assert.Single(testLogger.Messages, log => + log.LogLevel == LogLevel.Warning && + string.Equals( + $"Path base in address http://127.0.0.1:0{registerPathBase} is not supported and will be ignored. To specify a path base, use {nameof(IApplicationBuilder)}.UsePathBase().", + log.Message, + StringComparison.Ordinal)); + } + + Assert.Equal("", contextPathBase); + Assert.Equal(requestPath, contextPath); + + + } + private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress) { var builder = new WebHostBuilder() diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs index 2da9a8144c..d60253405b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/RequestTargetProcessingTests.cs @@ -19,18 +19,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public async Task RequestPathIsNotNormalized() { var testContext = new TestServiceContext(); - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - PathBase = "/\u0041\u030A" - }; + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); using (var server = new TestServer(async context => { - Assert.Equal("/\u0041\u030A", context.Request.PathBase.Value); - Assert.Equal("/B/\u0041\u030A", context.Request.Path.Value); + Assert.Equal("/\u0041\u030A/B/\u0041\u030A", context.Request.Path.Value); - context.Response.Headers["Content-Length"] = new[] { "11" }; + context.Response.Headers.ContentLength = 11; await context.Response.WriteAsync("Hello World"); }, testContext, listenOptions)) { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ServerAddressTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ServerAddressTests.cs index c8920d384b..340ea1db27 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ServerAddressTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ServerAddressTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text; using Microsoft.AspNetCore.Server.Kestrel; using Xunit; @@ -41,21 +40,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] - [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] + [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80")] [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "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", "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", null)] + [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")] + [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80")] + [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000")] [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", null)] + [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 = ServerAddress.FromUrl(url); @@ -67,24 +66,5 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(toString ?? url, serverAddress.ToString()); } - - [Fact] - public void PathBaseIsNotNormalized() - { - var serverAddres = ServerAddress.FromUrl("http://localhost:8080/p\u0041\u030Athbase"); - - Assert.False(serverAddres.PathBase.IsNormalized(NormalizationForm.FormC)); - Assert.Equal("/p\u0041\u030Athbase", serverAddres.PathBase); - } - - [Fact] - public void WithHostReturnsNewInstanceWithDifferentHost() - { - var serverAddress = ServerAddress.FromUrl("http://localhost:8080"); - var newAddress = serverAddress.WithHost("otherhost"); - Assert.NotSame(serverAddress, newAddress); - Assert.Equal("otherhost", newAddress.Host); - Assert.Equal("localhost", serverAddress.Host); - } } }