Implement IHttpRequestFeature.RawTarget (aspnet/HttpAbstractions#596).
This commit is contained in:
parent
290e1e3f3f
commit
50208a3a79
|
|
@ -152,6 +152,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
}
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.RawTarget
|
||||
{
|
||||
get
|
||||
{
|
||||
return RawTarget;
|
||||
}
|
||||
set
|
||||
{
|
||||
RawTarget = value;
|
||||
}
|
||||
}
|
||||
|
||||
IHeaderDictionary IHttpRequestFeature.Headers
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue