Remove splitting of path and path base (#1050).

This commit is contained in:
Cesar Blum Silveira 2017-03-12 20:24:22 -07:00
parent d4c0d4b81e
commit 632780dd16
8 changed files with 85 additions and 224 deletions

View File

@ -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<byte> 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<byte> path)
{
// .NET 451 doesn't have pointer overloads for Encoding.GetString so we

View File

@ -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));

View File

@ -81,9 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// </remarks>
public List<IConnectionAdapter> ConnectionAdapters { get; } = new List<IConnectionAdapter>();
// 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://<file handle>";
default:
throw new InvalidOperationException();

View File

@ -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
};
}
}
}

View File

@ -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<JObject>(responseText);
Assert.Equal(expectedPathBase, pathFacts["PathBase"].Value<string>());
Assert.Equal(expectedPath, pathFacts["Path"].Value<string>());
}
}
}
}
}

View File

@ -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<ILoggerFactory>(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()

View File

@ -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))
{

View File

@ -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);
}
}
}