// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Text;
using System.Web.Http;
namespace System.Net.Http.Formatting.Parsers
{
///
/// HTTP Request Line parser for parsing the first line (the request line) in an HTTP request.
///
internal class HttpRequestLineParser
{
internal const int MinRequestLineSize = 14;
private const int DefaultTokenAllocation = 2 * 1024;
private int _totalBytesConsumed;
private int _maximumHeaderLength;
private HttpRequestLineState _requestLineState;
private HttpUnsortedRequest _httpRequest;
private StringBuilder _currentToken = new StringBuilder(DefaultTokenAllocation);
///
/// Initializes a new instance of the class.
///
/// instance where the request line properties will be set as they are parsed.
/// Maximum length of HTTP header.
public HttpRequestLineParser(HttpUnsortedRequest httpRequest, int maxRequestLineSize)
{
// The minimum length which would be an empty header terminated by CRLF
if (maxRequestLineSize < MinRequestLineSize)
{
throw Error.ArgumentMustBeGreaterThanOrEqualTo("maxRequestLineSize", maxRequestLineSize, MinRequestLineSize);
}
if (httpRequest == null)
{
throw Error.ArgumentNull("httpRequest");
}
_httpRequest = httpRequest;
_maximumHeaderLength = maxRequestLineSize;
}
private enum HttpRequestLineState
{
RequestMethod = 0,
RequestUri,
BeforeVersionNumbers,
MajorVersionNumber,
MinorVersionNumber,
AfterCarriageReturn
}
///
/// Parse an HTTP request line.
/// Bytes are parsed in a consuming manner from the beginning of the request buffer meaning that the same bytes can not be
/// present in the request buffer.
///
/// Request buffer from where request is read
/// Size of request buffer
/// Offset into request buffer
/// State of the parser.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
public ParserState ParseBuffer(
byte[] buffer,
int bytesReady,
ref int bytesConsumed)
{
if (buffer == null)
{
throw Error.ArgumentNull("buffer");
}
ParserState parseStatus = ParserState.NeedMoreData;
if (bytesConsumed >= bytesReady)
{
// We already can tell we need more data
return parseStatus;
}
try
{
parseStatus = ParseRequestLine(
buffer,
bytesReady,
ref bytesConsumed,
ref _requestLineState,
_maximumHeaderLength,
ref _totalBytesConsumed,
_currentToken,
_httpRequest);
}
catch (Exception)
{
parseStatus = ParserState.Invalid;
}
return parseStatus;
}
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
private static ParserState ParseRequestLine(
byte[] buffer,
int bytesReady,
ref int bytesConsumed,
ref HttpRequestLineState requestLineState,
int maximumHeaderLength,
ref int totalBytesConsumed,
StringBuilder currentToken,
HttpUnsortedRequest httpRequest)
{
Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseRequestLine()|(bytesReady - bytesConsumed) < 0");
Contract.Assert(maximumHeaderLength <= 0 || totalBytesConsumed <= maximumHeaderLength, "ParseRequestLine()|Headers already read exceeds limit.");
// Remember where we started.
int initialBytesParsed = bytesConsumed;
int segmentStart;
// Set up parsing status with what will happen if we exceed the buffer.
ParserState parseStatus = ParserState.DataTooBig;
int effectiveMax = maximumHeaderLength <= 0 ? Int32.MaxValue : (maximumHeaderLength - totalBytesConsumed + bytesConsumed);
if (bytesReady < effectiveMax)
{
parseStatus = ParserState.NeedMoreData;
effectiveMax = bytesReady;
}
Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max header length.");
switch (requestLineState)
{
case HttpRequestLineState.RequestMethod:
segmentStart = bytesConsumed;
while (buffer[bytesConsumed] != ' ')
{
if (buffer[bytesConsumed] < 0x21 || buffer[bytesConsumed] > 0x7a)
{
parseStatus = ParserState.Invalid;
goto quit;
}
if (++bytesConsumed == effectiveMax)
{
string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(method);
goto quit;
}
}
if (bytesConsumed > segmentStart)
{
string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(method);
}
// Copy value out
httpRequest.Method = new HttpMethod(currentToken.ToString());
currentToken.Clear();
// Move past the SP
requestLineState = HttpRequestLineState.RequestUri;
if (++bytesConsumed == effectiveMax)
{
goto quit;
}
goto case HttpRequestLineState.RequestUri;
case HttpRequestLineState.RequestUri:
segmentStart = bytesConsumed;
while (buffer[bytesConsumed] != ' ')
{
if (buffer[bytesConsumed] == '\r')
{
parseStatus = ParserState.Invalid;
goto quit;
}
if (++bytesConsumed == effectiveMax)
{
string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(addr);
goto quit;
}
}
if (bytesConsumed > segmentStart)
{
string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(addr);
}
// URI validation happens when we create the URI later.
if (currentToken.Length == 0)
{
throw new FormatException(Properties.Resources.HttpMessageParserEmptyUri);
}
// Copy value out
httpRequest.RequestUri = currentToken.ToString();
currentToken.Clear();
// Move past the SP
requestLineState = HttpRequestLineState.BeforeVersionNumbers;
if (++bytesConsumed == effectiveMax)
{
goto quit;
}
goto case HttpRequestLineState.BeforeVersionNumbers;
case HttpRequestLineState.BeforeVersionNumbers:
segmentStart = bytesConsumed;
while (buffer[bytesConsumed] != '/')
{
if (buffer[bytesConsumed] < 0x21 || buffer[bytesConsumed] > 0x7a)
{
parseStatus = ParserState.Invalid;
goto quit;
}
if (++bytesConsumed == effectiveMax)
{
string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(token);
goto quit;
}
}
if (bytesConsumed > segmentStart)
{
string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(token);
}
// Validate value
string version = currentToken.ToString();
if (String.CompareOrdinal(FormattingUtilities.HttpVersionToken, version) != 0)
{
throw new FormatException(Error.Format(Properties.Resources.HttpInvalidVersion, version, FormattingUtilities.HttpVersionToken));
}
currentToken.Clear();
// Move past the '/'
requestLineState = HttpRequestLineState.MajorVersionNumber;
if (++bytesConsumed == effectiveMax)
{
goto quit;
}
goto case HttpRequestLineState.MajorVersionNumber;
case HttpRequestLineState.MajorVersionNumber:
segmentStart = bytesConsumed;
while (buffer[bytesConsumed] != '.')
{
if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
{
parseStatus = ParserState.Invalid;
goto quit;
}
if (++bytesConsumed == effectiveMax)
{
string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(major);
goto quit;
}
}
if (bytesConsumed > segmentStart)
{
string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(major);
}
// Move past the "."
currentToken.Append('.');
requestLineState = HttpRequestLineState.MinorVersionNumber;
if (++bytesConsumed == effectiveMax)
{
goto quit;
}
goto case HttpRequestLineState.MinorVersionNumber;
case HttpRequestLineState.MinorVersionNumber:
segmentStart = bytesConsumed;
while (buffer[bytesConsumed] != '\r')
{
if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
{
parseStatus = ParserState.Invalid;
goto quit;
}
if (++bytesConsumed == effectiveMax)
{
string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(minor);
goto quit;
}
}
if (bytesConsumed > segmentStart)
{
string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
currentToken.Append(minor);
}
// Copy out value
httpRequest.Version = Version.Parse(currentToken.ToString());
currentToken.Clear();
// Move past the CR
requestLineState = HttpRequestLineState.AfterCarriageReturn;
if (++bytesConsumed == effectiveMax)
{
goto quit;
}
goto case HttpRequestLineState.AfterCarriageReturn;
case HttpRequestLineState.AfterCarriageReturn:
if (buffer[bytesConsumed] != '\n')
{
parseStatus = ParserState.Invalid;
goto quit;
}
parseStatus = ParserState.Done;
bytesConsumed++;
break;
}
quit:
totalBytesConsumed += bytesConsumed - initialBytesParsed;
return parseStatus;
}
}
}