Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

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

Merged
merged 1 commit into from
May 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ string IHttpRequestFeature.QueryString
}
}

string IHttpRequestFeature.RawTarget
{
get
{
return RawTarget;
}
set
{
RawTarget = value;
}
}

IHeaderDictionary IHttpRequestFeature.Headers
{
get
Expand Down
48 changes: 40 additions & 8 deletions src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public Frame(ConnectionContext context)
public string PathBase { get; set; }
public string Path { get; set; }
public string QueryString { get; set; }
public string RawTarget { get; set; }
public string HttpVersion
{
get
Expand Down Expand Up @@ -860,6 +861,8 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
queryString = begin.GetAsciiString(scan);
}

var queryEnd = scan;

if (pathBegin.Peek() == ' ')
{
RejectRequest("Missing request target.");
Expand Down Expand Up @@ -907,8 +910,12 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
// 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);
Expand All @@ -918,27 +925,42 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
{
// 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;
Expand Down Expand Up @@ -978,6 +1000,16 @@ private bool RequestUrlStartsWithPathBase(string requestUrl, out bool caseMatche
{
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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,50 +156,6 @@ public async Task DoesNotHangOnConnectionCloseRequest()
}
}

[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()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

@halter73 halter73 May 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would these test cases pass with WebListenter after you add RawTarget there? I mainly wondering whether Path, PathBase and QueryString would all be an empty string.

Should this be added to ServerTests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebListener actually rejects those requests. Maybe we should do the same in Kestrel? The only request I was able to make to a target not starting with / was OPTIONS *.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, WebListener turned * into /* in Path.

{
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");
}
}
}
}
}