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

Commit 0addd0b

Browse files
author
Nate McMaster
committed
Handle requests using absolute-form request URIs.
An absolute-form request URI has a start line in form: "GET http://host/path HTTP/1.1". RFC 7230 section 5.3.2 stipulates that servers should allow absolute-form request URIs. This change will handles requests using absolute-form. The scheme and authority section of the absolute URI are ignored, but will still appear in IHttpRequestFeature.RawTarget. Resolves #666
1 parent b46e48f commit 0addd0b

File tree

4 files changed

+182
-14
lines changed

4 files changed

+182
-14
lines changed

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

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public abstract partial class Frame : IFrameControl
2828
private const byte ByteCR = (byte)'\r';
2929
private const byte ByteLF = (byte)'\n';
3030
private const byte ByteColon = (byte)':';
31+
private const byte ByteForwardSlash = (byte)'/';
3132
private const byte ByteSpace = (byte)' ';
3233
private const byte ByteTab = (byte)'\t';
3334
private const byte ByteQuestionMark = (byte)'?';
@@ -973,6 +974,8 @@ private void CreateResponseHeader(
973974

974975
public RequestLineStatus TakeStartLine(SocketInput input)
975976
{
977+
// expected start line format: https://tools.ietf.org/html/rfc7230#section-3.1.1
978+
976979
const int MaxInvalidRequestLineChars = 32;
977980

978981
var scan = input.ConsumingStart();
@@ -1011,6 +1014,7 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10111014
}
10121015
end.Take();
10131016

1017+
// begin consuming method
10141018
string method;
10151019
var begin = scan;
10161020
if (!begin.GetKnownMethod(out method))
@@ -1045,8 +1049,38 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10451049
scan.Skip(method.Length);
10461050
}
10471051

1048-
scan.Take();
1052+
scan.Take(); // consume space
1053+
1054+
// begin consuming request-target
10491055
begin = scan;
1056+
1057+
string requestUriScheme;
1058+
string requestAuthority = null;
1059+
if (scan.GetKnownUriScheme(out requestUriScheme))
1060+
{
1061+
// Request URIs can be in absolute form
1062+
// See https://tools.ietf.org/html/rfc7230#section-5.3.2
1063+
// This will skip over scheme and authority for determinie path, but preserving the vlues for rawTarget.
1064+
// We rely on the Host header and server configuration to determine the effective host, port, and scheme
1065+
// for this request.
1066+
scan.Skip(requestUriScheme.Length);
1067+
begin = scan;
1068+
1069+
// an absolute URI is not required to end in a slash but must not be empty
1070+
// see https://tools.ietf.org/html/rfc3986#section-4.3
1071+
1072+
var pathIndex = scan.Seek(ByteForwardSlash, ByteSpace, ref end);
1073+
if (pathIndex == -1)
1074+
{
1075+
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1076+
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1077+
}
1078+
1079+
// TODO consider handling unicode host names
1080+
requestAuthority = begin.GetAsciiString(ref scan);
1081+
begin = scan;
1082+
}
1083+
10501084
var needDecode = false;
10511085
var chFound = scan.Seek(ByteSpace, ByteQuestionMark, BytePercentage, ref end);
10521086
if (chFound == -1)
@@ -1082,13 +1116,15 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10821116

10831117
var queryEnd = scan;
10841118

