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
|
IHeaderDictionary IHttpRequestFeature.Headers
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
public string PathBase { get; set; }
|
public string PathBase { get; set; }
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public string QueryString { get; set; }
|
public string QueryString { get; set; }
|
||||||
|
public string RawTarget { get; set; }
|
||||||
public string HttpVersion
|
public string HttpVersion
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -860,6 +861,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
queryString = begin.GetAsciiString(scan);
|
queryString = begin.GetAsciiString(scan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queryEnd = scan;
|
||||||
|
|
||||||
if (pathBegin.Peek() == ' ')
|
if (pathBegin.Peek() == ' ')
|
||||||
{
|
{
|
||||||
RejectRequest("Missing request target.");
|
RejectRequest("Missing request target.");
|
||||||
|
|
@ -907,8 +910,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
|
// 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"
|
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
|
||||||
string requestUrlPath;
|
string requestUrlPath;
|
||||||
|
string rawTarget;
|
||||||
if (needDecode)
|
if (needDecode)
|
||||||
{
|
{
|
||||||
|
// Read raw target before mutating memory.
|
||||||
|
rawTarget = pathBegin.GetAsciiString(queryEnd);
|
||||||
|
|
||||||
// URI was encoded, unescape and then parse as utf8
|
// URI was encoded, unescape and then parse as utf8
|
||||||
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
|
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
|
||||||
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
|
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
|
||||||
|
|
@ -918,27 +925,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
{
|
{
|
||||||
// URI wasn't encoded, parse as ASCII
|
// URI wasn't encoded, parse as ASCII
|
||||||
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
|
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;
|
consumed = scan;
|
||||||
Method = method;
|
Method = method;
|
||||||
QueryString = queryString;
|
QueryString = queryString;
|
||||||
|
RawTarget = rawTarget;
|
||||||
HttpVersion = httpVersion;
|
HttpVersion = httpVersion;
|
||||||
|
|
||||||
bool caseMatches;
|
bool caseMatches;
|
||||||
|
if (RequestUrlStartsWithPathBase(normalizedTarget, out 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);
|
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
|
||||||
Path = requestUrlPath.Substring(_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
|
else
|
||||||
{
|
{
|
||||||
Path = requestUrlPath;
|
Path = string.Empty;
|
||||||
|
PathBase = string.Empty;
|
||||||
|
QueryString = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RequestLineStatus.Done;
|
return RequestLineStatus.Done;
|
||||||
|
|
@ -978,6 +1000,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
{
|
{
|
||||||
caseMatches = true;
|
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++)
|
for (var i = 0; i < _pathBase.Length; i++)
|
||||||
{
|
{
|
||||||
if (requestUrl[i] != _pathBase[i])
|
if (requestUrl[i] != _pathBase[i])
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
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)
|
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
|
||||||
{
|
{
|
||||||
var builder = new WebHostBuilder()
|
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