497 lines
20 KiB
C#
497 lines
20 KiB
C#
// 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;
|
|
using System.Diagnostics;
|
|
using System.IO.Pipelines;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web.Utf8;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
|
|
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|
{
|
|
public abstract partial class Http1Connection : HttpProtocol
|
|
{
|
|
private const byte ByteAsterisk = (byte)'*';
|
|
private const byte ByteForwardSlash = (byte)'/';
|
|
private const string Asterisk = "*";
|
|
|
|
private readonly Http1ConnectionContext _context;
|
|
private readonly IHttpParser<Http1ParsingHandler> _parser;
|
|
protected readonly long _keepAliveTicks;
|
|
private readonly long _requestHeadersTimeoutTicks;
|
|
|
|
private volatile bool _requestTimedOut;
|
|
private uint _requestCount;
|
|
|
|
private HttpRequestTarget _requestTargetForm = HttpRequestTarget.Unknown;
|
|
private Uri _absoluteRequestTarget;
|
|
|
|
private int _remainingRequestHeadersBytesAllowed;
|
|
|
|
public Http1Connection(Http1ConnectionContext context)
|
|
: base(context)
|
|
{
|
|
_context = context;
|
|
_parser = ServiceContext.HttpParser;
|
|
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
|
|
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
|
|
|
|
Output = new Http1OutputProducer(_context.Application.Input, _context.Transport.Output, _context.ConnectionId, _context.ServiceContext.Log, _context.TimeoutControl);
|
|
}
|
|
|
|
public IPipeReader Input => _context.Transport.Input;
|
|
|
|
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
|
|
public bool RequestTimedOut => _requestTimedOut;
|
|
|
|
public override bool IsUpgradableRequest => _upgradeAvailable;
|
|
|
|
/// <summary>
|
|
/// Stops the request processing loop between requests.
|
|
/// Called on all active connections when the server wants to initiate a shutdown
|
|
/// and after a keep-alive timeout.
|
|
/// </summary>
|
|
public void StopProcessingNextRequest()
|
|
{
|
|
_keepAlive = false;
|
|
Input.CancelPendingRead();
|
|
}
|
|
|
|
public void SendTimeoutResponse()
|
|
{
|
|
_requestTimedOut = true;
|
|
Input.CancelPendingRead();
|
|
}
|
|
|
|
public void ParseRequest(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
|
{
|
|
consumed = buffer.Start;
|
|
examined = buffer.End;
|
|
|
|
switch (_requestProcessingStatus)
|
|
{
|
|
case RequestProcessingStatus.RequestPending:
|
|
if (buffer.IsEmpty)
|
|
{
|
|
break;
|
|
}
|
|
|
|
TimeoutControl.ResetTimeout(_requestHeadersTimeoutTicks, TimeoutAction.SendTimeoutResponse);
|
|
|
|
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
|
|
goto case RequestProcessingStatus.ParsingRequestLine;
|
|
case RequestProcessingStatus.ParsingRequestLine:
|
|
if (TakeStartLine(buffer, out consumed, out examined))
|
|
{
|
|
buffer = buffer.Slice(consumed, buffer.End);
|
|
|
|
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
|
|
goto case RequestProcessingStatus.ParsingHeaders;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
case RequestProcessingStatus.ParsingHeaders:
|
|
if (TakeMessageHeaders(buffer, out consumed, out examined))
|
|
{
|
|
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
public bool TakeStartLine(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
|
{
|
|
var overLength = false;
|
|
if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize)
|
|
{
|
|
buffer = buffer.Slice(buffer.Start, ServerOptions.Limits.MaxRequestLineSize);
|
|
overLength = true;
|
|
}
|
|
|
|
var result = _parser.ParseRequestLine(new Http1ParsingHandler(this), buffer, out consumed, out examined);
|
|
if (!result && overLength)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.RequestLineTooLong);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public bool TakeMessageHeaders(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
|
{
|
|
// Make sure the buffer is limited
|
|
bool overLength = false;
|
|
if (buffer.Length >= _remainingRequestHeadersBytesAllowed)
|
|
{
|
|
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
|
|
|
|
// If we sliced it means the current buffer bigger than what we're
|
|
// allowed to look at
|
|
overLength = true;
|
|
}
|
|
|
|
var result = _parser.ParseHeaders(new Http1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes);
|
|
_remainingRequestHeadersBytesAllowed -= consumedBytes;
|
|
|
|
if (!result && overLength)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.HeadersExceedMaxTotalSize);
|
|
}
|
|
if (result)
|
|
{
|
|
TimeoutControl.CancelTimeout();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, 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);
|
|
}
|
|
else
|
|
{
|
|
// Assume anything else is considered authority form.
|
|
// FYI: this should be an edge case. This should only happen when
|
|
// a client mistakenly thinks this server is a proxy server.
|
|
OnAuthorityFormTarget(method, target);
|
|
}
|
|
|
|
Method = method != HttpMethod.Custom
|
|
? HttpUtilities.MethodToString(method) ?? string.Empty
|
|
: customMethod.GetAsciiStringNonNullCharacters();
|
|
_httpVersion = 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 != null, "QueryString was not set");
|
|
Debug.Assert(HttpVersion != null, "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 /");
|
|
|
|
_requestTargetForm = HttpRequestTarget.OriginForm;
|
|
|
|
// 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"
|
|
string requestUrlPath = null;
|
|
string rawTarget = null;
|
|
|
|
try
|
|
{
|
|
// Read raw target before mutating memory.
|
|
rawTarget = target.GetAsciiStringNonNullCharacters();
|
|
|
|
if (pathEncoded)
|
|
{
|
|
// URI was encoded, unescape and then parse as UTF-8
|
|
// Disabling warning temporary
|
|
#pragma warning disable 618
|
|
var pathLength = UrlEncoder.Decode(path, path);
|
|
#pragma warning restore 618
|
|
|
|
// Removing dot segments must be done after unescaping. From RFC 3986:
|
|
//
|
|
// URI producing applications should percent-encode data octets that
|
|
// correspond to characters in the reserved set unless these characters
|
|
// are specifically allowed by the URI scheme to represent data in that
|
|
// component. If a reserved character is found in a URI component and
|
|
// no delimiting role is known for that character, then it must be
|
|
// interpreted as representing the data octet corresponding to that
|
|
// character's encoding in US-ASCII.
|
|
//
|
|
// https://tools.ietf.org/html/rfc3986#section-2.2
|
|
pathLength = PathNormalizer.RemoveDotSegments(path.Slice(0, pathLength));
|
|
|
|
requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
|
|
}
|
|
else
|
|
{
|
|
var pathLength = PathNormalizer.RemoveDotSegments(path);
|
|
|
|
if (path.Length == pathLength && query.Length == 0)
|
|
{
|
|
// If no decoding was required, no dot segments were removed and
|
|
// there is no query, the request path is the same as the raw target
|
|
requestUrlPath = rawTarget;
|
|
}
|
|
else
|
|
{
|
|
requestUrlPath = path.Slice(0, pathLength).GetAsciiStringNonNullCharacters();
|
|
}
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
ThrowRequestTargetRejected(target);
|
|
}
|
|
|
|
QueryString = query.GetAsciiStringNonNullCharacters();
|
|
RawTarget = rawTarget;
|
|
Path = requestUrlPath;
|
|
}
|
|
|
|
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
|
|
{
|
|
_requestTargetForm = HttpRequestTarget.AuthorityForm;
|
|
|
|
// This is not complete validation. It 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++)
|
|
{
|
|
var ch = target[i];
|
|
if (!UriUtilities.IsValidAuthorityCharacter(ch))
|
|
{
|
|
ThrowRequestTargetRejected(target);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.ConnectMethodRequired);
|
|
}
|
|
|
|
// 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;
|
|
QueryString = string.Empty;
|
|
}
|
|
|
|
private void OnAsteriskFormTarget(HttpMethod method)
|
|
{
|
|
_requestTargetForm = HttpRequestTarget.AsteriskForm;
|
|
|
|
// 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)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.OptionsMethodRequired);
|
|
}
|
|
|
|
RawTarget = Asterisk;
|
|
Path = string.Empty;
|
|
QueryString = string.Empty;
|
|
}
|
|
|
|
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
|
|
{
|
|
_requestTargetForm = HttpRequestTarget.AbsoluteForm;
|
|
|
|
// 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))
|
|
{
|
|
ThrowRequestTargetRejected(target);
|
|
}
|
|
|
|
_absoluteRequestTarget = uri;
|
|
Path = uri.LocalPath;
|
|
// don't use uri.Query because we need the unescaped version
|
|
QueryString = query.GetAsciiStringNonNullCharacters();
|
|
}
|
|
|
|
private unsafe static string GetUtf8String(Span<byte> path)
|
|
{
|
|
// .NET 451 doesn't have pointer overloads for Encoding.GetString so we
|
|
// copy to an array
|
|
fixed (byte* pointer = &path.DangerousGetPinnableReference())
|
|
{
|
|
return Encoding.UTF8.GetString(pointer, path.Length);
|
|
}
|
|
}
|
|
|
|
protected void EnsureHostHeaderExists()
|
|
{
|
|
if (_httpVersion == Http.HttpVersion.Http10)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc7230#section-5.4
|
|
// A server MUST respond with a 400 (Bad Request) status code to any
|
|
// HTTP/1.1 request message that lacks a Host header field and to any
|
|
// request message that contains more than one Host header field or a
|
|
// Host header field with an invalid field-value.
|
|
|
|
var host = HttpRequestHeaders.HeaderHost;
|
|
if (host.Count <= 0)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.MissingHostHeader);
|
|
}
|
|
else if (host.Count > 1)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.MultipleHostHeaders);
|
|
}
|
|
else if (_requestTargetForm == HttpRequestTarget.AuthorityForm)
|
|
{
|
|
if (!host.Equals(RawTarget))
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.InvalidHostHeader, host.ToString());
|
|
}
|
|
}
|
|
else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm)
|
|
{
|
|
// If the target URI includes an authority component, then a
|
|
// client MUST send a field - value for Host that is identical to that
|
|
// authority component, excluding any userinfo subcomponent and its "@"
|
|
// delimiter.
|
|
|
|
// System.Uri doesn't not tell us if the port was in the original string or not.
|
|
// When IsDefaultPort = true, we will allow Host: with or without the default port
|
|
var authorityAndPort = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port;
|
|
if ((host != _absoluteRequestTarget.Authority || !_absoluteRequestTarget.IsDefaultPort)
|
|
&& host != authorityAndPort)
|
|
{
|
|
ThrowRequestRejected(RequestRejectionReason.InvalidHostHeader, host.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnReset()
|
|
{
|
|
FastFeatureSet(typeof(IHttpUpgradeFeature), this);
|
|
|
|
_requestTimedOut = false;
|
|
_requestTargetForm = HttpRequestTarget.Unknown;
|
|
_absoluteRequestTarget = null;
|
|
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
|
|
_requestCount++;
|
|
}
|
|
|
|
protected override void OnRequestProcessingEnding()
|
|
{
|
|
Input.Complete();
|
|
}
|
|
|
|
protected override string CreateRequestId()
|
|
=> StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', _requestCount);
|
|
|
|
protected override MessageBody CreateMessageBody()
|
|
=> Http1MessageBody.For(_httpVersion, HttpRequestHeaders, this);
|
|
|
|
protected override void BeginRequestProcessing()
|
|
{
|
|
// Reset the features and timeout.
|
|
Reset();
|
|
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutAction.StopProcessingNextRequest);
|
|
}
|
|
|
|
protected override bool BeginRead(out ReadableBufferAwaitable awaitable)
|
|
{
|
|
awaitable = Input.ReadAsync();
|
|
return true;
|
|
}
|
|
|
|
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
|
|
{
|
|
var examined = result.Buffer.End;
|
|
var consumed = result.Buffer.End;
|
|
|
|
try
|
|
{
|
|
ParseRequest(result.Buffer, out consumed, out examined);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
if (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders)
|
|
{
|
|
throw BadHttpRequestException.GetException(RequestRejectionReason
|
|
.MalformedRequestInvalidHeaders);
|
|
}
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
Input.Advance(consumed, examined);
|
|
}
|
|
|
|
if (result.IsCompleted)
|
|
{
|
|
switch (_requestProcessingStatus)
|
|
{
|
|
case RequestProcessingStatus.RequestPending:
|
|
endConnection = true;
|
|
return true;
|
|
case RequestProcessingStatus.ParsingRequestLine:
|
|
throw BadHttpRequestException.GetException(
|
|
RequestRejectionReason.InvalidRequestLine);
|
|
case RequestProcessingStatus.ParsingHeaders:
|
|
throw BadHttpRequestException.GetException(
|
|
RequestRejectionReason.MalformedRequestInvalidHeaders);
|
|
}
|
|
}
|
|
else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
|
|
{
|
|
// Stop the request processing loop if the server is shutting down or there was a keep-alive timeout
|
|
// and there is no ongoing request.
|
|
endConnection = true;
|
|
return true;
|
|
}
|
|
else if (RequestTimedOut)
|
|
{
|
|
// In this case, there is an ongoing request but the start line/header parsing has timed out, so send
|
|
// a 408 response.
|
|
throw BadHttpRequestException.GetException(RequestRejectionReason.RequestTimeout);
|
|
}
|
|
|
|
endConnection = false;
|
|
if (_requestProcessingStatus == RequestProcessingStatus.AppStarted)
|
|
{
|
|
EnsureHostHeaderExists();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|