1085-
if (pathBegin.Peek() == ByteSpace)
1119+
if (pathBegin.Peek() == ByteSpace && requestAuthority == null)
10861120
{
10871121
RejectRequest(RequestRejectionReason.InvalidRequestLine,
10881122
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
10891123
}
10901124

1091-
scan.Take();
1125+
scan.Take(); // consume space
1126+
1127+
// begin consuming HTTP-version
10921128
begin = scan;
10931129
if (scan.Seek(ByteCR, ref end) == -1)
10941130
{
@@ -1123,11 +1159,11 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11231159
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
11241160
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
11251161
string requestUrlPath;
1126-
string rawTarget;
1162+
string rawUrlPath;
11271163
if (needDecode)
11281164
{
11291165
// Read raw target before mutating memory.
1130-
rawTarget = pathBegin.GetAsciiString(ref queryEnd);
1166+
rawUrlPath = pathBegin.GetAsciiString(ref queryEnd);
11311167

11321168
// URI was encoded, unescape and then parse as utf8
11331169
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
@@ -1142,31 +1178,33 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11421178
{
11431179
// No need to allocate an extra string if the path didn't need
11441180
// decoding and there's no query string following it.
1145-
rawTarget = requestUrlPath;
1181+
rawUrlPath = requestUrlPath;
11461182
}
11471183
else
11481184
{
1149-
rawTarget = pathBegin.GetAsciiString(ref queryEnd);
1185+
rawUrlPath = pathBegin.GetAsciiString(ref queryEnd);
11501186
}
11511187
}
11521188

1153-
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
1189+
var normalizedUrlPath = requestUrlPath == null
1190+
? string.Empty
1191+
: PathNormalizer.RemoveDotSegments(requestUrlPath);
11541192

11551193
consumed = scan;
11561194
Method = method;
11571195
QueryString = queryString;
1158-
RawTarget = rawTarget;
1196+
RawTarget = requestUriScheme + requestAuthority + rawUrlPath;
11591197
HttpVersion = httpVersion;
11601198

11611199
bool caseMatches;
1162-
if (RequestUrlStartsWithPathBase(normalizedTarget, out caseMatches))
1200+
if (RequestUrlStartsWithPathBase(normalizedUrlPath, out caseMatches))
11631201
{
1164-
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
1165-
Path = normalizedTarget.Substring(_pathBase.Length);
1202+
PathBase = caseMatches ? _pathBase : normalizedUrlPath.Substring(0, _pathBase.Length);
1203+
Path = normalizedUrlPath.Substring(_pathBase.Length);
11661204
}
1167-
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
1205+
else if (rawUrlPath?.Length > 0 && rawUrlPath[0] == '/') // check rawUrlPath since normalizedUrlPath can be "" or "/" after dot segment removal
11681206
{
1169-
Path = normalizedTarget;
1207+
Path = normalizedUrlPath;
11701208
}
11711209
else
11721210
{

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/MemoryPoolIteratorExtensions.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public static class MemoryPoolIteratorExtensions
1414
public const string Http10Version = "HTTP/1.0";
1515
public const string Http11Version = "HTTP/1.1";
1616

17+
public const string HttpScheme = "http://";
18+
public const string HttpsScheme = "https://";
19+
1720
// readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079
1821
private readonly static ulong _httpConnectMethodLong = GetAsciiStringAsLong("CONNECT ");
1922
private readonly static ulong _httpDeleteMethodLong = GetAsciiStringAsLong("DELETE \0");
@@ -28,6 +31,9 @@ public static class MemoryPoolIteratorExtensions
2831
private readonly static ulong _http10VersionLong = GetAsciiStringAsLong("HTTP/1.0");
2932
private readonly static ulong _http11VersionLong = GetAsciiStringAsLong("HTTP/1.1");
3033

34+
private readonly static ulong _httpSchemeLong = GetAsciiStringAsLong("http://\0");
35+
private readonly static ulong _httpsSchemeLong = GetAsciiStringAsLong("https://");
36+
3137
private readonly static ulong _mask8Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff });
3238
private readonly static ulong _mask7Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 });
3339
private readonly static ulong _mask6Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00 });
@@ -157,6 +163,42 @@ public static bool GetKnownMethod(this MemoryPoolIterator begin, out string know
157163
return false;
158164
}
159165

166+
/// <summary>
167+
/// Checks 8 bytes from <paramref name="begin"/> that correspond to a known URI scheme.
168+
/// </summary>
169+
/// <remarks>
170+
/// Currently recognizes these schemes:
171+
/// - 'http://'
172+
/// - 'https://'
173+
/// </remarks>
174+
/// <param name="begin">The iterator</param>
175+
/// <param name="knownScheme">A reference to the known scheme, if the input matches any</param>
176+
/// <returns></returns>
177+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
178+
public static bool GetKnownUriScheme(this MemoryPoolIterator begin, out string knownScheme)
179+
{
180+
knownScheme = null;
181+
ulong value;
182+
if (!begin.TryPeekLong(out value))
183+
{
184+
return false;
185+
}
186+
187+
if ((value & _mask7Chars) == _httpSchemeLong)
188+
{
189+
knownScheme = HttpScheme;
190+
return true;
191+
}
192+
193+
if (value == _httpsSchemeLong)
194+
{
195+
knownScheme = HttpsScheme;
196+
return true;
197+
}
198+
199+
return false;
200+
}
201+
160202
/// <summary>
161203
/// Checks 9 bytes from <paramref name="begin"/> correspond to a known HTTP version.
162204
/// </summary>

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,72 @@ public async Task RequestAbortedTokenFiredOnClientFIN()
453453
}
454454
}
455455

