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

Commit 5b1d778

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 57b3685 commit 5b1d778

File tree

6 files changed

+323
-34
lines changed

6 files changed

+323
-34
lines changed

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

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ public abstract partial class Frame : IFrameControl
2929
private const byte ByteCR = (byte)'\r';
3030
private const byte ByteLF = (byte)'\n';
3131
private const byte ByteColon = (byte)':';
32+
private const byte ByteForwardSlash = (byte)'/';
3233
private const byte ByteSpace = (byte)' ';
3334
private const byte ByteTab = (byte)'\t';
3435
private const byte ByteQuestionMark = (byte)'?';
3536
private const byte BytePercentage = (byte)'%';
3637

38+
private const string EmptyPath = "/";
39+
3740
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
3841
private static readonly ArraySegment<byte> _continueBytes = CreateAsciiByteArraySegment("HTTP/1.1 100 Continue\r\n\r\n");
3942

@@ -976,7 +979,7 @@ private void CreateResponseHeader(
976979

977980
public RequestLineStatus TakeStartLine(SocketInput input)
978981
{
979-
const int MaxInvalidRequestLineChars = 32;
982+
// expected start line format: https://tools.ietf.org/html/rfc7230#section-3.1.1
980983

981984
var scan = input.ConsumingStart();
982985
var start = scan;
@@ -1014,22 +1017,21 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10141017
}
10151018
end.Take();
10161019

1020+
// begin consuming method
10171021
string method;
10181022
var begin = scan;
10191023
if (!begin.GetKnownMethod(out method))
10201024
{
10211025
if (scan.Seek(ByteSpace, ref end) == -1)
10221026
{
1023-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1024-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1027+
RejectInvalidStartLine(start, end);
10251028
}
10261029

10271030
method = begin.GetAsciiString(ref scan);
10281031

10291032
if (method == null)
10301033
{
1031-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1032-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1034+
RejectInvalidStartLine(start, end);
10331035
}
10341036

10351037
// Note: We're not in the fast path any more (GetKnownMethod should have handled any HTTP Method we're aware of)
@@ -1038,8 +1040,7 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10381040
{
10391041
if (!IsValidTokenChar(method[i]))
10401042
{
1041-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1042-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1043+
RejectInvalidStartLine(start, end);
10431044
}
10441045
}
10451046
}
@@ -1048,55 +1049,78 @@ public RequestLineStatus TakeStartLine(SocketInput input)
10481049
scan.Skip(method.Length);
10491050
}
10501051

1052+
// consume space
10511053
scan.Take();
1054+
1055+
// begin consuming request-target
10521056
begin = scan;
1057+
1058+
var targetBegin = scan;
1059+
1060+
if (targetBegin.Peek() != ByteForwardSlash)
1061+
{
1062+
string requestUriScheme;
1063+
if (scan.GetKnownHttpSchema(out requestUriScheme))
1064+
{
1065+
// Start-line can contain uri in absolute form. e.g. 'GET http://contoso.com/favicon.ico HTTP/1.1'
1066+
// Clients should only send this to proxies, but the spec requires we handle it anyways.
1067+
// cref https://tools.ietf.org/html/rfc7230#section-5.3
1068+
scan.Skip(requestUriScheme.Length);
1069+
1070+
if (scan.Seek(ByteForwardSlash, ByteSpace, ByteQuestionMark, ref end) == -1)
1071+
{
1072+
RejectInvalidStartLine(start, end);
1073+
}
1074+
1075+
begin = scan;
1076+
}
1077+
}
1078+
10531079
var needDecode = false;
10541080
var chFound = scan.Seek(ByteSpace, ByteQuestionMark, BytePercentage, ref end);
10551081
if (chFound == -1)
10561082
{
1057-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1058-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1083+
RejectInvalidStartLine(start, end);
10591084
}
10601085
else if (chFound == BytePercentage)
10611086
{
10621087
needDecode = true;
10631088
chFound = scan.Seek(ByteSpace, ByteQuestionMark, ref end);
10641089
if (chFound == -1)
10651090
{
1066-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1067-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1091+
RejectInvalidStartLine(start, end);
10681092
}
10691093
}
10701094

10711095
var pathBegin = begin;
10721096
var pathEnd = scan;
10731097

1074-
var queryString = "";
1098+
var queryString = string.Empty;
10751099
if (chFound == ByteQuestionMark)
10761100
{
10771101
begin = scan;
10781102
if (scan.Seek(ByteSpace, ref end) == -1)
10791103
{
1080-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1081-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1104+
RejectInvalidStartLine(start, end);
10821105
}
10831106
queryString = begin.GetAsciiString(ref scan);
10841107
}
10851108

10861109
var queryEnd = scan;
10871110

1088-
if (pathBegin.Peek() == ByteSpace)
1111+
if (pathBegin.Peek() == ByteSpace && targetBegin.Index == pathBegin.Index)
10891112
{
1090-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1091-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1113+
RejectInvalidStartLine(start, end);
10921114
}
10931115

1116+
// consume space
10941117
scan.Take();
1118+
1119+
// begin consuming HTTP-version
10951120
begin = scan;
10961121
if (scan.Seek(ByteCR, ref end) == -1)
10971122
{
1098-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1099-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1123+
RejectInvalidStartLine(start, end);
11001124
}
11011125

11021126
string httpVersion;
@@ -1106,20 +1130,19 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11061130

11071131
if (httpVersion == string.Empty)
11081132
{
1109-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1110-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1133+
RejectInvalidStartLine(start, end);
11111134
}
11121135
else
11131136
{
11141137
RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion, httpVersion);
11151138
}
11161139
}
11171140

1118-
scan.Take(); // consume CR
1141+
// consume CR
1142+
scan.Take();
11191143
if (scan.Take() != ByteLF)
11201144
{
1121-
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1122-
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars) : string.Empty);
1145+
RejectInvalidStartLine(start, end);
11231146
}
11241147

11251148
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
@@ -1130,7 +1153,7 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11301153
if (needDecode)
11311154
{
11321155
// Read raw target before mutating memory.
1133-
rawTarget = pathBegin.GetAsciiString(ref queryEnd);
1156+
rawTarget = targetBegin.GetAsciiString(ref queryEnd);
11341157

11351158
// URI was encoded, unescape and then parse as utf8
11361159
pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
@@ -1141,19 +1164,34 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11411164
// URI wasn't encoded, parse as ASCII
11421165
requestUrlPath = pathBegin.GetAsciiString(ref pathEnd);
11431166

1144-
if (queryString.Length == 0)
1167+
if (queryString.Length == 0 && targetBegin.Index == pathBegin.Index)
11451168
{
11461169
// No need to allocate an extra string if the path didn't need
1147-
// decoding and there's no query string following it.
1170+
// decoding and there's no query string following it, and the
1171+
// request-target isn't absolute-form
11481172
rawTarget = requestUrlPath;
11491173
}
11501174
else
11511175
{
1152-
rawTarget = pathBegin.GetAsciiString(ref queryEnd);
1176+
rawTarget = targetBegin.GetAsciiString(ref queryEnd);
11531177
}
11541178
}
11551179

1156-
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
1180+
if (targetBegin.Index < pathBegin.Index)
1181+
{
1182+
// validation of absolute-form URI may be slow, but clients
1183+
// should not be sending this form anyways, so perf optimization
1184+
// not high priority
1185+
Uri _;
1186+
if (!Uri.TryCreate(rawTarget, UriKind.Absolute, out _))
1187+
{
1188+
RejectInvalidStartLine(start, end);
1189+
}
1190+
}
1191+
1192+
var normalizedUrlPath = requestUrlPath == null
1193+
? EmptyPath
1194+
: PathNormalizer.RemoveDotSegments(requestUrlPath);
11571195

11581196
consumed = scan;
11591197
Method = method;
@@ -1162,17 +1200,24 @@ public RequestLineStatus TakeStartLine(SocketInput input)
11621200
HttpVersion = httpVersion;
11631201

11641202
bool caseMatches;
1165-
if (RequestUrlStartsWithPathBase(normalizedTarget, out caseMatches))
1203+
if (RequestUrlStartsWithPathBase(normalizedUrlPath, out caseMatches))
11661204
{
1167-
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
1168-
Path = normalizedTarget.Substring(_pathBase.Length);
1205+
// request-target is in origin-form or absolute-form
1206+
// and path should be adjusted for matching the server base path
1207+
PathBase = caseMatches ? _pathBase : normalizedUrlPath.Substring(0, _pathBase.Length);
1208+
Path = normalizedUrlPath.Substring(_pathBase.Length);
11691209
}
1170-
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
1210+
else if ((requestUrlPath?.Length > 0 && requestUrlPath[0] == '/')
1211+
|| (rawTarget.Length > 0 && rawTarget[0] == '/')
1212+
|| ReferenceEquals(normalizedUrlPath, EmptyPath))
11711213
{
1172-
Path = normalizedTarget;
1214+
// request-target is in origin-form or absolute-form
1215+
Path = normalizedUrlPath;
11731216
}
11741217
else
11751218
{
1219+
// request-target is in asterisk-form and authority-form
1220+
// also the catch-all for other malformed request-targets
11761221
Path = string.Empty;
11771222
PathBase = string.Empty;
11781223
QueryString = string.Empty;
@@ -1480,6 +1525,16 @@ private void ThrowResponseAbortedException()
14801525
_applicationException);
14811526
}
14821527

1528+
private void RejectInvalidStartLine(MemoryPoolIterator start, MemoryPoolIterator end)
1529+
{
1530+
const int MaxInvalidRequestLineChars = 32;
1531+
1532+
RejectRequest(RequestRejectionReason.InvalidRequestLine,
1533+
Log.IsEnabled(LogLevel.Information)
1534+
? start.GetAsciiStringEscaped(end, MaxInvalidRequestLineChars)
1535+
: string.Empty);
1536+
}
1537+
14831538
public void RejectRequest(RequestRejectionReason reason)
14841539
{
14851540
RejectRequest(BadHttpRequestException.GetException(reason));

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

Lines changed: 37 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,37 @@ 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 'http://' or 'https://'
168+
/// </summary>
169+
/// <param name="begin">The iterator</param>
170+
/// <param name="knownScheme">A reference to the known scheme, if the input matches any</param>
171+
/// <returns>True when memory starts with known http or https schema</returns>
172+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
173+
public static bool GetKnownHttpSchema(this MemoryPoolIterator begin, out string knownScheme)
174+
{
175+
knownScheme = null;
176+
ulong value;
177+
if (!begin.TryPeekLong(out value))
178+
{
179+
return false;
180+
}
181+
182+
if ((value & _mask7Chars) == _httpSchemeLong)
183+
{
184+
knownScheme = HttpScheme;
185+
return true;
186+
}
187+
188+
if (value == _httpsSchemeLong)
189+
{
190+
knownScheme = HttpsScheme;
191+
return true;
192+
}
193+
194+
return false;
195+
}
196+
160197
/// <summary>
161198
/// Checks 9 bytes from <paramref name="begin"/> correspond to a known HTTP version.
162199
/// </summary>

0 commit comments

Comments
 (0)