-
Notifications
You must be signed in to change notification settings - Fork 522
Handle absolute, asterisk, and authority-form request targets #1470
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,6 @@ | |
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Text.Encodings.Web.Utf8; | ||
using System.Text.Utf8; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Http; | ||
|
@@ -27,6 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http | |
{ | ||
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler | ||
{ | ||
private const byte ByteAsterisk = (byte)'*'; | ||
private const byte ByteForwardSlash = (byte)'/'; | ||
private const byte BytePercentage = (byte)'%'; | ||
|
||
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n"); | ||
|
@@ -39,6 +40,9 @@ public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IH | |
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n"); | ||
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel"); | ||
|
||
private const string EmptyPath = "/"; | ||
private const string Asterisk = "*"; | ||
|
||
private readonly object _onStartingSync = new Object(); | ||
private readonly object _onCompletedSync = new Object(); | ||
|
||
|
@@ -818,7 +822,7 @@ protected Task ProduceEnd() | |
// that should take precedence. | ||
if (_requestRejectedException != null) | ||
{ | ||
SetErrorResponseHeaders(statusCode: _requestRejectedException.StatusCode); | ||
SetErrorResponseException(_requestRejectedException); | ||
} | ||
else | ||
{ | ||
|
@@ -1077,7 +1081,7 @@ public bool TakeMessageHeaders(ReadableBuffer buffer, out ReadCursor consumed, o | |
{ | ||
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed); | ||
|
||
// If we sliced it means the current buffer bigger than what we're | ||
// If we sliced it means the current buffer bigger than what we're | ||
// allowed to look at | ||
overLength = true; | ||
} | ||
|
@@ -1127,6 +1131,16 @@ private void RejectNonBodyTransferEncodingResponse(bool appCompleted) | |
} | ||
} | ||
|
||
private void SetErrorResponseException(BadHttpRequestException ex) | ||
{ | ||
SetErrorResponseHeaders(ex.StatusCode); | ||
|
||
if (!StringValues.IsNullOrEmpty(ex.AllowedHeader)) | ||
{ | ||
FrameResponseHeaders.HeaderAllow = ex.AllowedHeader; | ||
} | ||
} | ||
|
||
private void SetErrorResponseHeaders(int statusCode) | ||
{ | ||
Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started."); | ||
|
@@ -1179,6 +1193,15 @@ public void RejectRequest(RequestRejectionReason reason, string value) | |
throw BadHttpRequestException.GetException(reason, value); | ||
} | ||
|
||
private void RejectRequestLine(Span<byte> requestLine) | ||
{ | ||
Debug.Assert(Log.IsEnabled(LogLevel.Information) == true, "Use RejectRequest instead to improve inlining when log is disabled"); | ||
|
||
const int MaxRequestLineError = 32; | ||
var line = requestLine.GetAsciiStringEscaped(MaxRequestLineError); | ||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine, line); | ||
} | ||
|
||
public void SetBadRequestState(RequestRejectionReason reason) | ||
{ | ||
SetBadRequestState(BadHttpRequestException.GetException(reason)); | ||
|
@@ -1190,7 +1213,7 @@ public void SetBadRequestState(BadHttpRequestException ex) | |
|
||
if (!HasResponseStarted) | ||
{ | ||
SetErrorResponseHeaders(ex.StatusCode); | ||
SetErrorResponseException(ex); | ||
} | ||
|
||
_keepAlive = false; | ||
|
@@ -1216,8 +1239,51 @@ protected void ReportApplicationError(Exception ex) | |
Log.ApplicationError(ConnectionId, ex); | ||
} | ||
|
||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded) | ||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded) | ||
{ | ||
Debug.Assert(target.Length != 0, "Request target must be non-zero length"); | ||
|
||
var ch = target[0]; | ||
if (ch == ByteForwardSlash) | ||
{ | ||
// origin-form. | ||
// The most common form of request-target. | ||
// https://tools.ietf.org/html/rfc7230#section-5.3.1 | ||
OnOriginFormTarget(method, version, target, path, query, customMethod, pathEncoded); | ||
} | ||
else if (ch == ByteAsterisk && target.Length == 1) | ||
{ | ||
OnAsteriskFormTarget(method); | ||
} | ||
else if (target.GetKnownHttpScheme(out var scheme)) | ||
{ | ||
OnAbsoluteFormTarget(target, query, line); | ||
} | ||
else | ||
{ | ||
// Assume anything else is considered authority form. | ||
// FYI: this should be an edge case. This should only happen when | ||
// a client mistakenly things this server is a proxy server. | ||
|
||
OnAuthorityFormTarget(method, target, line); | ||
} | ||
|
||
Method = method != HttpMethod.Custom | ||
? HttpUtilities.MethodToString(method) ?? string.Empty | ||
: customMethod.GetAsciiStringNonNullCharacters(); | ||
HttpVersion = HttpUtilities.VersionToString(version); | ||
|
||
Debug.Assert(RawTarget != null, "RawTarget was not set"); | ||
Debug.Assert(Method != null, "Method was not set"); | ||
Debug.Assert(Path != null, "Path was not set"); | ||
Debug.Assert(QueryString != "QueryString was not set"); | ||
Debug.Assert(HttpVersion != "HttpVersion was not set"); | ||
} | ||
|
||
private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded) | ||
{ | ||
Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /"); | ||
|
||
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11 | ||
// 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" | ||
|
@@ -1249,34 +1315,111 @@ public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> targe | |
} | ||
} | ||
|
||
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath); | ||
if (method != HttpMethod.Custom) | ||
QueryString = query.GetAsciiStringNonNullCharacters(); | ||
RawTarget = rawTarget; | ||
SetNormalizedPath(requestUrlPath); | ||
} | ||
|
||
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target, Span<byte> line) | ||
{ | ||
// TODO Validate that target is a correct host[:port] string. | ||
// Reject as 400 if not. This is just a quick scan for invalid characters | ||
// but doesn't check that the target fully matches the URI spec. | ||
for (var i = 0; i < target.Length; i++) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fear repeated scanning of strings can be bad for performance numbers. I think we'll end up moving more validation into the parser. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Last time we talked about this, we agreed we we're okay with sub-optimal performance on edge cases. This code shouldn't be hit unless we have a bad client or malformed requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
How long ago was that? I'll wait for the benchmarks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{ | ||
var ch = target[i]; | ||
if (!UriUtilities.IsValidAuthorityCharacter(ch)) | ||
{ | ||
if (Log.IsEnabled(LogLevel.Information)) | ||
{ | ||
RejectRequestLine(line); | ||
} | ||
|
||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine); | ||
} | ||
} | ||
|
||
// The authority-form of request-target is only used for CONNECT | ||
// requests (https://tools.ietf.org/html/rfc7231#section-4.3.6). | ||
if (method != HttpMethod.Connect) | ||
{ | ||
Method = HttpUtilities.MethodToString(method) ?? string.Empty; | ||
RejectRequest(RequestRejectionReason.ConnectMethodRequired); | ||
} | ||
else | ||
|
||
// When making a CONNECT request to establish a tunnel through one or | ||
// more proxies, a client MUST send only the target URI's authority | ||
// component(excluding any userinfo and its "@" delimiter) as the | ||
// request - target.For example, | ||
// | ||
// CONNECT www.example.com:80 HTTP/1.1 | ||
// | ||
// Allowed characters in the 'host + port' section of authority. | ||
// See https://tools.ietf.org/html/rfc3986#section-3.2 | ||
|
||
RawTarget = target.GetAsciiStringNonNullCharacters(); | ||
Path = string.Empty; | ||
PathBase = string.Empty; | ||
QueryString = string.Empty; | ||
} | ||
|
||
private void OnAsteriskFormTarget(HttpMethod method) | ||
{ | ||
// The asterisk-form of request-target is only used for a server-wide | ||
// OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7). | ||
if (method != HttpMethod.Options) | ||
{ | ||
RejectRequest(RequestRejectionReason.OptionsMethodRequired); | ||
} | ||
|
||
RawTarget = Asterisk; | ||
Path = string.Empty; | ||
PathBase = string.Empty; | ||
QueryString = string.Empty; | ||
} | ||
|
||
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query, Span<byte> line) | ||
{ | ||
// absolute-form | ||
// https://tools.ietf.org/html/rfc7230#section-5.3.2 | ||
|
||
// This code should be the edge-case. | ||
|
||
// From the spec: | ||
// a server MUST accept the absolute-form in requests, even though | ||
// HTTP/1.1 clients will only send them in requests to proxies. | ||
|
||
RawTarget = target.GetAsciiStringNonNullCharacters(); | ||
|
||
// Validation of absolute URIs is slow, but clients | ||
// should not be sending this form anyways, so perf optimization | ||
// not high priority | ||
|
||
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri)) | ||
{ | ||
Method = customMethod.GetAsciiStringNonNullCharacters(); | ||
if (Log.IsEnabled(LogLevel.Information)) | ||
{ | ||
RejectRequestLine(line); | ||
} | ||
|
||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine); | ||
} | ||
|
||
SetNormalizedPath(uri.LocalPath); | ||
// don't use uri.Query because we need the unescaped version | ||
QueryString = query.GetAsciiStringNonNullCharacters(); | ||
RawTarget = rawTarget; | ||
HttpVersion = HttpUtilities.VersionToString(version); | ||
} | ||
|
||
private void SetNormalizedPath(string requestPath) | ||
{ | ||
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestPath); | ||
if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches)) | ||
{ | ||
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 = string.Empty; | ||
PathBase = string.Empty; | ||
QueryString = string.Empty; | ||
Path = normalizedTarget; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// 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. | ||
|
||
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http | ||
{ | ||
public enum HttpScheme | ||
{ | ||
Unknown = -1, | ||
Http = 0, | ||
Https = 1 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1469 introduces rejection of request targets, so you won't have to pass the entire line here anymore.