Reuse previous materialized strings (#8374)
This commit is contained in:
parent
4d78c21575
commit
e4fbd598b5
|
|
@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.SchedulingMode ApplicationSchedulingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
|
||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
|
||||
|
|
@ -251,6 +252,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
public partial interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(System.Span<byte> name, System.Span<byte> value);
|
||||
void OnHeadersComplete();
|
||||
}
|
||||
public partial interface IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private const byte ByteAsterisk = (byte)'*';
|
||||
private const byte ByteForwardSlash = (byte)'/';
|
||||
private const string Asterisk = "*";
|
||||
private const string ForwardSlash = "/";
|
||||
|
||||
private readonly HttpConnectionContext _context;
|
||||
private readonly IHttpParser<Http1ParsingHandler> _parser;
|
||||
|
|
@ -268,16 +269,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
_requestTargetForm = HttpRequestTarget.OriginForm;
|
||||
|
||||
if (target.Length == 1)
|
||||
{
|
||||
// If target.Length == 1 it can only be a forward slash (e.g. home page)
|
||||
// and we know RawTarget and Path are the same and QueryString is Empty
|
||||
RawTarget = ForwardSlash;
|
||||
Path = ForwardSlash;
|
||||
QueryString = string.Empty;
|
||||
// Clear parsedData as we won't check it if we come via this path again,
|
||||
// an setting to null is fast as it doesn't need to use a GC write barrier.
|
||||
_parsedRawTarget = _parsedPath = _parsedQueryString = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
try
|
||||
{
|
||||
var disableStringReuse = ServerOptions.DisableStringReuse;
|
||||
// Read raw target before mutating memory.
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||
Path = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
|
||||
var previousValue = _parsedRawTarget;
|
||||
if (disableStringReuse ||
|
||||
previousValue == null || previousValue.Length != target.Length ||
|
||||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
|
||||
{
|
||||
// The previous string does not match what the bytes would convert to,
|
||||
// so we will need to generate a new string.
|
||||
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
previousValue = _parsedQueryString;
|
||||
if (disableStringReuse ||
|
||||
previousValue == null || previousValue.Length != query.Length ||
|
||||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
|
||||
{
|
||||
// The previous string does not match what the bytes would convert to,
|
||||
// so we will need to generate a new string.
|
||||
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same as previous
|
||||
QueryString = _parsedQueryString;
|
||||
}
|
||||
|
||||
if (path.Length == 1)
|
||||
{
|
||||
// If path.Length == 1 it can only be a forward slash (e.g. home page)
|
||||
Path = _parsedPath = ForwardSlash;
|
||||
}
|
||||
else
|
||||
{
|
||||
Path = _parsedPath = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// As RawTarget is the same we can reuse the previous parsed values.
|
||||
RawTarget = _parsedRawTarget;
|
||||
Path = _parsedPath;
|
||||
QueryString = _parsedQueryString;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
|
|
@ -312,9 +365,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
//
|
||||
// Allowed characters in the 'host + port' section of authority.
|
||||
// See https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
var previousValue = _parsedRawTarget;
|
||||
if (ServerOptions.DisableStringReuse ||
|
||||
previousValue == null || previousValue.Length != target.Length ||
|
||||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
|
||||
{
|
||||
// The previous string does not match what the bytes would convert to,
|
||||
// so we will need to generate a new string.
|
||||
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reuse previous value
|
||||
RawTarget = _parsedRawTarget;
|
||||
}
|
||||
|
||||
Path = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
// Clear parsedData for path and queryString as we won't check it if we come via this path again,
|
||||
// an setting to null is fast as it doesn't need to use a GC write barrier.
|
||||
_parsedPath = _parsedQueryString = null;
|
||||
}
|
||||
|
||||
private void OnAsteriskFormTarget(HttpMethod method)
|
||||
|
|
@ -331,6 +402,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
RawTarget = Asterisk;
|
||||
Path = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
// Clear parsedData as we won't check it if we come via this path again,
|
||||
// an setting to null is fast as it doesn't need to use a GC write barrier.
|
||||
_parsedRawTarget = _parsedPath = _parsedQueryString = null;
|
||||
}
|
||||
|
||||
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
|
||||
|
|
@ -346,21 +420,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
// 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))
|
||||
var disableStringReuse = ServerOptions.DisableStringReuse;
|
||||
var previousValue = _parsedRawTarget;
|
||||
if (disableStringReuse ||
|
||||
previousValue == null || previousValue.Length != target.Length ||
|
||||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
|
||||
{
|
||||
ThrowRequestTargetRejected(target);
|
||||
}
|
||||
// The previous string does not match what the bytes would convert to,
|
||||
// so we will need to generate a new string.
|
||||
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
_absoluteRequestTarget = uri;
|
||||
Path = uri.LocalPath;
|
||||
// don't use uri.Query because we need the unescaped version
|
||||
QueryString = query.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 = _parsedPath = uri.LocalPath;
|
||||
// don't use uri.Query because we need the unescaped version
|
||||
previousValue = _parsedQueryString;
|
||||
if (disableStringReuse ||
|
||||
previousValue == null || previousValue.Length != query.Length ||
|
||||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
|
||||
{
|
||||
// The previous string does not match what the bytes would convert to,
|
||||
// so we will need to generate a new string.
|
||||
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
else
|
||||
{
|
||||
QueryString = _parsedQueryString;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// As RawTarget is the same we can reuse the previous values.
|
||||
RawTarget = _parsedRawTarget;
|
||||
Path = _parsedPath;
|
||||
QueryString = _parsedQueryString;
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnsureHostHeaderExists()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> Connection.OnHeader(name, value);
|
||||
|
||||
public void OnHeadersComplete()
|
||||
=> Connection.OnHeadersComplete();
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
internal abstract class HttpHeaders : IHeaderDictionary
|
||||
{
|
||||
protected long _bits = 0;
|
||||
protected long? _contentLength;
|
||||
protected bool _isReadOnly;
|
||||
protected Dictionary<string, StringValues> MaybeUnknown;
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
done = true;
|
||||
handler.OnHeadersComplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_context = context;
|
||||
|
||||
ServerOptions = ServiceContext.ServerOptions;
|
||||
HttpRequestHeaders = new HttpRequestHeaders(reuseHeaderValues: !ServerOptions.DisableStringReuse);
|
||||
HttpResponseControl = this;
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +125,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
public string Scheme { get; set; }
|
||||
public HttpMethod Method { get; set; }
|
||||
public string PathBase { get; set; }
|
||||
|
||||
protected string _parsedPath = null;
|
||||
public string Path { get; set; }
|
||||
|
||||
protected string _parsedQueryString = null;
|
||||
public string QueryString { get; set; }
|
||||
|
||||
protected string _parsedRawTarget = null;
|
||||
public string RawTarget { get; set; }
|
||||
|
||||
public string HttpVersion
|
||||
|
|
@ -275,7 +282,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
public bool HasFlushedHeaders => _requestProcessingStatus == RequestProcessingStatus.HeadersFlushed;
|
||||
|
||||
protected HttpRequestHeaders HttpRequestHeaders { get; } = new HttpRequestHeaders();
|
||||
protected HttpRequestHeaders HttpRequestHeaders { get; }
|
||||
|
||||
protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();
|
||||
|
||||
|
|
@ -492,9 +499,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
|
||||
}
|
||||
var valueString = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
|
||||
HttpRequestHeaders.Append(name, valueString);
|
||||
HttpRequestHeaders.Append(name, value);
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
{
|
||||
HttpRequestHeaders.OnHeadersComplete();
|
||||
}
|
||||
|
||||
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers.Text;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -13,6 +14,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
internal sealed partial class HttpRequestHeaders : HttpHeaders
|
||||
{
|
||||
private readonly bool _reuseHeaderValues;
|
||||
private long _previousBits = 0;
|
||||
|
||||
public HttpRequestHeaders(bool reuseHeaderValues = true)
|
||||
{
|
||||
_reuseHeaderValues = reuseHeaderValues;
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
{
|
||||
var bitsToClear = _previousBits & ~_bits;
|
||||
_previousBits = 0;
|
||||
|
||||
if (bitsToClear != 0)
|
||||
{
|
||||
// Some previous headers were not reused or overwritten.
|
||||
|
||||
// While they cannot be accessed by the current request (as they were not supplied by it)
|
||||
// there is no point in holding on to them, so clear them now,
|
||||
// to allow them to get collected by the GC.
|
||||
Clear(bitsToClear);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearFast()
|
||||
{
|
||||
if (!_reuseHeaderValues)
|
||||
{
|
||||
// If we aren't reusing headers clear them all
|
||||
Clear(_bits);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we are reusing headers, store the currently set headers for comparison later
|
||||
_previousBits = _bits;
|
||||
}
|
||||
|
||||
// Mark no headers as currently in use
|
||||
_bits = 0;
|
||||
// Clear ContentLength and any unknown headers as we will never reuse them
|
||||
_contentLength = null;
|
||||
MaybeUnknown?.Clear();
|
||||
}
|
||||
|
||||
private static long ParseContentLength(string value)
|
||||
{
|
||||
if (!HeaderUtilities.TryParseNonNegativeInt64(value, out var parsed))
|
||||
|
|
@ -23,34 +68,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
return parsed;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendContentLength(Span<byte> value)
|
||||
{
|
||||
if (_contentLength.HasValue)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
|
||||
}
|
||||
|
||||
if (!Utf8Parser.TryParse(value, out long parsed, out var consumed) ||
|
||||
parsed < 0 ||
|
||||
consumed != value.Length)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetAsciiOrUTF8StringNonNullCharacters());
|
||||
}
|
||||
|
||||
_contentLength = parsed;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void SetValueUnknown(string key, StringValues value)
|
||||
{
|
||||
Unknown[key] = value;
|
||||
}
|
||||
|
||||
public unsafe void Append(Span<byte> name, string value)
|
||||
{
|
||||
fixed (byte* namePtr = name)
|
||||
{
|
||||
Append(namePtr, name.Length, value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void AppendUnknownHeaders(byte* pKeyBytes, int keyLength, string value)
|
||||
private unsafe void AppendUnknownHeaders(Span<byte> name, string valueString)
|
||||
{
|
||||
string key = new string('\0', keyLength);
|
||||
string key = new string('\0', name.Length);
|
||||
fixed (byte* pKeyBytes = name)
|
||||
fixed (char* keyBuffer = key)
|
||||
{
|
||||
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
|
||||
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length))
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
|
||||
}
|
||||
}
|
||||
|
||||
Unknown.TryGetValue(key, out var existing);
|
||||
Unknown[key] = AppendValue(existing, value);
|
||||
Unknown[key] = AppendValue(existing, valueString);
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
public interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(Span<byte> name, Span<byte> value);
|
||||
void OnHeadersComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -1092,6 +1092,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
=> _currentHeadersStream.OnHeadersComplete();
|
||||
|
||||
private void ValidateHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1
|
||||
|
|
|
|||
|
|
@ -2,13 +2,19 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||
{
|
||||
internal class StringUtilities
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static unsafe bool TryGetAsciiString(byte* input, char* output, int count)
|
||||
{
|
||||
// Calculate end position
|
||||
|
|
@ -109,6 +115,261 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
return isValid;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, Span<byte> newValue)
|
||||
{
|
||||
// previousValue is a previously materialized string which *must* have already passed validation.
|
||||
Debug.Assert(IsValidHeaderString(previousValue));
|
||||
|
||||
// Ascii bytes => Utf-16 chars will be the same length.
|
||||
// The caller should have already compared lengths before calling this method.
|
||||
// However; let's double check, and early exit if they are not the same length.
|
||||
if (previousValue.Length != newValue.Length)
|
||||
{
|
||||
// Lengths don't match, so there cannot be an exact ascii conversion between the two.
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
// Use IntPtr values rather than int, to avoid unnecessary 32 -> 64 movs on 64-bit.
|
||||
// Unfortunately this means we also need to cast to byte* for comparisons as IntPtr doesn't
|
||||
// support operator comparisons (e.g. <=, >, etc).
|
||||
//
|
||||
// Note: Pointer comparison is unsigned, so we use the compare pattern (offset + length <= count)
|
||||
// rather than (offset <= count - length) which we'd do with signed comparison to avoid overflow.
|
||||
// This isn't problematic as we know the maximum length is max string length (from test above)
|
||||
// which is a signed value so half the size of the unsigned pointer value so we can safely add
|
||||
// a Vector<byte>.Count to it without overflowing.
|
||||
var count = (IntPtr)newValue.Length;
|
||||
var offset = (IntPtr)0;
|
||||
|
||||
// Get references to the first byte in the span, and the first char in the string.
|
||||
ref var bytes = ref MemoryMarshal.GetReference(newValue);
|
||||
ref var str = ref MemoryMarshal.GetReference(previousValue.AsSpan());
|
||||
|
||||
do
|
||||
{
|
||||
// If Vector not-accelerated or remaining less than vector size
|
||||
if (!Vector.IsHardwareAccelerated || (byte*)(offset + Vector<byte>.Count) > (byte*)count)
|
||||
{
|
||||
if (IntPtr.Size == 8) // Use Intrinsic switch for branch elimination
|
||||
{
|
||||
// 64-bit: Loop longs by default
|
||||
while ((byte*)(offset + sizeof(long)) <= (byte*)count)
|
||||
{
|
||||
if (!WidenFourAsciiBytesToUtf16AndCompareToChars(
|
||||
ref Unsafe.Add(ref str, offset),
|
||||
Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref bytes, offset))) ||
|
||||
!WidenFourAsciiBytesToUtf16AndCompareToChars(
|
||||
ref Unsafe.Add(ref str, offset + 4),
|
||||
Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref bytes, offset + 4))))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
offset += sizeof(long);
|
||||
}
|
||||
if ((byte*)(offset + sizeof(int)) <= (byte*)count)
|
||||
{
|
||||
if (!WidenFourAsciiBytesToUtf16AndCompareToChars(
|
||||
ref Unsafe.Add(ref str, offset),
|
||||
Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref bytes, offset))))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
offset += sizeof(int);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 32-bit: Loop ints by default
|
||||
while ((byte*)(offset + sizeof(int)) <= (byte*)count)
|
||||
{
|
||||
if (!WidenFourAsciiBytesToUtf16AndCompareToChars(
|
||||
ref Unsafe.Add(ref str, offset),
|
||||
Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref bytes, offset))))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
offset += sizeof(int);
|
||||
}
|
||||
}
|
||||
if ((byte*)(offset + sizeof(short)) <= (byte*)count)
|
||||
{
|
||||
if (!WidenTwoAsciiBytesToUtf16AndCompareToChars(
|
||||
ref Unsafe.Add(ref str, offset),
|
||||
Unsafe.ReadUnaligned<ushort>(ref Unsafe.Add(ref bytes, offset))))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
offset += sizeof(short);
|
||||
}
|
||||
if ((byte*)offset < (byte*)count)
|
||||
{
|
||||
var ch = (char)Unsafe.Add(ref bytes, offset);
|
||||
if (((ch & 0x80) != 0) || Unsafe.Add(ref str, offset) != ch)
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
}
|
||||
|
||||
// End of input reached, there are no inequalities via widening; so the input bytes are both ascii
|
||||
// and a match to the string if it was converted via Encoding.ASCII.GetString(...)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a comparision vector for all bits being equal
|
||||
var AllTrue = new Vector<short>(-1);
|
||||
// do/while as entry condition already checked, remaining length must be Vector<byte>.Count or larger.
|
||||
do
|
||||
{
|
||||
// Read a Vector length from the input as bytes
|
||||
var vector = Unsafe.ReadUnaligned<Vector<sbyte>>(ref Unsafe.Add(ref bytes, offset));
|
||||
if (!CheckBytesInAsciiRange(vector))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
// Widen the bytes directly to chars (ushort) as if they were ascii.
|
||||
// As widening doubles the size we get two vectors back.
|
||||
Vector.Widen(vector, out var vector0, out var vector1);
|
||||
// Read two char vectors from the string to perform the match.
|
||||
var compare0 = Unsafe.ReadUnaligned<Vector<short>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref str, offset)));
|
||||
var compare1 = Unsafe.ReadUnaligned<Vector<short>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref str, offset + Vector<ushort>.Count)));
|
||||
|
||||
// If the string is not ascii, then the widened bytes cannot match
|
||||
// as each widened byte element as chars will be in the range 0-255
|
||||
// so cannot match any higher unicode values.
|
||||
|
||||
// Compare to our all bits true comparision vector
|
||||
if (!AllTrue.Equals(
|
||||
// BitwiseAnd the two equals together
|
||||
Vector.BitwiseAnd(
|
||||
// Check equality for the two widened vectors
|
||||
Vector.Equals(compare0, vector0),
|
||||
Vector.Equals(compare1, vector1))))
|
||||
{
|
||||
goto NotEqual;
|
||||
}
|
||||
|
||||
offset += Vector<byte>.Count;
|
||||
} while ((byte*)(offset + Vector<byte>.Count) <= (byte*)count);
|
||||
|
||||
// Vector path done, loop back to do non-Vector
|
||||
// If is a exact multiple of vector size, bail now
|
||||
} while ((byte*)offset < (byte*)count);
|
||||
|
||||
// If we get here (input is exactly a multiple of Vector length) then there are no inequalities via widening;
|
||||
// so the input bytes are both ascii and a match to the string if it was converted via Encoding.ASCII.GetString(...)
|
||||
return true;
|
||||
NotEqual:
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a DWORD which represents a buffer of 4 bytes, widens the buffer into 4 WORDs and
|
||||
/// compares them to the WORD buffer with machine endianness.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool WidenFourAsciiBytesToUtf16AndCompareToChars(ref char charStart, uint value)
|
||||
{
|
||||
if (!AllBytesInUInt32AreAscii(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Bmi2.X64.IsSupported)
|
||||
{
|
||||
// BMI2 will work regardless of the processor's endianness.
|
||||
return Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref charStart)) ==
|
||||
Bmi2.X64.ParallelBitDeposit(value, 0x00FF00FF_00FF00FFul);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
return charStart == (char)(byte)value &&
|
||||
Unsafe.Add(ref charStart, 1) == (char)(byte)(value >> 8) &&
|
||||
Unsafe.Add(ref charStart, 2) == (char)(byte)(value >> 16) &&
|
||||
Unsafe.Add(ref charStart, 3) == (char)(value >> 24);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Unsafe.Add(ref charStart, 3) == (char)(byte)value &&
|
||||
Unsafe.Add(ref charStart, 2) == (char)(byte)(value >> 8) &&
|
||||
Unsafe.Add(ref charStart, 1) == (char)(byte)(value >> 16) &&
|
||||
charStart == (char)(value >> 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a WORD which represents a buffer of 2 bytes, widens the buffer into 2 WORDs and
|
||||
/// compares them to the WORD buffer with machine endianness.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool WidenTwoAsciiBytesToUtf16AndCompareToChars(ref char charStart, ushort value)
|
||||
{
|
||||
if (!AllBytesInUInt16AreAscii(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Bmi2.IsSupported)
|
||||
{
|
||||
// BMI2 will work regardless of the processor's endianness.
|
||||
return Unsafe.ReadUnaligned<uint>(ref Unsafe.As<char, byte>(ref charStart)) ==
|
||||
Bmi2.ParallelBitDeposit(value, 0x00FF00FFu);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
return charStart == (char)(byte)value &&
|
||||
Unsafe.Add(ref charStart, 1) == (char)(byte)(value >> 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Unsafe.Add(ref charStart, 1) == (char)(byte)value &&
|
||||
charStart == (char)(byte)(value >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> iff all bytes in <paramref name="value"/> are ASCII.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool AllBytesInUInt32AreAscii(uint value)
|
||||
{
|
||||
return ((value & 0x80808080u) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> iff all bytes in <paramref name="value"/> are ASCII.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool AllBytesInUInt16AreAscii(ushort value)
|
||||
{
|
||||
return ((value & 0x8080u) == 0);
|
||||
}
|
||||
|
||||
private unsafe static bool IsValidHeaderString(string value)
|
||||
{
|
||||
// Method for Debug.Assert to ensure BytesOrdinalEqualsStringAndAscii
|
||||
// is not called with an unvalidated string comparitor.
|
||||
try
|
||||
{
|
||||
if (value is null) return false;
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetByteCount(value);
|
||||
return !value.Contains('\0');
|
||||
}
|
||||
catch (DecoderFallbackException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly char[] s_encode16Chars = "0123456789ABCDEF".ToCharArray();
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
/// </remarks>
|
||||
public bool AllowSynchronousIO { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that controls whether the string values materialized
|
||||
/// will be reused across requests; if they match, or if the strings will always be reallocated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to false.
|
||||
/// </remarks>
|
||||
public bool DisableStringReuse { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Enables the Listen options callback to resolve and use services registered by the application during startup.
|
||||
/// Typically initialized by UseKestrel()"/>.
|
||||
|
|
|
|||
|
|
@ -24,16 +24,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
.Concat(byteRange)
|
||||
.ToArray();
|
||||
|
||||
var s = new Span<byte>(byteArray).GetAsciiStringNonNullCharacters();
|
||||
var span = new Span<byte>(byteArray);
|
||||
|
||||
Assert.Equal(s.Length, byteArray.Length);
|
||||
|
||||
for (var i = 1; i < byteArray.Length; i++)
|
||||
for (var i = 0; i <= byteArray.Length; i++)
|
||||
{
|
||||
var sb = (byte)s[i];
|
||||
var b = byteArray[i];
|
||||
// Test all the lengths to hit all the different length paths e.g. Vector, long, short, char
|
||||
Test(span.Slice(i));
|
||||
}
|
||||
|
||||
Assert.Equal(sb, b);
|
||||
static void Test(Span<byte> asciiBytes)
|
||||
{
|
||||
var s = asciiBytes.GetAsciiStringNonNullCharacters();
|
||||
|
||||
Assert.True(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, asciiBytes));
|
||||
Assert.Equal(s.Length, asciiBytes.Length);
|
||||
|
||||
for (var i = 0; i < asciiBytes.Length; i++)
|
||||
{
|
||||
var sb = (byte)s[i];
|
||||
var b = asciiBytes[i];
|
||||
|
||||
Assert.Equal(sb, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,8 +71,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
var byteRange = Enumerable.Range(0, 16384 + 64).Select(x => (byte)((x & 0x7f) | 0x01)).ToArray();
|
||||
var expectedByteRange = byteRange.Concat(byteRange).ToArray();
|
||||
|
||||
var s = new Span<byte>(expectedByteRange).GetAsciiStringNonNullCharacters();
|
||||
|
||||
var span = new Span<byte>(expectedByteRange);
|
||||
var s = span.GetAsciiStringNonNullCharacters();
|
||||
|
||||
Assert.Equal(expectedByteRange.Length, s.Length);
|
||||
|
||||
|
|
@ -68,8 +81,74 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
var sb = (byte)((s[i] & 0x7f) | 0x01);
|
||||
var b = expectedByteRange[i];
|
||||
}
|
||||
|
||||
Assert.Equal(sb, b);
|
||||
Assert.True(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
private void DifferentLengthsAreNotEqual()
|
||||
{
|
||||
var byteRange = Enumerable.Range(0, 4096).Select(x => (byte)((x & 0x7f) | 0x01)).ToArray();
|
||||
var expectedByteRange = byteRange.Concat(byteRange).ToArray();
|
||||
|
||||
for (var i = 1; i < byteRange.Length; i++)
|
||||
{
|
||||
var span = new Span<byte>(expectedByteRange);
|
||||
var s = span.GetAsciiStringNonNullCharacters();
|
||||
|
||||
Assert.True(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, span));
|
||||
|
||||
// One off end
|
||||
Assert.False(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, span.Slice(0, span.Length - 1)));
|
||||
Assert.False(StringUtilities.BytesOrdinalEqualsStringAndAscii(s.Substring(0, s.Length - 1), span));
|
||||
|
||||
// One off start
|
||||
Assert.False(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, span.Slice(1, span.Length - 1)));
|
||||
Assert.False(StringUtilities.BytesOrdinalEqualsStringAndAscii(s.Substring(1, s.Length - 1), span));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
private void AsciiBytesEqualAsciiStrings()
|
||||
{
|
||||
var byteRange = Enumerable.Range(1, 127).Select(x => (byte)x);
|
||||
|
||||
var byteArray = byteRange
|
||||
.Concat(byteRange)
|
||||
.Concat(byteRange)
|
||||
.Concat(byteRange)
|
||||
.Concat(byteRange)
|
||||
.Concat(byteRange)
|
||||
.ToArray();
|
||||
|
||||
var span = new Span<byte>(byteArray);
|
||||
|
||||
for (var i = 0; i <= byteArray.Length; i++)
|
||||
{
|
||||
// Test all the lengths to hit all the different length paths e.g. Vector, long, short, char
|
||||
Test(span.Slice(i));
|
||||
}
|
||||
|
||||
static void Test(Span<byte> asciiBytes)
|
||||
{
|
||||
var s = asciiBytes.GetAsciiStringNonNullCharacters();
|
||||
|
||||
// Should start as equal
|
||||
Assert.True(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, asciiBytes));
|
||||
|
||||
for (var i = 0; i < asciiBytes.Length; i++)
|
||||
{
|
||||
var b = asciiBytes[i];
|
||||
|
||||
// Change one byte, ensure is not equal
|
||||
asciiBytes[i] = (byte)(b + 1);
|
||||
Assert.False(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, asciiBytes));
|
||||
|
||||
// Change byte back for next iteration, ensure is equal again
|
||||
asciiBytes[i] = b;
|
||||
Assert.True(StringUtilities.BytesOrdinalEqualsStringAndAscii(s, asciiBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
|
||||
[Fact]
|
||||
public void DecodesIndexedHeaderField_StaticTable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -483,6 +483,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Headers[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
{
|
||||
Method = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters();
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
using static CodeGenerator.KnownHeaders;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
|
|
@ -307,8 +309,347 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
var encoding = Encoding.GetEncoding("iso-8859-1");
|
||||
var exception = Assert.Throws<BadHttpRequestException>(
|
||||
() => headers.Append(encoding.GetBytes(key), "value"));
|
||||
() => headers.Append(encoding.GetBytes(key), Encoding.ASCII.GetBytes("value")));
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseOnlyWhenAllowed(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue = "Hello";
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var prevName = ChangeNameCase(header.Name, variant: i);
|
||||
var nextName = ChangeNameCase(header.Name, variant: i + 1);
|
||||
|
||||
var values = GetHeaderValues(headers, prevName, nextName, HeaderValue, HeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.PrevHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.PrevHeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.NextHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.NextHeaderValue);
|
||||
|
||||
Assert.Equal(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
if (reuseValue)
|
||||
{
|
||||
// When materalized string is reused previous and new should be the same object
|
||||
Assert.Same(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// When materalized string is not reused previous and new should be the different objects
|
||||
Assert.NotSame(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseChangedValuesOverwrite(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue1 = "Hello1";
|
||||
const string HeaderValue2 = "Hello2";
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var prevName = ChangeNameCase(header.Name, variant: i);
|
||||
var nextName = ChangeNameCase(header.Name, variant: i + 1);
|
||||
|
||||
var values = GetHeaderValues(headers, prevName, nextName, HeaderValue1, HeaderValue2);
|
||||
|
||||
Assert.Equal(HeaderValue1, values.PrevHeaderValue);
|
||||
Assert.NotSame(HeaderValue1, values.PrevHeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue2, values.NextHeaderValue);
|
||||
Assert.NotSame(HeaderValue2, values.NextHeaderValue);
|
||||
|
||||
Assert.NotEqual(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseMissingValuesClear(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue1 = "Hello1";
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var prevName = ChangeNameCase(header.Name, variant: i);
|
||||
var nextName = ChangeNameCase(header.Name, variant: i + 1);
|
||||
|
||||
var values = GetHeaderValues(headers, prevName, nextName, HeaderValue1, nextValue: null);
|
||||
|
||||
Assert.Equal(HeaderValue1, values.PrevHeaderValue);
|
||||
Assert.NotSame(HeaderValue1, values.PrevHeaderValue);
|
||||
|
||||
Assert.Equal(string.Empty, values.NextHeaderValue);
|
||||
|
||||
Assert.NotEqual(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseNeverWhenNotAscii(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue = "Hello \u03a0";
|
||||
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var prevName = ChangeNameCase(header.Name, variant: i);
|
||||
var nextName = ChangeNameCase(header.Name, variant: i + 1);
|
||||
|
||||
var values = GetHeaderValues(headers, prevName, nextName, HeaderValue, HeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.PrevHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.PrevHeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.NextHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.NextHeaderValue);
|
||||
|
||||
Assert.Equal(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
|
||||
Assert.NotSame(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseLatin1NotConfusedForUtf16AndStillRejected(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
|
||||
var headerValue = new char[127]; // 64 + 32 + 16 + 8 + 4 + 2 + 1
|
||||
for (var i = 0; i < headerValue.Length; i++)
|
||||
{
|
||||
headerValue[i] = 'a';
|
||||
}
|
||||
|
||||
for (var i = 0; i < headerValue.Length; i++)
|
||||
{
|
||||
// Set non-ascii Latin char that is valid Utf16 when widened; but not a valid utf8 -> utf16 conversion.
|
||||
headerValue[i] = '\u00a3';
|
||||
|
||||
for (var mode = 0; mode <= 1; mode++)
|
||||
{
|
||||
string headerValueUtf16Latin1CrossOver;
|
||||
if (mode == 0)
|
||||
{
|
||||
// Full length
|
||||
headerValueUtf16Latin1CrossOver = new string(headerValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Truncated length (to ensure different paths from changing lengths in matching)
|
||||
headerValueUtf16Latin1CrossOver = new string(headerValue.AsSpan().Slice(0, i + 1));
|
||||
}
|
||||
|
||||
headers.Reset();
|
||||
|
||||
var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
|
||||
var prevSpan = Encoding.UTF8.GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
|
||||
|
||||
headers.Append(headerName, prevSpan);
|
||||
headers.OnHeadersComplete();
|
||||
var prevHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.Equal(headerValueUtf16Latin1CrossOver, prevHeaderValue);
|
||||
Assert.NotSame(headerValueUtf16Latin1CrossOver, prevHeaderValue);
|
||||
|
||||
headers.Reset();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
|
||||
var nextSpan = Encoding.GetEncoding("iso-8859-1").GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
|
||||
|
||||
Assert.False(nextSpan.SequenceEqual(Encoding.ASCII.GetBytes(headerValueUtf16Latin1CrossOver)));
|
||||
|
||||
headers.Append(headerName, nextSpan);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset back to Ascii
|
||||
headerValue[i] = 'a';
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueReuseNeverWhenUnknownHeader()
|
||||
{
|
||||
const string HeaderName = "An-Unknown-Header";
|
||||
const string HeaderValue = "Hello";
|
||||
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: true);
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var prevName = ChangeNameCase(HeaderName, variant: i);
|
||||
var nextName = ChangeNameCase(HeaderName, variant: i + 1);
|
||||
|
||||
var values = GetHeaderValues(headers, prevName, nextName, HeaderValue, HeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.PrevHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.PrevHeaderValue);
|
||||
|
||||
Assert.Equal(HeaderValue, values.NextHeaderValue);
|
||||
Assert.NotSame(HeaderValue, values.NextHeaderValue);
|
||||
|
||||
Assert.Equal(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
|
||||
Assert.NotSame(values.PrevHeaderValue, values.NextHeaderValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void ValueReuseEmptyAfterReset(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue = "Hello";
|
||||
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
|
||||
var prevSpan = Encoding.UTF8.GetBytes(HeaderValue).AsSpan();
|
||||
|
||||
headers.Append(headerName, prevSpan);
|
||||
headers.OnHeadersComplete();
|
||||
var prevHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.NotNull(prevHeaderValue);
|
||||
Assert.NotEqual(string.Empty, prevHeaderValue);
|
||||
Assert.Equal(HeaderValue, prevHeaderValue);
|
||||
Assert.NotSame(HeaderValue, prevHeaderValue);
|
||||
Assert.Single(headers);
|
||||
var count = headers.Count;
|
||||
Assert.Equal(1, count);
|
||||
|
||||
headers.Reset();
|
||||
|
||||
// Empty after reset
|
||||
var nextHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.NotNull(nextHeaderValue);
|
||||
Assert.Equal(string.Empty, nextHeaderValue);
|
||||
Assert.NotEqual(HeaderValue, nextHeaderValue);
|
||||
Assert.Empty(headers);
|
||||
count = headers.Count;
|
||||
Assert.Equal(0, count);
|
||||
|
||||
headers.OnHeadersComplete();
|
||||
|
||||
// Still empty after complete
|
||||
nextHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.NotNull(nextHeaderValue);
|
||||
Assert.Equal(string.Empty, nextHeaderValue);
|
||||
Assert.NotEqual(HeaderValue, nextHeaderValue);
|
||||
Assert.Empty(headers);
|
||||
count = headers.Count;
|
||||
Assert.Equal(0, count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(KnownRequestHeaders))]
|
||||
public void MultiValueReuseEmptyAfterReset(bool reuseValue, KnownHeader header)
|
||||
{
|
||||
const string HeaderValue1 = "Hello1";
|
||||
const string HeaderValue2 = "Hello2";
|
||||
|
||||
var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
|
||||
var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
|
||||
var prevSpan1 = Encoding.UTF8.GetBytes(HeaderValue1).AsSpan();
|
||||
var prevSpan2 = Encoding.UTF8.GetBytes(HeaderValue2).AsSpan();
|
||||
|
||||
headers.Append(headerName, prevSpan1);
|
||||
headers.Append(headerName, prevSpan2);
|
||||
headers.OnHeadersComplete();
|
||||
var prevHeaderValue = ((IHeaderDictionary)headers)[header.Name];
|
||||
|
||||
Assert.Equal(2, prevHeaderValue.Count);
|
||||
|
||||
Assert.NotEqual(string.Empty, prevHeaderValue.ToString());
|
||||
Assert.Single(headers);
|
||||
var count = headers.Count;
|
||||
Assert.Equal(1, count);
|
||||
|
||||
headers.Reset();
|
||||
|
||||
// Empty after reset
|
||||
var nextHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.NotNull(nextHeaderValue);
|
||||
Assert.Equal(string.Empty, nextHeaderValue);
|
||||
Assert.Empty(headers);
|
||||
count = headers.Count;
|
||||
Assert.Equal(0, count);
|
||||
|
||||
headers.OnHeadersComplete();
|
||||
|
||||
// Still empty after complete
|
||||
nextHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
|
||||
|
||||
Assert.NotNull(nextHeaderValue);
|
||||
Assert.Equal(string.Empty, nextHeaderValue);
|
||||
Assert.Empty(headers);
|
||||
count = headers.Count;
|
||||
Assert.Equal(0, count);
|
||||
}
|
||||
|
||||
private static (string PrevHeaderValue, string NextHeaderValue) GetHeaderValues(HttpRequestHeaders headers, string prevName, string nextName, string prevValue, string nextValue)
|
||||
{
|
||||
headers.Reset();
|
||||
var headerName = Encoding.ASCII.GetBytes(prevName).AsSpan();
|
||||
var prevSpan = Encoding.UTF8.GetBytes(prevValue).AsSpan();
|
||||
|
||||
headers.Append(headerName, prevSpan);
|
||||
headers.OnHeadersComplete();
|
||||
var prevHeaderValue = ((IHeaderDictionary)headers)[prevName].ToString();
|
||||
|
||||
headers.Reset();
|
||||
|
||||
if (nextValue != null)
|
||||
{
|
||||
headerName = Encoding.ASCII.GetBytes(prevName).AsSpan();
|
||||
var nextSpan = Encoding.UTF8.GetBytes(nextValue).AsSpan();
|
||||
headers.Append(headerName, nextSpan);
|
||||
}
|
||||
|
||||
headers.OnHeadersComplete();
|
||||
|
||||
var newHeaderValue = ((IHeaderDictionary)headers)[nextName].ToString();
|
||||
|
||||
return (prevHeaderValue, newHeaderValue);
|
||||
}
|
||||
|
||||
private static string ChangeNameCase(string name, int variant)
|
||||
{
|
||||
switch ((variant / 2) % 3)
|
||||
{
|
||||
case 0:
|
||||
return name;
|
||||
case 1:
|
||||
return name.ToLowerInvariant();
|
||||
case 2:
|
||||
return name.ToUpperInvariant();
|
||||
}
|
||||
|
||||
// Never reached
|
||||
Assert.False(true);
|
||||
return name;
|
||||
}
|
||||
|
||||
// Content-Length is numeric not a string, so we exclude it from the string reuse tests
|
||||
public static IEnumerable<object[]> KnownRequestHeaders =>
|
||||
RequestHeaders.Where(h => h.Name != "Content-Length").Select(h => new object[] { true, h }).Concat(
|
||||
RequestHeaders.Where(h => h.Name != "Content-Length").Select(h => new object[] { false, h }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<Compile Include="$(SharedSourceRoot)NullScope.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Buffers.Testing\*.cs" />
|
||||
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
|
||||
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" LinkBase="shared" />
|
||||
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,536 @@
|
|||
// 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.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||
{
|
||||
public class StartLineTests : IDisposable
|
||||
{
|
||||
private readonly static IKestrelTrace _trace = Mock.Of<IKestrelTrace>();
|
||||
|
||||
private IDuplexPipe Transport { get; }
|
||||
private MemoryPool<byte> MemoryPool { get; }
|
||||
private Http1Connection Http1Connection { get; }
|
||||
private Http1ParsingHandler ParsingHandler {get;}
|
||||
private IHttpParser<Http1ParsingHandler> Parser { get; }
|
||||
|
||||
[Fact]
|
||||
public void InOriginForm()
|
||||
{
|
||||
var rawTarget = "/path%20with%20spaces?q=123&w=xyzw1";
|
||||
var path = "/path with spaces";
|
||||
var query = "?q=123&w=xyzw1";
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InAuthorityForm()
|
||||
{
|
||||
var rawTarget = "example.com:1234";
|
||||
var path = string.Empty;
|
||||
var query = string.Empty;
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
// Empty strings, so interned and the same.
|
||||
Assert.Same(path, Http1Connection.Path);
|
||||
Assert.Same(query, Http1Connection.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InAbsoluteForm()
|
||||
{
|
||||
var rawTarget = "http://localhost/path1?q=123&w=xyzw";
|
||||
var path = "/path1";
|
||||
var query = "?q=123&w=xyzw";
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InAsteriskForm()
|
||||
{
|
||||
var rawTarget = "*";
|
||||
var path = string.Empty;
|
||||
var query = string.Empty;
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// Asterisk is interned string, so the same.
|
||||
Assert.Same(rawTarget, Http1Connection.RawTarget);
|
||||
// Empty strings, so interned and the same.
|
||||
Assert.Same(path, Http1Connection.Path);
|
||||
Assert.Same(query, Http1Connection.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentFormsWorkTogether()
|
||||
{
|
||||
// InOriginForm
|
||||
var rawTarget = "/a%20path%20with%20spaces?q=123&w=xyzw12";
|
||||
var path = "/a path with spaces";
|
||||
var query = "?q=123&w=xyzw12";
|
||||
Http1Connection.Reset();
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
|
||||
InAuthorityForm();
|
||||
|
||||
InOriginForm();
|
||||
InAbsoluteForm();
|
||||
|
||||
InOriginForm();
|
||||
InAsteriskForm();
|
||||
|
||||
InAuthorityForm();
|
||||
InAsteriskForm();
|
||||
|
||||
InAbsoluteForm();
|
||||
InAuthorityForm();
|
||||
|
||||
InAbsoluteForm();
|
||||
InAsteriskForm();
|
||||
|
||||
InAbsoluteForm();
|
||||
InAuthorityForm();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/abs/path", "/abs/path", "")]
|
||||
[InlineData("/", "/", "")]
|
||||
[InlineData("/path", "/path", "")]
|
||||
[InlineData("/?q=123&w=xyz", "/", "?q=123&w=xyz")]
|
||||
[InlineData("/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
|
||||
[InlineData("/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
|
||||
public void OriginForms(string rawTarget, string path, string query)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
var prevRequestUrl = Http1Connection.RawTarget;
|
||||
var prevPath = Http1Connection.Path;
|
||||
var prevQuery = Http1Connection.QueryString;
|
||||
|
||||
// Identical requests keep same materialized string values
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
// Parser decodes % encoding in place, so we need to recreate the ROS
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
// string.Empty is used for empty strings, so should be the same.
|
||||
Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));
|
||||
|
||||
// However, materalized strings are reused if generated for previous requests.
|
||||
|
||||
Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.Same(prevPath, Http1Connection.Path);
|
||||
Assert.Same(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
prevRequestUrl = Http1Connection.RawTarget;
|
||||
prevPath = Http1Connection.Path;
|
||||
prevQuery = Http1Connection.QueryString;
|
||||
}
|
||||
|
||||
// Different OriginForm request changes values
|
||||
|
||||
rawTarget = "/path1?q=123&w=xyzw";
|
||||
path = "/path1";
|
||||
query = "?q=123&w=xyzw";
|
||||
|
||||
Http1Connection.Reset();
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
|
||||
// Not equal previous request.
|
||||
Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.NotEqual(prevPath, Http1Connection.Path);
|
||||
Assert.NotEqual(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
DifferentFormsWorkTogether();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/abs/path", "/abs/path", "")]
|
||||
[InlineData("https://localhost/abs/path", "/abs/path", "")] // handles mismatch scheme
|
||||
[InlineData("https://localhost:22/abs/path", "/abs/path", "")] // handles mismatched ports
|
||||
[InlineData("https://differenthost/abs/path", "/abs/path", "")] // handles mismatched hostname
|
||||
[InlineData("http://localhost/", "/", "")]
|
||||
[InlineData("http://root@contoso.com/path", "/path", "")]
|
||||
[InlineData("http://root:password@contoso.com/path", "/path", "")]
|
||||
[InlineData("https://localhost/", "/", "")]
|
||||
[InlineData("http://localhost", "/", "")]
|
||||
[InlineData("http://127.0.0.1/", "/", "")]
|
||||
[InlineData("http://[::1]/", "/", "")]
|
||||
[InlineData("http://[::1]:8080/", "/", "")]
|
||||
[InlineData("http://localhost?q=123&w=xyz", "/", "?q=123&w=xyz")]
|
||||
[InlineData("http://localhost/?q=123&w=xyz", "/", "?q=123&w=xyz")]
|
||||
[InlineData("http://localhost/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
|
||||
[InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
|
||||
public void AbsoluteForms(string rawTarget, string path, string query)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
var prevRequestUrl = Http1Connection.RawTarget;
|
||||
var prevPath = Http1Connection.Path;
|
||||
var prevQuery = Http1Connection.QueryString;
|
||||
|
||||
// Identical requests keep same materialized string values
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
// string.Empty is used for empty strings, so should be the same.
|
||||
Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));
|
||||
|
||||
// However, materalized strings are reused if generated for previous requests.
|
||||
|
||||
Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.Same(prevPath, Http1Connection.Path);
|
||||
Assert.Same(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
prevRequestUrl = Http1Connection.RawTarget;
|
||||
prevPath = Http1Connection.Path;
|
||||
prevQuery = Http1Connection.QueryString;
|
||||
}
|
||||
|
||||
// Different Absolute Form request changes values
|
||||
|
||||
rawTarget = "http://localhost/path1?q=123&w=xyzw";
|
||||
path = "/path1";
|
||||
query = "?q=123&w=xyzw";
|
||||
|
||||
Http1Connection.Reset();
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
|
||||
// Not equal previous request.
|
||||
Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.NotEqual(prevPath, Http1Connection.Path);
|
||||
Assert.NotEqual(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
DifferentFormsWorkTogether();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsteriskForms()
|
||||
{
|
||||
var rawTarget = "*";
|
||||
var path = string.Empty;
|
||||
var query = string.Empty;
|
||||
|
||||
Http1Connection.Reset();
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
var prevRequestUrl = Http1Connection.RawTarget;
|
||||
var prevPath = Http1Connection.Path;
|
||||
var prevQuery = Http1Connection.QueryString;
|
||||
|
||||
// Identical requests keep same materialized string values
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// Also same as the inputs (interned strings).
|
||||
|
||||
Assert.Same(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Same(path, Http1Connection.Path);
|
||||
Assert.Same(query, Http1Connection.QueryString);
|
||||
|
||||
// Materalized strings are reused if generated for previous requests.
|
||||
|
||||
Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.Same(prevPath, Http1Connection.Path);
|
||||
Assert.Same(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
prevRequestUrl = Http1Connection.RawTarget;
|
||||
prevPath = Http1Connection.Path;
|
||||
prevQuery = Http1Connection.QueryString;
|
||||
}
|
||||
|
||||
// Different request changes values (can't be Astrisk Form as all the same)
|
||||
rawTarget = "http://localhost/path1?q=123&w=xyzw";
|
||||
path = "/path1";
|
||||
query = "?q=123&w=xyzw";
|
||||
|
||||
Http1Connection.Reset();
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
|
||||
Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.NotSame(path, Http1Connection.Path);
|
||||
Assert.NotSame(query, Http1Connection.QueryString);
|
||||
|
||||
// Not equal previous request.
|
||||
Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.NotEqual(prevPath, Http1Connection.Path);
|
||||
Assert.NotEqual(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
DifferentFormsWorkTogether();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localhost", "", "")]
|
||||
[InlineData("localhost:22", "", "")] // handles mismatched ports
|
||||
[InlineData("differenthost", "", "")] // handles mismatched hostname
|
||||
[InlineData("127.0.0.1", "", "")]
|
||||
[InlineData("[::1]", "", "")]
|
||||
[InlineData("[::1]:8080", "", "")]
|
||||
public void AuthorityForms(string rawTarget, string path, string query)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
var prevRequestUrl = Http1Connection.RawTarget;
|
||||
var prevPath = Http1Connection.Path;
|
||||
var prevQuery = Http1Connection.QueryString;
|
||||
|
||||
// Identical requests keep same materialized string values
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
Http1Connection.Reset();
|
||||
// RawTarget, Path, QueryString are null after reset
|
||||
Assert.Null(Http1Connection.RawTarget);
|
||||
Assert.Null(Http1Connection.Path);
|
||||
Assert.Null(Http1Connection.QueryString);
|
||||
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
|
||||
Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _));
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// RawTarget not the same as the input.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
// Others same as the inputs, empty strings.
|
||||
Assert.Same(path, Http1Connection.Path);
|
||||
Assert.Same(query, Http1Connection.QueryString);
|
||||
|
||||
// However, materalized strings are reused if generated for previous requests.
|
||||
|
||||
Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
|
||||
Assert.Same(prevPath, Http1Connection.Path);
|
||||
Assert.Same(prevQuery, Http1Connection.QueryString);
|
||||
|
||||
prevRequestUrl = Http1Connection.RawTarget;
|
||||
prevPath = Http1Connection.Path;
|
||||
prevQuery = Http1Connection.QueryString;
|
||||
}
|
||||
|
||||
// Different Authority Form request changes values
|
||||
rawTarget = "example.org:2345";
|
||||
path = "";
|
||||
query = "";
|
||||
|
||||
Http1Connection.Reset();
|
||||
ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
|
||||
Parser.ParseRequestLine(ParsingHandler, ros, out _, out _);
|
||||
|
||||
// Equal the inputs.
|
||||
Assert.Equal(rawTarget, Http1Connection.RawTarget);
|
||||
Assert.Equal(path, Http1Connection.Path);
|
||||
Assert.Equal(query, Http1Connection.QueryString);
|
||||
|
||||
// But not the same as the inputs.
|
||||
Assert.NotSame(rawTarget, Http1Connection.RawTarget);
|
||||
// Empty interned strings
|
||||
Assert.Same(path, Http1Connection.Path);
|
||||
Assert.Same(query, Http1Connection.QueryString);
|
||||
|
||||
// Not equal previous request.
|
||||
Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
|
||||
|
||||
DifferentFormsWorkTogether();
|
||||
}
|
||||
|
||||
public StartLineTests()
|
||||
{
|
||||
MemoryPool = KestrelMemoryPool.Create();
|
||||
var options = new PipeOptions(MemoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
|
||||
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||
Transport = pair.Transport;
|
||||
|
||||
var serviceContext = new ServiceContext
|
||||
{
|
||||
ServerOptions = new KestrelServerOptions(),
|
||||
Log = _trace,
|
||||
HttpParser = new HttpParser<Http1ParsingHandler>()
|
||||
};
|
||||
|
||||
Http1Connection = new Http1Connection(context: new HttpConnectionContext
|
||||
{
|
||||
ServiceContext = serviceContext,
|
||||
ConnectionFeatures = new FeatureCollection(),
|
||||
MemoryPool = MemoryPool,
|
||||
Transport = Transport,
|
||||
TimeoutControl = new TimeoutControl(timeoutHandler: null)
|
||||
});
|
||||
|
||||
Parser = new HttpParser<Http1ParsingHandler>(showErrorDetails: true);
|
||||
ParsingHandler = new Http1ParsingHandler(Http1Connection);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Transport.Input.Complete();
|
||||
Transport.Output.Complete();
|
||||
|
||||
MemoryPool.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> RequestHandler.Connection.OnHeader(name, value);
|
||||
|
||||
public void OnHeadersComplete()
|
||||
=> RequestHandler.Connection.OnHeadersComplete();
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
{
|
||||
}
|
||||
|
||||
private struct Adapter : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
public HttpParserBenchmark RequestHandler;
|
||||
|
|
@ -84,6 +88,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> RequestHandler.OnHeader(name, value);
|
||||
|
||||
public void OnHeadersComplete()
|
||||
=> RequestHandler.OnHeadersComplete();
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
handler.OnHeader(new Span<byte>(_hostHeaderName), new Span<byte>(_hostHeaderValue));
|
||||
handler.OnHeader(new Span<byte>(_acceptHeaderName), new Span<byte>(_acceptHeaderValue));
|
||||
handler.OnHeader(new Span<byte>(_connectionHeaderName), new Span<byte>(_connectionHeaderValue));
|
||||
handler.OnHeadersComplete();
|
||||
|
||||
consumedBytes = 0;
|
||||
consumed = buffer.Start;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,12 @@ namespace PlatformBenchmarks
|
|||
{
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_0
|
||||
public void OnHeadersComplete()
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
public async ValueTask OnReadCompletedAsync()
|
||||
{
|
||||
await Writer.FlushAsync();
|
||||
|
|
|
|||
|
|
@ -11,114 +11,11 @@ namespace CodeGenerator
|
|||
{
|
||||
public class KnownHeaders
|
||||
{
|
||||
static string Each<T>(IEnumerable<T> values, Func<T, string> formatter)
|
||||
{
|
||||
return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : "";
|
||||
}
|
||||
public readonly static KnownHeader[] RequestHeaders;
|
||||
public readonly static KnownHeader[] ResponseHeaders;
|
||||
public readonly static KnownHeader[] ResponseTrailers;
|
||||
|
||||
static string AppendSwitch(IEnumerable<IGrouping<int, KnownHeader>> values, string className) =>
|
||||
$@"var pUL = (ulong*)pUB;
|
||||
var pUI = (uint*)pUB;
|
||||
var pUS = (ushort*)pUB;
|
||||
var stringValue = new StringValues(value);
|
||||
switch (keyLength)
|
||||
{{{Each(values, byLength => $@"
|
||||
case {byLength.Key}:
|
||||
{{{Each(byLength, header => $@"
|
||||
if ({header.EqualIgnoreCaseBytes()})
|
||||
{{{(header.Identifier == "ContentLength" ? $@"
|
||||
if (_contentLength.HasValue)
|
||||
{{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
|
||||
}}
|
||||
else
|
||||
{{
|
||||
_contentLength = ParseContentLength(value);
|
||||
}}
|
||||
return;" : $@"
|
||||
if ({header.TestBit()})
|
||||
{{
|
||||
_headers._{header.Identifier} = AppendValue(_headers._{header.Identifier}, value);
|
||||
}}
|
||||
else
|
||||
{{
|
||||
{header.SetBit()};
|
||||
_headers._{header.Identifier} = stringValue;{(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
}}
|
||||
return;")}
|
||||
}}
|
||||
")}}}
|
||||
break;
|
||||
")}}}";
|
||||
|
||||
class KnownHeader
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Identifier => Name.Replace("-", "");
|
||||
|
||||
public byte[] Bytes => Encoding.ASCII.GetBytes($"\r\n{Name}: ");
|
||||
public int BytesOffset { get; set; }
|
||||
public int BytesCount { get; set; }
|
||||
public bool ExistenceCheck { get; set; }
|
||||
public bool FastCount { get; set; }
|
||||
public bool EnhancedSetter { get; set; }
|
||||
public bool PrimaryHeader { get; set; }
|
||||
public string TestBit() => $"(_bits & {"0x" + (1L << Index).ToString("x")}L) != 0";
|
||||
public string TestTempBit() => $"(tempBits & {"0x" + (1L << Index).ToString("x")}L) != 0";
|
||||
public string TestNotTempBit() => $"(tempBits & ~{"0x" + (1L << Index).ToString("x")}L) == 0";
|
||||
public string TestNotBit() => $"(_bits & {"0x" + (1L << Index).ToString("x")}L) == 0";
|
||||
public string SetBit() => $"_bits |= {"0x" + (1L << Index).ToString("x")}L";
|
||||
public string ClearBit() => $"_bits &= ~{"0x" + (1L << Index).ToString("x")}L";
|
||||
|
||||
public string EqualIgnoreCaseBytes()
|
||||
{
|
||||
var result = "";
|
||||
var delim = "";
|
||||
var index = 0;
|
||||
while (index != Name.Length)
|
||||
{
|
||||
if (Name.Length - index >= 8)
|
||||
{
|
||||
result += delim + Term(Name, index, 8, "pUL", "uL");
|
||||
index += 8;
|
||||
}
|
||||
else if (Name.Length - index >= 4)
|
||||
{
|
||||
result += delim + Term(Name, index, 4, "pUI", "u");
|
||||
index += 4;
|
||||
}
|
||||
else if (Name.Length - index >= 2)
|
||||
{
|
||||
result += delim + Term(Name, index, 2, "pUS", "u");
|
||||
index += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 1, "pUB", "u");
|
||||
index += 1;
|
||||
}
|
||||
delim = " && ";
|
||||
}
|
||||
return $"({result})";
|
||||
}
|
||||
protected string Term(string name, int offset, int count, string array, string suffix)
|
||||
{
|
||||
ulong mask = 0;
|
||||
ulong comp = 0;
|
||||
for (var scan = 0; scan < count; scan++)
|
||||
{
|
||||
var ch = (byte)name[offset + count - scan - 1];
|
||||
var isAlpha = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
|
||||
comp = (comp << 8) + (ch & (isAlpha ? 0xdfu : 0xffu));
|
||||
mask = (mask << 8) + (isAlpha ? 0xdfu : 0xffu);
|
||||
}
|
||||
return $"(({array}[{offset / count}] & {mask}{suffix}) == {comp}{suffix})";
|
||||
}
|
||||
}
|
||||
|
||||
public static string GeneratedFile()
|
||||
static KnownHeaders()
|
||||
{
|
||||
var requestPrimaryHeaders = new[]
|
||||
{
|
||||
|
|
@ -173,7 +70,7 @@ namespace CodeGenerator
|
|||
{
|
||||
"Host"
|
||||
};
|
||||
var requestHeaders = commonHeaders.Concat(new[]
|
||||
RequestHeaders = commonHeaders.Concat(new[]
|
||||
{
|
||||
"Accept",
|
||||
"Accept-Charset",
|
||||
|
|
@ -196,6 +93,8 @@ namespace CodeGenerator
|
|||
"TE",
|
||||
"Translate",
|
||||
"User-Agent",
|
||||
"DNT",
|
||||
"Upgrade-Insecure-Requests"
|
||||
})
|
||||
.Concat(corsRequestHeaders)
|
||||
.Select((header, index) => new KnownHeader
|
||||
|
|
@ -213,8 +112,6 @@ namespace CodeGenerator
|
|||
PrimaryHeader = requestPrimaryHeaders.Contains("Content-Length")
|
||||
}})
|
||||
.ToArray();
|
||||
Debug.Assert(requestHeaders.Length <= 64);
|
||||
Debug.Assert(requestHeaders.Max(x => x.Index) <= 62);
|
||||
|
||||
var responseHeadersExistence = new[]
|
||||
{
|
||||
|
|
@ -240,7 +137,7 @@ namespace CodeGenerator
|
|||
"Access-Control-Expose-Headers",
|
||||
"Access-Control-Max-Age",
|
||||
};
|
||||
var responseHeaders = commonHeaders.Concat(new[]
|
||||
ResponseHeaders = commonHeaders.Concat(new[]
|
||||
{
|
||||
"Accept-Ranges",
|
||||
"Age",
|
||||
|
|
@ -271,7 +168,7 @@ namespace CodeGenerator
|
|||
}})
|
||||
.ToArray();
|
||||
|
||||
var responseTrailers = new[]
|
||||
ResponseTrailers = new[]
|
||||
{
|
||||
"ETag",
|
||||
}
|
||||
|
|
@ -284,11 +181,359 @@ namespace CodeGenerator
|
|||
PrimaryHeader = responsePrimaryHeaders.Contains(header)
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
static string Each<T>(IEnumerable<T> values, Func<T, string> formatter)
|
||||
{
|
||||
return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : "";
|
||||
}
|
||||
|
||||
static string Each<T>(IEnumerable<T> values, Func<T, int, string> formatter)
|
||||
{
|
||||
return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : "";
|
||||
}
|
||||
|
||||
static string AppendSwitch(IEnumerable<IGrouping<int, KnownHeader>> values) =>
|
||||
$@"switch (name.Length)
|
||||
{{{Each(values, byLength => $@"
|
||||
case {byLength.Key}:{AppendSwitchSection(byLength.Key, byLength.OrderBy(h => (h.PrimaryHeader ? "_" : "") + h.Name))}
|
||||
break;")}
|
||||
}}";
|
||||
|
||||
static string AppendSwitchSection(int length, IOrderedEnumerable<KnownHeader> values)
|
||||
{
|
||||
var useVarForFirstTerm = values.Count() > 1 && values.Select(h => h.FirstNameIgnoreCaseSegment()).Distinct().Count() == 1;
|
||||
var firstTermVarExpression = values.Select(h => h.FirstNameIgnoreCaseSegment()).FirstOrDefault();
|
||||
var firstTermVar = $"firstTerm{length}";
|
||||
|
||||
var start = "";
|
||||
if (useVarForFirstTerm)
|
||||
{
|
||||
start = $@"
|
||||
var {firstTermVar} = {firstTermVarExpression};";
|
||||
}
|
||||
else
|
||||
{
|
||||
firstTermVar = "";
|
||||
}
|
||||
|
||||
var groups = values.GroupBy(header => header.EqualIgnoreCaseBytesFirstTerm());
|
||||
return start + $@"{Each(groups, (byFirstTerm, i) => $@"{(byFirstTerm.Count() == 1 ? $@"{Each(byFirstTerm, header => $@"
|
||||
{(i > 0 ? "else " : "")}if ({header.EqualIgnoreCaseBytes(firstTermVar)})
|
||||
{{{(header.Identifier == "ContentLength" ? $@"
|
||||
AppendContentLength(value);
|
||||
return;" : $@"
|
||||
flag = {header.FlagBit()};
|
||||
values = ref _headers._{header.Identifier};")}
|
||||
}}")}" : $@"
|
||||
if ({byFirstTerm.Key.Replace(firstTermVarExpression, firstTermVar)})
|
||||
{{{Each(byFirstTerm, (header, i) => $@"
|
||||
{(i > 0 ? "else " : "")}if ({header.EqualIgnoreCaseBytesSecondTermOnwards()})
|
||||
{{{(header.Identifier == "ContentLength" ? $@"
|
||||
AppendContentLength(value);
|
||||
return;" : $@"
|
||||
flag = {header.FlagBit()};
|
||||
values = ref _headers._{header.Identifier};")}
|
||||
}}")}
|
||||
}}")}")}";
|
||||
}
|
||||
|
||||
public class KnownHeader
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Identifier => Name.Replace("-", "");
|
||||
|
||||
public byte[] Bytes => Encoding.ASCII.GetBytes($"\r\n{Name}: ");
|
||||
public int BytesOffset { get; set; }
|
||||
public int BytesCount { get; set; }
|
||||
public bool ExistenceCheck { get; set; }
|
||||
public bool FastCount { get; set; }
|
||||
public bool EnhancedSetter { get; set; }
|
||||
public bool PrimaryHeader { get; set; }
|
||||
public string FlagBit() => $"{"0x" + (1L << Index).ToString("x")}L";
|
||||
public string TestBit() => $"(_bits & {"0x" + (1L << Index).ToString("x")}L) != 0";
|
||||
public string TestTempBit() => $"(tempBits & {"0x" + (1L << Index).ToString("x")}L) != 0";
|
||||
public string TestNotTempBit() => $"(tempBits & ~{"0x" + (1L << Index).ToString("x")}L) == 0";
|
||||
public string TestNotBit() => $"(_bits & {"0x" + (1L << Index).ToString("x")}L) == 0";
|
||||
public string SetBit() => $"_bits |= {"0x" + (1L << Index).ToString("x")}L";
|
||||
public string ClearBit() => $"_bits &= ~{"0x" + (1L << Index).ToString("x")}L";
|
||||
|
||||
private void GetMaskAndComp(string name, int offset, int count, out ulong mask, out ulong comp)
|
||||
{
|
||||
mask = 0;
|
||||
comp = 0;
|
||||
for (var scan = 0; scan < count; scan++)
|
||||
{
|
||||
var ch = (byte)name[offset + count - scan - 1];
|
||||
var isAlpha = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
|
||||
comp = (comp << 8) + (ch & (isAlpha ? 0xdfu : 0xffu));
|
||||
mask = (mask << 8) + (isAlpha ? 0xdfu : 0xffu);
|
||||
}
|
||||
}
|
||||
|
||||
private string NameTerm(string name, int offset, int count, string type, string suffix)
|
||||
{
|
||||
GetMaskAndComp(name, offset, count, out var mask, out var comp);
|
||||
|
||||
if (offset == 0)
|
||||
{
|
||||
if (type == "byte")
|
||||
{
|
||||
return $"(nameStart & 0x{mask:x}{suffix})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"(Unsafe.ReadUnaligned<{type}>(ref nameStart) & 0x{mask:x}{suffix})";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (type == "byte")
|
||||
{
|
||||
return $"(Unsafe.AddByteOffset(ref nameStart, (IntPtr){offset / count}) & 0x{mask:x}{suffix})";
|
||||
}
|
||||
else if ((offset / count) == 1)
|
||||
{
|
||||
return $"(Unsafe.ReadUnaligned<{type}>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof({type}))) & 0x{mask:x}{suffix})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"(Unsafe.ReadUnaligned<{type}>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)({offset / count} * sizeof({type})))) & 0x{mask:x}{suffix})";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private string EqualityTerm(string name, int offset, int count, string type, string suffix)
|
||||
{
|
||||
GetMaskAndComp(name, offset, count, out var mask, out var comp);
|
||||
|
||||
return $"0x{comp:x}{suffix}";
|
||||
}
|
||||
|
||||
private string Term(string name, int offset, int count, string type, string suffix)
|
||||
{
|
||||
GetMaskAndComp(name, offset, count, out var mask, out var comp);
|
||||
|
||||
return $"({NameTerm(name, offset, count, type, suffix)} == {EqualityTerm(name, offset, count, type, suffix)})";
|
||||
}
|
||||
|
||||
public string FirstNameIgnoreCaseSegment()
|
||||
{
|
||||
var result = "";
|
||||
if (Name.Length >= 8)
|
||||
{
|
||||
result = NameTerm(Name, 0, 8, "ulong", "uL");
|
||||
}
|
||||
else if (Name.Length >= 4)
|
||||
{
|
||||
result = NameTerm(Name, 0, 4, "uint", "u");
|
||||
}
|
||||
else if (Name.Length >= 2)
|
||||
{
|
||||
result = NameTerm(Name, 0, 2, "ushort", "u");
|
||||
}
|
||||
else
|
||||
{
|
||||
result = NameTerm(Name, 0, 1, "byte", "u");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string EqualIgnoreCaseBytes(string firstTermVar = "")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(firstTermVar))
|
||||
{
|
||||
return EqualIgnoreCaseBytesWithVar(firstTermVar);
|
||||
}
|
||||
|
||||
var result = "";
|
||||
var delim = "";
|
||||
var index = 0;
|
||||
while (index != Name.Length)
|
||||
{
|
||||
if (Name.Length - index >= 8)
|
||||
{
|
||||
result += delim + Term(Name, index, 8, "ulong", "uL");
|
||||
index += 8;
|
||||
}
|
||||
else if (Name.Length - index >= 4)
|
||||
{
|
||||
result += delim + Term(Name, index, 4, "uint", "u");
|
||||
index += 4;
|
||||
}
|
||||
else if (Name.Length - index >= 2)
|
||||
{
|
||||
result += delim + Term(Name, index, 2, "ushort", "u");
|
||||
index += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 1, "byte", "u");
|
||||
index += 1;
|
||||
}
|
||||
delim = " && ";
|
||||
}
|
||||
return result;
|
||||
|
||||
string EqualIgnoreCaseBytesWithVar(string firstTermVar)
|
||||
{
|
||||
var result = "";
|
||||
var delim = " && ";
|
||||
var index = 0;
|
||||
var isFirst = true;
|
||||
while (index != Name.Length)
|
||||
{
|
||||
if (Name.Length - index >= 8)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
result = $"({firstTermVar} == {EqualityTerm(Name, index, 8, "ulong", "uL")})";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 8, "ulong", "uL");
|
||||
}
|
||||
|
||||
index += 8;
|
||||
}
|
||||
else if (Name.Length - index >= 4)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
result = $"({firstTermVar} == {EqualityTerm(Name, index, 4, "uint", "u")})";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 4, "uint", "u");
|
||||
}
|
||||
index += 4;
|
||||
}
|
||||
else if (Name.Length - index >= 2)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
result = $"({firstTermVar} == {EqualityTerm(Name, index, 2, "ushort", "u")})";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 2, "ushort", "u");
|
||||
}
|
||||
index += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
result = $"({firstTermVar} == {EqualityTerm(Name, index, 1, "byte", "u")})";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += delim + Term(Name, index, 1, "byte", "u");
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public string EqualIgnoreCaseBytesFirstTerm()
|
||||
{
|
||||
var result = "";
|
||||
if (Name.Length >= 8)
|
||||
{
|
||||
result = Term(Name, 0, 8, "ulong", "uL");
|
||||
}
|
||||
else if (Name.Length >= 4)
|
||||
{
|
||||
result = Term(Name, 0, 4, "uint", "u");
|
||||
}
|
||||
else if (Name.Length >= 2)
|
||||
{
|
||||
result = Term(Name, 0, 2, "ushort", "u");
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Term(Name, 0, 1, "byte", "u");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string EqualIgnoreCaseBytesSecondTermOnwards()
|
||||
{
|
||||
var result = "";
|
||||
var delim = "";
|
||||
var index = 0;
|
||||
var isFirst = true;
|
||||
while (index != Name.Length)
|
||||
{
|
||||
if (Name.Length - index >= 8)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
result += delim + Term(Name, index, 8, "ulong", "uL");
|
||||
}
|
||||
|
||||
index += 8;
|
||||
}
|
||||
else if (Name.Length - index >= 4)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
result += delim + Term(Name, index, 4, "uint", "u");
|
||||
}
|
||||
index += 4;
|
||||
}
|
||||
else if (Name.Length - index >= 2)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
result += delim + Term(Name, index, 2, "ushort", "u");
|
||||
}
|
||||
index += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
result += delim + Term(Name, index, 1, "byte", "u");
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
delim = " && ";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GeneratedFile()
|
||||
{
|
||||
|
||||
var requestHeaders = RequestHeaders;
|
||||
Debug.Assert(requestHeaders.Length <= 64);
|
||||
Debug.Assert(requestHeaders.Max(x => x.Index) <= 62);
|
||||
|
||||
// 63 for responseHeaders as it steals one bit for Content-Length in CopyTo(ref MemoryPoolIterator output)
|
||||
var responseHeaders = ResponseHeaders;
|
||||
Debug.Assert(responseHeaders.Length <= 63);
|
||||
Debug.Assert(responseHeaders.Count(x => x.Index == 63) == 1);
|
||||
|
||||
var responseTrailers = ResponseTrailers;
|
||||
|
||||
var loops = new[]
|
||||
{
|
||||
new
|
||||
|
|
@ -331,8 +576,10 @@ using System.Collections.Generic;
|
|||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{{
|
||||
|
|
@ -345,8 +592,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{Each(loop.Bytes, b => $"{b},")}
|
||||
}};"
|
||||
: "")}
|
||||
|
||||
private long _bits = 0;
|
||||
private HeaderReferences _headers;
|
||||
{Each(loop.Headers.Where(header => header.ExistenceCheck), header => $@"
|
||||
public bool Has{header.Identifier} => {header.TestBit()};")}
|
||||
|
|
@ -510,8 +755,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
return MaybeUnknown?.Remove(key) ?? false;
|
||||
}}
|
||||
|
||||
protected override void ClearFast()
|
||||
{(loop.ClassName != "HttpRequestHeaders" ?
|
||||
$@" protected override void ClearFast()
|
||||
{{
|
||||
MaybeUnknown?.Clear();
|
||||
_contentLength = null;
|
||||
|
|
@ -534,7 +779,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}}
|
||||
")}
|
||||
}}
|
||||
|
||||
" :
|
||||
$@" private void Clear(long bitsToClear)
|
||||
{{
|
||||
var tempBits = bitsToClear;
|
||||
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength").OrderBy(h => !h.PrimaryHeader), header => $@"
|
||||
if ({header.TestTempBit()})
|
||||
{{
|
||||
_headers._{header.Identifier} = default(StringValues);
|
||||
if({header.TestNotTempBit()})
|
||||
{{
|
||||
return;
|
||||
}}
|
||||
tempBits &= ~{"0x" + (1L << header.Index).ToString("x")}L;
|
||||
}}
|
||||
")}
|
||||
}}
|
||||
")}
|
||||
protected override bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
|
||||
{{
|
||||
if (arrayIndex < 0)
|
||||
|
|
@ -625,22 +886,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}}
|
||||
}}
|
||||
}} while (tempBits != 0);
|
||||
}}" : "")}
|
||||
{(loop.ClassName == "HttpRequestHeaders" ? $@"
|
||||
public unsafe void Append(byte* pKeyBytes, int keyLength, string value)
|
||||
}}" : "")}{(loop.ClassName == "HttpRequestHeaders" ? $@"
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public unsafe void Append(Span<byte> name, Span<byte> value)
|
||||
{{
|
||||
var pUB = pKeyBytes;
|
||||
{AppendSwitch(loop.Headers.Where(h => h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName)}
|
||||
ref byte nameStart = ref MemoryMarshal.GetReference(name);
|
||||
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
|
||||
var flag = 0L;
|
||||
|
||||
AppendNonPrimaryHeaders(pKeyBytes, keyLength, value);
|
||||
}}
|
||||
// Does the name matched any ""known"" headers
|
||||
{AppendSwitch(loop.Headers.GroupBy(x => x.Name.Length).OrderBy(x => x.Key))}
|
||||
|
||||
private unsafe void AppendNonPrimaryHeaders(byte* pKeyBytes, int keyLength, string value)
|
||||
{{
|
||||
var pUB = pKeyBytes;
|
||||
{AppendSwitch(loop.Headers.Where(h => !h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName)}
|
||||
if (flag != 0)
|
||||
{{
|
||||
// Matched a known header
|
||||
if ((_previousBits & flag) != 0)
|
||||
{{
|
||||
// Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header
|
||||
_previousBits ^= flag;
|
||||
|
||||
AppendUnknownHeaders(pKeyBytes, keyLength, value);
|
||||
// We will only reuse this header if there was only one previous header
|
||||
if (values.Count == 1)
|
||||
{{
|
||||
var previousValue = values.ToString();
|
||||
// Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same.
|
||||
// We do not consider Utf8 headers for reuse.
|
||||
if (previousValue.Length == value.Length &&
|
||||
StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value))
|
||||
{{
|
||||
// The previous string matches what the bytes would convert to, so we will just use that one.
|
||||
_bits |= flag;
|
||||
return;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
// We didn't have a previous matching header value, or have already added a header, so get the string for this value.
|
||||
var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
if ((_bits & flag) == 0)
|
||||
{{
|
||||
// We didn't already have a header set, so add a new one.
|
||||
_bits |= flag;
|
||||
values = new StringValues(valueStr);
|
||||
}}
|
||||
else
|
||||
{{
|
||||
// We already had a header set, so concatenate the new one.
|
||||
values = AppendValue(values, valueStr);
|
||||
}}
|
||||
}}
|
||||
else
|
||||
{{
|
||||
// The header was not one of the ""known"" headers.
|
||||
// Convert value to string first, because passing two spans causes 8 bytes stack zeroing in
|
||||
// this method with rep stosd, which is slower than necessary.
|
||||
var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
AppendUnknownHeaders(name, valueStr);
|
||||
}}
|
||||
}}" : "")}
|
||||
|
||||
private struct HeaderReferences
|
||||
|
|
@ -406,6 +406,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
|
||||
protected void CreateConnection()
|
||||
{
|
||||
var limits = _serviceContext.ServerOptions.Limits;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
<IsTestAssetProject>true</IsTestAssetProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Features" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue