Implement IHttpRequestFeature.RawTarget (aspnet/HttpAbstractions#596).

This commit is contained in:
Cesar Blum Silveira 2016-05-25 15:55:07 -07:00
parent 290e1e3f3f
commit 50208a3a79
5 changed files with 183 additions and 89 deletions

View File

@ -152,6 +152,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
}
}
string IHttpRequestFeature.RawTarget
{
get
{
return RawTarget;
}
set
{
RawTarget = value;
}
}
IHeaderDictionary IHttpRequestFeature.Headers
{
get

View File

@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public string PathBase { get; set; }
public string Path { get; set; }
public string QueryString { get; set; }
public string RawTarget { get; set; }
public string HttpVersion
{
get
@ -860,6 +861,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
queryString = begin.GetAsciiString(scan);
}
var queryEnd = scan;
if (pathBegin.Peek() == ' ')
{
RejectRequest("Missing request target.");
@ -907,8 +910,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
string requestUrlPath;
string rawTarget;
if (needDecode)
{
// Read raw target before mutating memory.
rawTarget = pathBegin.GetAsciiString(queryEnd);
// URI was encoded, unescape and then parse as utf8
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
@ -918,27 +925,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{
// URI wasn't encoded, parse as ASCII
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
if (queryString.Length == 0)
{
// No need to allocate an extra string if the path didn't need
// decoding and there's no query string following it.
rawTarget = requestUrlPath;
}
else
{
rawTarget = pathBegin.GetAsciiString(queryEnd);
}
}
requestUrlPath = PathNormalizer.RemoveDotSegments(requestUrlPath);
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
consumed = scan;
Method = method;
QueryString = queryString;
RawTarget = rawTarget;
HttpVersion = httpVersion;
bool caseMatches;
if (!string.IsNullOrEmpty(_pathBase) &&
(requestUrlPath.Length == _pathBase.Length || (requestUrlPath.Length > _pathBase.Length && requestUrlPath[_pathBase.Length] == '/')) &&
RequestUrlStartsWithPathBase(requestUrlPath, out caseMatches))
if (RequestUrlStartsWithPathBase(normalizedTarget, out caseMatches))
{
PathBase = caseMatches ? _pathBase : requestUrlPath.Substring(0, _pathBase.Length);
Path = requestUrlPath.Substring(_pathBase.Length);
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
Path = normalizedTarget.Substring(_pathBase.Length);
}
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
{
Path = normalizedTarget;
}
else
{
Path = requestUrlPath;
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
}
return RequestLineStatus.Done;
@ -978,6 +1000,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{
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])

View File

@ -4,8 +4,6 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -158,50 +156,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
[Fact]
public void RequestPathIsNormalized()
{
var builder = new WebHostBuilder()
.UseKestrel()
.UseUrls($"http://127.0.0.1:0/\u0041\u030A")
.Configure(app =>
{
app.Run(async context =>
{
var connection = context.Connection;
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
await context.Response.WriteAsync("hello, world");
});
});
using (var host = builder.Build())
{
host.Start();
using (var socket = TestConnection.CreateConnectedLoopbackSocket(host.GetPort()))
{
socket.Send(Encoding.ASCII.GetBytes("GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.1\r\n\r\n"));
socket.Shutdown(SocketShutdown.Send);
var response = new StringBuilder();
var buffer = new byte[4096];
while (true)
{
var length = socket.Receive(buffer);
if (length == 0)
{
break;
}
response.Append(Encoding.ASCII.GetString(buffer, 0, length));
}
Assert.StartsWith("HTTP/1.1 200 OK", response.ToString());
}
}
}
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
{
var builder = new WebHostBuilder()

View File

@ -1,35 +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;
using System.Net.Sockets;
using Microsoft.AspNetCore.Server.Kestrel.Networking;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class TestConnection
{
public static Socket CreateConnectedLoopbackSocket(int port)
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
if (PlatformApis.IsWindows)
{
const int SIO_LOOPBACK_FAST_PATH = -1744830448;
var optionInValue = BitConverter.GetBytes(1);
try
{
socket.IOControl(SIO_LOOPBACK_FAST_PATH, optionInValue, null);
}
catch
{
// If the operating system version on this machine did
// not support SIO_LOOPBACK_FAST_PATH (i.e. version
// prior to Windows 8 / Windows Server 2012), handle the exception
}
}
socket.Connect(new IPEndPoint(IPAddress.Loopback, port));
return socket;
}
}
}

View File

@ -0,0 +1,131 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class RequestTargetProcessingTests
{
[Fact]
public async Task RequestPathIsNormalized()
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async context =>
{
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
context.Response.Headers["Content-Length"] = new[] { "11" };
await context.Response.WriteAsync("Hello World");
}, testContext, "http://127.0.0.1/\u0041\u030A"))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
"GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.0",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 11",
"",
"Hello World");
}
}
}
[Theory]
[InlineData("/")]
[InlineData("/.")]
[InlineData("/..")]
[InlineData("/./.")]
[InlineData("/./..")]
[InlineData("/../.")]
[InlineData("/../..")]
[InlineData("/path")]
[InlineData("/path?foo=1&bar=2")]
[InlineData("/hello%20world")]
[InlineData("/hello%20world?foo=1&bar=2")]
[InlineData("/base/path")]
[InlineData("/base/path?foo=1&bar=2")]
[InlineData("/base/hello%20world")]
[InlineData("/base/hello%20world?foo=1&bar=2")]
public async Task RequestFeatureContainsRawTarget(string requestTarget)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async context =>
{
Assert.Equal(requestTarget, context.Features.Get<IHttpRequestFeature>().RawTarget);
context.Response.Headers["Content-Length"] = new[] { "11" };
await context.Response.WriteAsync("Hello World");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
$"GET {requestTarget} HTTP/1.0",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 11",
"",
"Hello World");
}
}
}
[Theory]
[InlineData("*")]
[InlineData("*/?arg=value")]
[InlineData("*?arg=value")]
[InlineData("DoesNotStartWith/")]
[InlineData("DoesNotStartWith/?arg=value")]
[InlineData("DoesNotStartWithSlash?arg=value")]
[InlineData("./")]
[InlineData("../")]
[InlineData("../.")]
[InlineData(".././")]
[InlineData("../..")]
[InlineData("../../")]
public async Task NonPathRequestTargetSetInRawTarget(string requestTarget)
{
var testContext = new TestServiceContext();
using (var server = new TestServer(async context =>
{
Assert.Equal(requestTarget, context.Features.Get<IHttpRequestFeature>().RawTarget);
Assert.Empty(context.Request.Path.Value);
Assert.Empty(context.Request.PathBase.Value);
Assert.Empty(context.Request.QueryString.Value);
context.Response.Headers["Content-Length"] = new[] { "11" };
await context.Response.WriteAsync("Hello World");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
$"GET {requestTarget} HTTP/1.0",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 11",
"",
"Hello World");
}
}
}
}
}