456+
[Theory]
457+
[InlineData("http://localhost/abs/path", true, "/abs/path")]
458+
[InlineData("https://localhost/abs/path", true, "/abs/path")] // handles mismatch scheme
459+
[InlineData("https://localhost:22/abs/path", true, "/abs/path")] // handles mismatched ports
460+
[InlineData("https://differenthost/abs/path", true, "/abs/path")] // handles mismatched hostname
461+
[InlineData("http://localhost/", true, "/")]
462+
[InlineData("https://localhost/", true, "/")]
463+
[InlineData("http://localhost", true, "")]
464+
[InlineData("http://", false, null)]
465+
[InlineData("https://", false, null)]
466+
public async Task CanHandleRequestsWithUrlInAbsoluteForm(string requestUrl, bool valid, string expectedPath)
467+
{
468+
var pathTcs = new TaskCompletionSource<PathString>();
469+
var rawTargetTcs = new TaskCompletionSource<string>();
470+
var hostTcs = new TaskCompletionSource<HostString>();
471+
472+
var builder = new WebHostBuilder()
473+
.UseKestrel()
474+
.UseUrls("http://127.0.0.1:0")
475+
.Configure(app =>
476+
{
477+
app.Run(async context =>
478+
{
479+
pathTcs.TrySetResult(context.Request.Path);
480+
hostTcs.TrySetResult(context.Request.Host);
481+
rawTargetTcs.TrySetResult(context.Features.Get<IHttpRequestFeature>().RawTarget);
482+
await context.Response.WriteAsync("Done");
483+
});
484+
});
485+
486+
using (var host = builder.Build())
487+
{
488+
host.Start();
489+
490+
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
491+
{
492+
socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort()));
493+
var raw = $"GET {requestUrl} HTTP/1.1\r\n" +
494+
"Content-Length: 0\r\n" +
495+
"Host: localhost\r\n" +
496+
"Connection: close\r\n" +
497+
"\r\n";
498+
499+
socket.Send(Encoding.ASCII.GetBytes(raw));
500+
if (valid)
501+
{
502+
await Task.WhenAll(pathTcs.Task, rawTargetTcs.Task, hostTcs.Task).OrTimeout(TimeSpan.FromSeconds(30));
503+
Assert.Equal(new PathString(expectedPath), pathTcs.Task.Result);
504+
Assert.Equal(requestUrl, rawTargetTcs.Task.Result);
505+
Assert.Equal("localhost", hostTcs.Task.Result.ToString());
506+
}
507+
else
508+
{
509+
var response = new StringBuilder();
510+
var responseBytes = new byte[4096];
511+
var received = 0;
512+
while ((received = socket.Receive(responseBytes)) > 0)
513+
{
514+
response.Append(Encoding.ASCII.GetString(responseBytes, 0, received));
515+
}
516+
Assert.StartsWith("HTTP/1.1 400", response.ToString());
517+
}
518+
}
519+
}
520+
}
521+
456522
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
457523
{
458524
var builder = new WebHostBuilder()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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;
5+
using System.Runtime.CompilerServices;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
9+
{
10+
public static class TaskExtensions
11+
{
12+
public static async Task OrTimeout(this Task task, TimeSpan timeout,
13+
[CallerFilePath] string file = null, [CallerLineNumber] int line = 0)
14+
{
15+
var finished = await Task.WhenAny(task, Task.Delay(timeout));
16+
if (!ReferenceEquals(finished, task))
17+
{
18+
throw new TimeoutException($"Task exceeded max running time of {timeout.TotalSeconds}s at {file}:{line}");
19+
}
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)