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

Commit 50208a3

Browse files
author
Cesar Blum Silveira
committed
Implement IHttpRequestFeature.RawTarget (aspnet/HttpAbstractions#596).
1 parent 290e1e3 commit 50208a3

File tree

5 files changed

+183
-89
lines changed

5 files changed

+183
-89
lines changed

src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.FeatureCollection.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ string IHttpRequestFeature.QueryString
152152
}
153153
}
154154

155+
string IHttpRequestFeature.RawTarget
156+
{
157+
get
158+
{
159+
return RawTarget;
160+
}
161+
set
162+
{
163+
RawTarget = value;
164+
}
165+
}
166+
155167
IHeaderDictionary IHttpRequestFeature.Headers
156168
{
157169
get

src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public Frame(ConnectionContext context)
8484
public string PathBase { get; set; }
8585
public string Path { get; set; }
8686
public string QueryString { get; set; }
87+
public string RawTarget { get; set; }
8788
public string HttpVersion
8889
{
8990
get
@@ -860,6 +861,8 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
860861
queryString = begin.GetAsciiString(scan);
861862
}
862863

864+
var queryEnd = scan;
865+
863866
if (pathBegin.Peek() == ' ')
864867
{
865868
RejectRequest("Missing request target.");
@@ -907,8 +910,12 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
907910
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
908911
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
909912
string requestUrlPath;
913+
string rawTarget;
910914
if (needDecode)
911915
{
916+
// Read raw target before mutating memory.
917+
rawTarget = pathBegin.GetAsciiString(queryEnd);
918+
912919
// URI was encoded, unescape and then parse as utf8
913920
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
914921
requestUrlPath = pathBegin.GetUtf8String(pathEnd);
@@ -918,27 +925,42 @@ protected RequestLineStatus TakeStartLine(SocketInput input)
918925
{
919926
// URI wasn't encoded, parse as ASCII
920927
requestUrlPath = pathBegin.GetAsciiString(pathEnd);
928+
929+
if (queryString.Length == 0)
930+
{
931+
// No need to allocate an extra string if the path didn't need
932+
// decoding and there's no query string following it.
933+
rawTarget = requestUrlPath;
934+
}
935+
else
936+
{
937+
rawTarget = pathBegin.GetAsciiString(queryEnd);
938+
}
921939
}
922940

923-
requestUrlPath = PathNormalizer.RemoveDotSegments(requestUrlPath);
941+
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
924942

925943
consumed = scan;
926944
Method = method;
927945
QueryString = queryString;
946+
RawTarget = rawTarget;
928947
HttpVersion = httpVersion;
929948

930949
bool caseMatches;
931-
932-
if (!string.IsNullOrEmpty(_pathBase) &&
933-
(requestUrlPath.Length == _pathBase.Length || (requestUrlPath.Length > _pathBase.Length && requestUrlPath[_pathBase.Length] == '/')) &&
934-
RequestUrlStartsWithPathBase(requestUrlPath, out caseMatches))
950+
if (RequestUrlStartsWithPathBase(normalizedTarget, out caseMatches))
935951
{
936-
PathBase = caseMatches ? _pathBase : requestUrlPath.Substring(0, _pathBase.Length);
937-
Path = requestUrlPath.Substring(_pathBase.Length);
952+
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
953+
Path = normalizedTarget.Substring(_pathBase.Length);
954+
}
955+
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
956+
{
957+
Path = normalizedTarget;
938958
}
939959
else
940960
{
941-
Path = requestUrlPath;
961+
Path = string.Empty;
962+
PathBase = string.Empty;
963+
QueryString = string.Empty;
942964
}
943965

944966
return RequestLineStatus.Done;
@@ -978,6 +1000,16 @@ private bool RequestUrlStartsWithPathBase(string requestUrl, out bool caseMatche
9781000
{
9791001
caseMatches = true;
9801002

1003+
if (string.IsNullOrEmpty(_pathBase))
1004+
{
1005+
return false;
1006+
}
1007+
1008+
if (requestUrl.Length < _pathBase.Length || (requestUrl.Length > _pathBase.Length && requestUrl[_pathBase.Length] != '/'))
1009+
{
1010+
return false;
1011+
}
1012+
9811013
for (var i = 0; i < _pathBase.Length; i++)
9821014
{
9831015
if (requestUrl[i] != _pathBase[i])

test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
using System;
55
using System.Globalization;
66
using System.Net.Http;
7-
using System.Net.Sockets;
8-
using System.Text;
97
using System.Threading.Tasks;
108
using Microsoft.AspNetCore.Builder;
119
using Microsoft.AspNetCore.Hosting;
@@ -158,50 +156,6 @@ public async Task DoesNotHangOnConnectionCloseRequest()
158156
}
159157
}
160158

161-
[Fact]
162-
public void RequestPathIsNormalized()
163-
{
164-
var builder = new WebHostBuilder()
165-
.UseKestrel()
166-
.UseUrls($"http://127.0.0.1:0/\u0041\u030A")
167-
.Configure(app =>
168-
{
169-
app.Run(async context =>
170-
{
171-
var connection = context.Connection;
172-
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
173-
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
174-
await context.Response.WriteAsync("hello, world");
175-
});
176-
});
177-
178-
using (var host = builder.Build())
179-
{
180-
host.Start();
181-
182-
using (var socket = TestConnection.CreateConnectedLoopbackSocket(host.GetPort()))
183-
{
184-
socket.Send(Encoding.ASCII.GetBytes("GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.1\r\n\r\n"));
185-
socket.Shutdown(SocketShutdown.Send);
186-
187-
var response = new StringBuilder();
188-
var buffer = new byte[4096];
189-
while (true)
190-
{
191-
var length = socket.Receive(buffer);
192-
if (length == 0)
193-
{
194-
break;
195-
}
196-
197-
response.Append(Encoding.ASCII.GetString(buffer, 0, length));
198-
}
199-
200-
Assert.StartsWith("HTTP/1.1 200 OK", response.ToString());
201-
}
202-
}
203-
}
204-
205159
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
206160
{
207161
var builder = new WebHostBuilder()

test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/TestConnection.cs

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Features;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Server.KestrelTests
10+
{
11+
public class RequestTargetProcessingTests
12+
{
13+
[Fact]
14+
public async Task RequestPathIsNormalized()
15+
{
16+
var testContext = new TestServiceContext();
17+
18+
using (var server = new TestServer(async context =>
19+
{
20+
Assert.Equal("/\u00C5", context.Request.PathBase.Value);
21+
Assert.Equal("/B/\u00C5", context.Request.Path.Value);
22+
23+
context.Response.Headers["Content-Length"] = new[] { "11" };
24+
await context.Response.WriteAsync("Hello World");
25+
}, testContext, "http://127.0.0.1/\u0041\u030A"))
26+
{
27+
using (var connection = server.CreateConnection())
28+
{
29+
await connection.SendEnd(
30+
"GET /%41%CC%8A/A/../B/%41%CC%8A HTTP/1.0",
31+
"",
32+
"");
33+
await connection.ReceiveEnd(
34+
"HTTP/1.1 200 OK",
35+
$"Date: {testContext.DateHeaderValue}",
36+
"Content-Length: 11",
37+
"",
38+
"Hello World");
39+
}
40+
}
41+
}
42+
43+
[Theory]
44+
[InlineData("/")]
45+
[InlineData("/.")]
46+
[InlineData("/..")]
47+
[InlineData("/./.")]
48+
[InlineData("/./..")]
49+
[InlineData("/../.")]
50+
[InlineData("/../..")]
51+
[InlineData("/path")]
52+
[InlineData("/path?foo=1&bar=2")]
53+
[InlineData("/hello%20world")]
54+
[InlineData("/hello%20world?foo=1&bar=2")]
55+
[InlineData("/base/path")]
56+
[InlineData("/base/path?foo=1&bar=2")]
57+
[InlineData("/base/hello%20world")]
58+
[InlineData("/base/hello%20world?foo=1&bar=2")]
59+
public async Task RequestFeatureContainsRawTarget(string requestTarget)
60+
{
61+
var testContext = new TestServiceContext();
62+
63+
using (var server = new TestServer(async context =>
64+
{
65+
Assert.Equal(requestTarget, context.Features.Get<IHttpRequestFeature>().RawTarget);
66+
67+
context.Response.Headers["Content-Length"] = new[] { "11" };
68+
await context.Response.WriteAsync("Hello World");
69+
}, testContext))
70+
{
71+
using (var connection = server.CreateConnection())
72+
{
73+
await connection.SendEnd(
74+
$"GET {requestTarget} HTTP/1.0",
75+
"",
76+
"");
77+
await connection.ReceiveEnd(
78+
"HTTP/1.1 200 OK",
79+
$"Date: {testContext.DateHeaderValue}",
80+
"Content-Length: 11",
81+
"",
82+
"Hello World");
83+
}
84+
}
85+
}
86+
87+
[Theory]
88+
[InlineData("*")]
89+
[InlineData("*/?arg=value")]
90+
[InlineData("*?arg=value")]
91+
[InlineData("DoesNotStartWith/")]
92+
[InlineData("DoesNotStartWith/?arg=value")]
93+
[InlineData("DoesNotStartWithSlash?arg=value")]
94+
[InlineData("./")]
95+
[InlineData("../")]
96+
[InlineData("../.")]
97+
[InlineData(".././")]
98+
[InlineData("../..")]
99+
[InlineData("../../")]
100+
public async Task NonPathRequestTargetSetInRawTarget(string requestTarget)
101+
{
102+
var testContext = new TestServiceContext();
103+
104+
using (var server = new TestServer(async context =>
105+
{
106+
Assert.Equal(requestTarget, context.Features.Get<IHttpRequestFeature>().RawTarget);
107+
Assert.Empty(context.Request.Path.Value);
108+
Assert.Empty(context.Request.PathBase.Value);
109+
Assert.Empty(context.Request.QueryString.Value);
110+
111+
context.Response.Headers["Content-Length"] = new[] { "11" };
112+
await context.Response.WriteAsync("Hello World");
113+
}, testContext))
114+
{
115+
using (var connection = server.CreateConnection())
116+
{
117+
await connection.SendEnd(
118+
$"GET {requestTarget} HTTP/1.0",
119+
"",
120+
"");
121+
await connection.ReceiveEnd(
122+
"HTTP/1.1 200 OK",
123+
$"Date: {testContext.DateHeaderValue}",
124+
"Content-Length: 11",
125+
"",
126+
"Hello World");
127+
}
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)