Support PathBase (#214).

This commit is contained in:
Cesar Blum Silveira 2015-11-19 21:19:03 -08:00
parent 8b310c4583
commit 921c338a40
9 changed files with 202 additions and 21 deletions

View File

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

View File

@ -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<KeyValuePair<Type, object>> MaybeExtra;
@ -118,12 +117,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
get
{
return _pathBase ?? "";
return PathBase ?? "";
}
set
{
_pathBase = value;
PathBase = value;
}
}

View File

@ -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<IFeatureCollection> _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();

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, StringValues> 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);

View File

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