Implement IHeaderDictionary.ContentLength

This commit is contained in:
Ben Adams 2017-01-24 22:55:48 +00:00 committed by Chris R
parent 815db3d210
commit ecca980c91
18 changed files with 3683 additions and 5195 deletions

View File

@ -43,6 +43,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
case RequestRejectionReason.MalformedRequestInvalidHeaders:
ex = new BadHttpRequestException("Malformed request: invalid headers.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.MultipleContentLengths:
ex = new BadHttpRequestException("Multiple Content-Length headers.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.UnexpectedEndOfRequestContent:
ex = new BadHttpRequestException("Unexpected end of request content.", StatusCodes.Status400BadRequest);
break;

View File

@ -41,7 +41,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
private static readonly byte[] _bytesContentLengthZero = Encoding.ASCII.GetBytes("\r\nContent-Length: 0");
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
@ -657,12 +656,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (responseHeaders != null &&
!responseHeaders.HasTransferEncoding &&
responseHeaders.HasContentLength &&
_responseBytesWritten + count > responseHeaders.HeaderContentLengthValue.Value)
responseHeaders.ContentLength.HasValue &&
_responseBytesWritten + count > responseHeaders.ContentLength.Value)
{
_keepAlive = false;
throw new InvalidOperationException(
$"Response Content-Length mismatch: too many bytes written ({_responseBytesWritten + count} of {responseHeaders.HeaderContentLengthValue.Value}).");
$"Response Content-Length mismatch: too many bytes written ({_responseBytesWritten + count} of {responseHeaders.ContentLength.Value}).");
}
_responseBytesWritten += count;
@ -679,8 +678,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// Called after VerifyAndUpdateWrite(), so _responseBytesWritten has already been updated.
if (responseHeaders != null &&
!responseHeaders.HasTransferEncoding &&
responseHeaders.HasContentLength &&
_responseBytesWritten == responseHeaders.HeaderContentLengthValue.Value)
responseHeaders.ContentLength.HasValue &&
_responseBytesWritten == responseHeaders.ContentLength.Value)
{
_abortedCts = null;
}
@ -692,8 +691,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (!HttpMethods.IsHead(Method) &&
!responseHeaders.HasTransferEncoding &&
responseHeaders.HeaderContentLengthValue.HasValue &&
_responseBytesWritten < responseHeaders.HeaderContentLengthValue.Value)
responseHeaders.ContentLength.HasValue &&
_responseBytesWritten < responseHeaders.ContentLength.Value)
{
// We need to close the connection if any bytes were written since the client
// cannot be certain of how many bytes it will receive.
@ -703,7 +702,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
ReportApplicationError(new InvalidOperationException(
$"Response Content-Length mismatch: too few bytes written ({_responseBytesWritten} of {responseHeaders.HeaderContentLengthValue.Value})."));
$"Response Content-Length mismatch: too few bytes written ({_responseBytesWritten} of {responseHeaders.ContentLength.Value})."));
}
}
@ -920,13 +919,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// automatically for HEAD requests or 204, 205, 304 responses.
if (_canHaveBody)
{
if (!hasTransferEncoding && !responseHeaders.HasContentLength)
if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
{
if (appCompleted && StatusCode != StatusCodes.Status101SwitchingProtocols)
{
// Since the app has completed and we are only now generating
// the headers we can safely set the Content-Length to 0.
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
responseHeaders.ContentLength = 0;
}
else
{
@ -1468,7 +1467,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
responseHeaders.ContentLength = 0;
if (ServerOptions.AddServerHeader)
{

View File

@ -5,19 +5,32 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public abstract class FrameHeaders : IHeaderDictionary
{
protected long? _contentLength;
protected bool _isReadOnly;
protected Dictionary<string, StringValues> MaybeUnknown;
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
public long? ContentLength
{
get { return _contentLength; }
set
{
if (value.HasValue && value.Value < 0)
{
ThrowInvalidContentLengthException(value.Value);
}
_contentLength = value;
}
}
StringValues IHeaderDictionary.this[string key]
{
get
@ -41,7 +54,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
get
{
// Unlike the IHeaderDictionary version, this getter will throw a KeyNotFoundException.
return GetValueFast(key);
StringValues value;
if (!TryGetValueFast(key, out value))
{
ThrowKeyNotFoundException();
}
return value;
}
set
{
@ -88,6 +106,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ClearFast();
}
[MethodImpl(MethodImplOptions.NoInlining)]
protected static StringValues AppendValue(StringValues existing, string append)
{
return StringValues.Concat(existing, append);
@ -112,16 +131,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected virtual int GetCountFast()
{ throw new NotImplementedException(); }
protected virtual StringValues GetValueFast(string key)
{ throw new NotImplementedException(); }
protected virtual bool TryGetValueFast(string key, out StringValues value)
{ throw new NotImplementedException(); }
protected virtual void SetValueFast(string key, StringValues value)
{ throw new NotImplementedException(); }
protected virtual void AddValueFast(string key, StringValues value)
protected virtual bool AddValueFast(string key, StringValues value)
{ throw new NotImplementedException(); }
protected virtual bool RemoveFast(string key)
@ -130,7 +146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected virtual void ClearFast()
{ throw new NotImplementedException(); }
protected virtual void CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
protected virtual bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
{ throw new NotImplementedException(); }
protected virtual IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
@ -147,7 +163,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
ThrowHeadersReadOnlyException();
}
AddValueFast(key, value);
if (!AddValueFast(key, value))
{
ThrowDuplicateKeyException();
}
}
void ICollection<KeyValuePair<string, StringValues>>.Clear()
@ -175,7 +195,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
void ICollection<KeyValuePair<string, StringValues>>.CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex)
{
CopyToFast(array, arrayIndex);
if (!CopyToFast(array, arrayIndex))
{
ThrowArgumentException();
}
}
IEnumerator IEnumerable.GetEnumerator()
@ -235,17 +258,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
public static long ParseContentLength(StringValues value)
{
long parsed;
if (!HeaderUtilities.TryParseInt64(value.ToString(), out parsed))
{
ThrowInvalidContentLengthException(value);
}
return parsed;
}
public static unsafe ConnectionOptions ParseConnection(StringValues connection)
{
var connectionOptions = ConnectionOptions.None;
@ -412,9 +424,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return transferEncodingOptions;
}
private static void ThrowInvalidContentLengthException(string value)
private static void ThrowInvalidContentLengthException(long value)
{
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");
throw new ArgumentOutOfRangeException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");
}
private static void ThrowInvalidHeaderCharacter(char ch)

View File

@ -3,12 +3,59 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public partial class FrameRequestHeaders : FrameHeaders
{
private static long ParseContentLength(string value)
{
long parsed;
if (!HeaderUtilities.TryParseInt64(value, out parsed))
{
ThrowInvalidContentLengthException(value);
}
return parsed;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void SetValueUnknown(string key, StringValues value)
{
Unknown[key] = value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe void AppendUnknownHeaders(byte* pKeyBytes, int keyLength, string value)
{
string key = new string('\0', keyLength);
fixed (char* keyBuffer = key)
{
if (!AsciiUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
{
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidCharactersInHeaderName);
}
}
StringValues existing;
Unknown.TryGetValue(key, out existing);
Unknown[key] = AppendValue(existing, value);
}
private static void ThrowInvalidContentLengthException(string value)
{
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidContentLength, value);
}
private static void ThrowMultipleContentLengthsException()
{
throw BadHttpRequestException.GetException(RequestRejectionReason.MultipleContentLengths);
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);

View File

@ -1,10 +1,13 @@
// 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.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
@ -13,20 +16,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private static readonly byte[] _CrLf = new[] { (byte)'\r', (byte)'\n' };
private static readonly byte[] _colonSpace = new[] { (byte)':', (byte)' ' };
private long? _contentLength;
public bool HasConnection => HeaderConnection.Count != 0;
public bool HasTransferEncoding => HeaderTransferEncoding.Count != 0;
public bool HasContentLength => HeaderContentLength.Count != 0;
public bool HasServer => HeaderServer.Count != 0;
public bool HasDate => HeaderDate.Count != 0;
public long? HeaderContentLengthValue => _contentLength;
public Enumerator GetEnumerator()
{
return new Enumerator(this);
@ -58,6 +55,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
private static long ParseContentLength(string value)
{
long parsed;
if (!HeaderUtilities.TryParseInt64(value, out parsed))
{
ThrowInvalidContentLengthException(value);
}
return parsed;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void SetValueUnknown(string key, StringValues value)
{
ValidateHeaderCharacters(key);
Unknown[key] = value;
}
private static void ThrowInvalidContentLengthException(string value)
{
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");
}
public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>
{
private readonly FrameResponseHeaders _collection;
@ -92,5 +112,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_state = 0;
}
}
}
}

View File

@ -8,12 +8,14 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public abstract class MessageBody
{
private static readonly MessageBody _zeroContentLengthClose = new ForZeroContentLength(keepAlive: false);
private static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
private readonly Frame _context;
private bool _send100Continue = true;
@ -266,18 +268,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return new ForChunkedEncoding(keepAlive, headers, context);
}
var unparsedContentLength = headers.HeaderContentLength;
if (unparsedContentLength.Count > 0)
if (headers.ContentLength.HasValue)
{
try
var contentLength = headers.ContentLength.Value;
if (contentLength == 0)
{
var contentLength = FrameHeaders.ParseContentLength(unparsedContentLength);
return new ForContentLength(keepAlive, contentLength, context);
}
catch (InvalidOperationException)
{
context.RejectRequest(RequestRejectionReason.InvalidContentLength, unparsedContentLength);
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
}
return new ForContentLength(keepAlive, contentLength, context);
}
// Avoid slowing down most common case
@ -292,7 +291,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
return new ForContentLength(keepAlive, 0, context);
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
}
private class ForRemainingData : MessageBody
@ -309,6 +308,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
private class ForZeroContentLength : MessageBody
{
public ForZeroContentLength(bool keepAlive)
: base(null)
{
RequestKeepAlive = keepAlive;
}
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)
{
return new ValueTask<ArraySegment<byte>>();
}
protected override void OnConsumedBytes(int count)
{
if (count > 0)
{
throw new InvalidDataException("Consuming non-existent data");
}
}
}
private class ForContentLength : MessageBody
{
private readonly long _contentLength;

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
InvalidRequestLine,
MalformedRequestInvalidHeaders,
InvalidContentLength,
MultipleContentLengths,
UnexpectedEndOfRequestContent,
BadChunkSuffix,
BadChunkSizeData,

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{
public struct MemoryPoolIterator
{
private const int _maxULongByteLength = 20;
private const ulong _xorPowerOfTwoToHighByte = (0x07ul |
0x06ul << 8 |
0x05ul << 16 |
@ -23,6 +24,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
private static readonly int _vectorSpan = Vector<byte>.Count;
[ThreadStatic]
private static byte[] _numericBytesScratch;
private MemoryPoolBlock _block;
private int _index;
@ -1082,6 +1086,101 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
_index = blockIndex;
}
private static byte[] NumericBytesScratch => _numericBytesScratch ?? CreateNumericBytesScratch();
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] CreateNumericBytesScratch()
{
var bytes = new byte[_maxULongByteLength];
_numericBytesScratch = bytes;
return bytes;
}
public unsafe void CopyFromNumeric(ulong value)
{
const byte AsciiDigitStart = (byte)'0';
var block = _block;
if (block == null)
{
return;
}
var blockIndex = _index;
var bytesLeftInBlock = block.Data.Offset + block.Data.Count - blockIndex;
var start = block.DataFixedPtr + blockIndex;
if (value < 10)
{
if (bytesLeftInBlock < 1)
{
CopyFromNumericOverflow(value);
return;
}
_index = blockIndex + 1;
block.End = blockIndex + 1;
*(start) = (byte)(((uint)value) + AsciiDigitStart);
}
else if (value < 100)
{
if (bytesLeftInBlock < 2)
{
CopyFromNumericOverflow(value);
return;
}
_index = blockIndex + 2;
block.End = blockIndex + 2;
var val = (uint)value;
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(tens + AsciiDigitStart);
*(start + 1) = (byte)(val - (tens * 10) + AsciiDigitStart);
}
else if (value < 1000)
{
if (bytesLeftInBlock < 3)
{
CopyFromNumericOverflow(value);
return;
}
_index = blockIndex + 3;
block.End = blockIndex + 3;
var val = (uint)value;
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(digit0 + AsciiDigitStart);
*(start + 1) = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
*(start + 2) = (byte)(val - (digits01 * 10) + AsciiDigitStart);
}
else
{
CopyFromNumericOverflow(value);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe void CopyFromNumericOverflow(ulong value)
{
const byte AsciiDigitStart = (byte)'0';
var position = _maxULongByteLength;
var byteBuffer = NumericBytesScratch;
do
{
// Consider using Math.DivRem() if available
var quotient = value / 10;
byteBuffer[--position] = (byte)(AsciiDigitStart + (value - quotient * 10)); // 0x30 = '0'
value = quotient;
}
while (value != 0);
CopyFrom(byteBuffer, position, _maxULongByteLength - position);
}
public unsafe string GetAsciiString(ref MemoryPoolIterator end)
{
var block = _block;
@ -1253,6 +1352,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
}
}
}

View File

@ -0,0 +1,209 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class ResponseHeaders
{
private const int InnerLoopCount = 512;
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager();
private static readonly MemoryPool _memoryPool = new MemoryPool();
private FrameResponseHeaders _responseHeadersDirect;
private HttpResponse _response;
[Params("ContentLengthNumeric", "ContentLengthString", "Plaintext", "Common", "Unknown")]
public string Type { get; set; }
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
public void SetHeaders()
{
switch (Type)
{
case "ContentLengthNumeric":
ContentLengthNumeric(InnerLoopCount);
break;
case "ContentLengthString":
ContentLengthString(InnerLoopCount);
break;
case "Plaintext":
Plaintext(InnerLoopCount);
break;
case "Common":
Common(InnerLoopCount);
break;
case "Unknown":
Unknown(InnerLoopCount);
break;
}
}
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
public void OutputHeaders()
{
for (var i = 0; i < InnerLoopCount; i++)
{
var block = _memoryPool.Lease();
var iter = new MemoryPoolIterator(block);
_responseHeadersDirect.CopyTo(ref iter);
ReturnBlocks(block);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ContentLengthNumeric(int count)
{
for (var i = 0; i < count; i++)
{
_responseHeadersDirect.Reset();
_response.ContentLength = 0;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ContentLengthString(int count)
{
for (var i = 0; i < count; i++)
{
_responseHeadersDirect.Reset();
_response.Headers["Content-Length"] = "0";
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Plaintext(int count)
{
for (var i = 0; i < count; i++)
{
_responseHeadersDirect.Reset();
_response.StatusCode = 200;
_response.ContentType = "text/plain";
_response.ContentLength = 13;
var dateHeaderValues = _dateHeaderValueManager.GetDateHeaderValues();
_responseHeadersDirect.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
_responseHeadersDirect.SetRawServer("Kestrel", _bytesServer);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Common(int count)
{
for (var i = 0; i < count; i++)
{
_responseHeadersDirect.Reset();
_response.StatusCode = 200;
_response.ContentType = "text/css";
_response.ContentLength = 421;
var headers = _response.Headers;
headers["Connection"] = "Close";
headers["Cache-Control"] = "public, max-age=30672000";
headers["Vary"] = "Accept-Encoding";
headers["Content-Encoding"] = "gzip";
headers["Expires"] = "Fri, 12 Jan 2018 22:01:55 GMT";
headers["Last-Modified"] = "Wed, 22 Jun 2016 20:08:29 GMT";
headers["Set-Cookie"] = "prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric";
headers["ETag"] = "\"54ef7954-1078\"";
headers["Transfer-Encoding"] = "chunked";
headers["Content-Language"] = "en-gb";
headers["Upgrade"] = "websocket";
headers["Via"] = "1.1 varnish";
headers["Access-Control-Allow-Origin"] = "*";
headers["Access-Control-Allow-credentials"] = "true";
headers["Access-Control-Expose-Headers"] = "Client-Protocol, Content-Length, Content-Type, X-Bandwidth-Est, X-Bandwidth-Est2, X-Bandwidth-Est-Comp, X-Bandwidth-Avg, X-Walltime-Ms, X-Sequence-Num";
var dateHeaderValues = _dateHeaderValueManager.GetDateHeaderValues();
_responseHeadersDirect.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
_responseHeadersDirect.SetRawServer("Kestrel", _bytesServer);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Unknown(int count)
{
for (var i = 0; i < count; i++)
{
_responseHeadersDirect.Reset();
_response.StatusCode = 200;
_response.ContentType = "text/plain";
_response.ContentLength = 13;
var headers = _response.Headers;
headers["Link"] = "<https://www.gravatar.com/avatar/6ae816bfaad7bbc58b17fac49ef5cced?d=404&s=250>; rel=\"canonical\"";
headers["X-Ua-Compatible"] = "IE=Edge";
headers["X-Powered-By"] = "ASP.NET";
headers["X-Content-Type-Options"] = "nosniff";
headers["X-Xss-Protection"] = "1; mode=block";
headers["X-Frame-Options"] = "SAMEORIGIN";
headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload";
headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self' cdnjs.cloudflare.com code.jquery.com scotthelme.disqus.com a.disquscdn.com www.google-analytics.com go.disqus.com platform.twitter.com cdn.syndication.twimg.com; style-src 'self' a.disquscdn.com fonts.googleapis.com cdnjs.cloudflare.com platform.twitter.com; img-src 'self' data: www.gravatar.com www.google-analytics.com links.services.disqus.com referrer.disqus.com a.disquscdn.com cdn.syndication.twimg.com syndication.twitter.com pbs.twimg.com platform.twitter.com abs.twimg.com; child-src fusiontables.googleusercontent.com fusiontables.google.com www.google.com disqus.com www.youtube.com syndication.twitter.com platform.twitter.com; frame-src fusiontables.googleusercontent.com fusiontables.google.com www.google.com disqus.com www.youtube.com syndication.twitter.com platform.twitter.com; connect-src 'self' links.services.disqus.com; font-src 'self' cdnjs.cloudflare.com fonts.gstatic.com fonts.googleapis.com; form-action 'self'; upgrade-insecure-requests;";
var dateHeaderValues = _dateHeaderValueManager.GetDateHeaderValues();
_responseHeadersDirect.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
_responseHeadersDirect.SetRawServer("Kestrel", _bytesServer);
}
}
[Setup]
public void Setup()
{
var connectionContext = new MockConnection(new KestrelServerOptions());
var frame = new Frame<object>(application: null, context: connectionContext);
frame.Reset();
frame.InitializeHeaders();
_responseHeadersDirect = (FrameResponseHeaders)frame.ResponseHeaders;
var context = new DefaultHttpContext(frame);
_response = new DefaultHttpResponse(context);
switch (Type)
{
case "ContentLengthNumeric":
ContentLengthNumeric(1);
break;
case "ContentLengthString":
ContentLengthString(1);
break;
case "Plaintext":
Plaintext(1);
break;
case "Common":
Common(1);
break;
case "Unknown":
Unknown(1);
break;
}
}
private static void ReturnBlocks(MemoryPoolBlock block)
{
while (block != null)
{
var returningBlock = block;
block = returningBlock.Next;
returningBlock.Pool.Return(returningBlock);
}
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public string GetValue(Summary summary, Benchmark benchmark)
{
var totalNanos = summary.Reports.First(r => r.Benchmark == benchmark).ResultStatistics.Mean;
var totalNanos = summary.Reports.First(r => r.Benchmark == benchmark)?.ResultStatistics?.Mean ?? 0;
// Make sure we don't divide by zero!!
return Math.Abs(totalNanos) > 0.0 ? (NanosPerSecond / totalNanos).ToString("N2") : "N/A";
}

View File

@ -202,6 +202,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
[Theory]
[InlineData("NaN")]
[InlineData("-1")]
public async Task BadRequestIfContentLengthInvalid(string contentLength)
{
using (var server = new TestServer(context => { return Task.FromResult(0); }))
{
using (var connection = server.CreateConnection())
{
await connection.Send($"GET / HTTP/1.1\r\nContent-Length: {contentLength}\r\n\r\n");
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue);
}
}
}
private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue)
{
await connection.ReceiveForcedEnd(

View File

@ -32,14 +32,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"Server: Kestrel",
"Content-Length: 0",
"",
"HTTP/1.1 200 OK",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"Server: Kestrel",
"Content-Length: 0",
"",
"");
}

View File

@ -1,8 +1,11 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
@ -223,5 +226,65 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var transferEncodingOptions = FrameHeaders.GetFinalTransferCoding(transferEncoding);
Assert.Equal(expectedTransferEncodingOptions, transferEncodingOptions);
}
[Fact]
public void ValidContentLengthsAccepted()
{
ValidContentLengthsAccepted(new FrameRequestHeaders());
ValidContentLengthsAccepted(new FrameResponseHeaders());
}
private static void ValidContentLengthsAccepted(FrameHeaders frameHeaders)
{
IDictionary<string, StringValues> headers = frameHeaders;
StringValues value;
Assert.False(headers.TryGetValue("Content-Length", out value));
Assert.Equal(null, frameHeaders.ContentLength);
Assert.False(frameHeaders.ContentLength.HasValue);
frameHeaders.ContentLength = 1;
Assert.True(headers.TryGetValue("Content-Length", out value));
Assert.Equal("1", value[0]);
Assert.Equal(1, frameHeaders.ContentLength);
Assert.True(frameHeaders.ContentLength.HasValue);
frameHeaders.ContentLength = long.MaxValue;
Assert.True(headers.TryGetValue("Content-Length", out value));
Assert.Equal(HeaderUtilities.FormatInt64(long.MaxValue), value[0]);
Assert.Equal(long.MaxValue, frameHeaders.ContentLength);
Assert.True(frameHeaders.ContentLength.HasValue);
frameHeaders.ContentLength = null;
Assert.False(headers.TryGetValue("Content-Length", out value));
Assert.Equal(null, frameHeaders.ContentLength);
Assert.False(frameHeaders.ContentLength.HasValue);
}
[Fact]
public void InvalidContentLengthsRejected()
{
InvalidContentLengthsRejected(new FrameRequestHeaders());
InvalidContentLengthsRejected(new FrameResponseHeaders());
}
private static void InvalidContentLengthsRejected(FrameHeaders frameHeaders)
{
IDictionary<string, StringValues> headers = frameHeaders;
StringValues value;
Assert.False(headers.TryGetValue("Content-Length", out value));
Assert.Equal(null, frameHeaders.ContentLength);
Assert.False(frameHeaders.ContentLength.HasValue);
Assert.Throws<ArgumentOutOfRangeException>(() => frameHeaders.ContentLength = -1);
Assert.Throws<ArgumentOutOfRangeException>(() => frameHeaders.ContentLength = long.MinValue);
Assert.False(headers.TryGetValue("Content-Length", out value));
Assert.Equal(null, frameHeaders.ContentLength);
Assert.False(frameHeaders.ContentLength.HasValue);
}
}
}

View File

@ -41,10 +41,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
headers["host"] = new[] { "value" };
headers["content-length"] = new[] { "0" };
Assert.NotNull(headers["host"]);
Assert.NotNull(headers["content-length"]);
Assert.Equal(1, headers["host"].Count);
Assert.Equal(1, headers["content-length"].Count);
Assert.Equal("value", headers["host"][0]);
Assert.Equal("0", headers["content-length"][0]);
}
[Fact]
@ -54,8 +58,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
headers["host"] = new[] { "value" };
headers["custom"] = new[] { "value" };
headers["Content-Length"] = new[] { "0" };
Assert.Equal(2, headers.Count);
Assert.Equal(3, headers.Count);
}
[Fact]
@ -66,14 +71,22 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
StringValues value;
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
headers["host"] = new[] { "value" };
Assert.True(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
headers["custom"] = new[] { "value" };
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
headers["Content-Length"] = new[] { "0" };
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
}
[Fact]
@ -83,6 +96,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Throws<KeyNotFoundException>(() => headers["custom"]);
Assert.Throws<KeyNotFoundException>(() => headers["host"]);
Assert.Throws<KeyNotFoundException>(() => headers["Content-Length"]);
}
[Fact]
@ -90,14 +104,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
var v1 = new[] { "localhost" };
var v2 = new[] { "value" };
var v2 = new[] { "0" };
var v3 = new[] { "value" };
headers["host"] = v1;
headers["custom"] = v2;
headers["Content-Length"] = v2;
headers["custom"] = v3;
Assert.Equal(
new[] {
new KeyValuePair<string, StringValues>("Host", v1),
new KeyValuePair<string, StringValues>("custom", v2),
new KeyValuePair<string, StringValues>("Content-Length", v2),
new KeyValuePair<string, StringValues>("custom", v3),
},
headers);
}
@ -107,16 +124,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
StringValues v1 = new[] { "localhost" };
StringValues v2 = new[] { "value" };
StringValues v2 = new[] { "0" };
StringValues v3 = new[] { "value" };
headers["host"] = v1;
headers["custom"] = v2;
headers["Content-Length"] = v2;
headers["custom"] = v3;
Assert.Equal<string>(
new[] { "Host", "custom" },
new[] { "Host", "Content-Length", "custom" },
headers.Keys);
Assert.Equal<StringValues>(
new[] { v1, v2 },
new[] { v1, v2, v3 },
headers.Values);
}
@ -126,29 +145,50 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
var kv1 = new KeyValuePair<string, StringValues>("host", new[] { "localhost" });
var kv2 = new KeyValuePair<string, StringValues>("custom", new[] { "value" });
var kv3 = new KeyValuePair<string, StringValues>("Content-Length", new[] { "0" });
var kv1b = new KeyValuePair<string, StringValues>("host", new[] { "not-localhost" });
var kv2b = new KeyValuePair<string, StringValues>("custom", new[] { "not-value" });
var kv3b = new KeyValuePair<string, StringValues>("Content-Length", new[] { "1" });
Assert.False(headers.ContainsKey("host"));
Assert.False(headers.ContainsKey("custom"));
Assert.False(headers.ContainsKey("Content-Length"));
Assert.False(headers.Contains(kv1));
Assert.False(headers.Contains(kv2));
Assert.False(headers.Contains(kv3));
headers["host"] = kv1.Value;
Assert.True(headers.ContainsKey("host"));
Assert.False(headers.ContainsKey("custom"));
Assert.False(headers.ContainsKey("Content-Length"));
Assert.True(headers.Contains(kv1));
Assert.False(headers.Contains(kv2));
Assert.False(headers.Contains(kv3));
Assert.False(headers.Contains(kv1b));
Assert.False(headers.Contains(kv2b));
Assert.False(headers.Contains(kv3b));
headers["custom"] = kv2.Value;
Assert.True(headers.ContainsKey("host"));
Assert.True(headers.ContainsKey("custom"));
Assert.False(headers.ContainsKey("Content-Length"));
Assert.True(headers.Contains(kv1));
Assert.True(headers.Contains(kv2));
Assert.False(headers.Contains(kv3));
Assert.False(headers.Contains(kv1b));
Assert.False(headers.Contains(kv2b));
Assert.False(headers.Contains(kv3b));
headers["Content-Length"] = kv3.Value;
Assert.True(headers.ContainsKey("host"));
Assert.True(headers.ContainsKey("custom"));
Assert.True(headers.ContainsKey("Content-Length"));
Assert.True(headers.Contains(kv1));
Assert.True(headers.Contains(kv2));
Assert.True(headers.Contains(kv3));
Assert.False(headers.Contains(kv1b));
Assert.False(headers.Contains(kv2b));
Assert.False(headers.Contains(kv3b));
}
[Fact]
@ -159,16 +199,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
StringValues value;
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
headers.Add("Content-Length", new[] { "0" });
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
Assert.Throws<ArgumentException>(() => headers.Add("host", new[] { "localhost" }));
Assert.Throws<ArgumentException>(() => headers.Add("custom", new[] { "value" }));
Assert.Throws<ArgumentException>(() => headers.Add("Content-Length", new[] { "0" }));
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
}
[Fact]
@ -177,17 +222,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
headers.Add("Content-Length", new[] { "0" });
StringValues value;
Assert.Equal(2, headers.Count);
Assert.Equal(3, headers.Count);
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
headers.Clear();
Assert.Equal(0, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
}
[Fact]
@ -196,25 +244,36 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
headers.Add("Content-Length", new[] { "0" });
StringValues value;
Assert.Equal(2, headers.Count);
Assert.Equal(3, headers.Count);
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
Assert.True(headers.Remove("host"));
Assert.False(headers.Remove("host"));
Assert.Equal(1, headers.Count);
Assert.Equal(2, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.Remove("custom"));
Assert.False(headers.Remove("custom"));
Assert.Equal(1, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.True(headers.TryGetValue("Content-Length", out value));
Assert.True(headers.Remove("Content-Length"));
Assert.False(headers.Remove("Content-Length"));
Assert.Equal(0, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
Assert.False(headers.TryGetValue("Content-Length", out value));
}
[Fact]
@ -222,9 +281,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
IDictionary<string, StringValues> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("Content-Length", new[] { "0" });
headers.Add("custom", new[] { "value" });
var entries = new KeyValuePair<string, StringValues>[4];
var entries = new KeyValuePair<string, StringValues>[5];
headers.CopyTo(entries, 1);
Assert.Null(entries[0].Key);
@ -233,11 +293,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal("Host", entries[1].Key);
Assert.Equal(new[] { "localhost" }, entries[1].Value);
Assert.Equal("custom", entries[2].Key);
Assert.Equal(new[] { "value" }, entries[2].Value);
Assert.Equal("Content-Length", entries[2].Key);
Assert.Equal(new[] { "0" }, entries[2].Value);
Assert.Null(entries[3].Key);
Assert.Equal(new StringValues(), entries[0].Value);
Assert.Equal("custom", entries[3].Key);
Assert.Equal(new[] { "value" }, entries[3].Value);
Assert.Null(entries[4].Key);
Assert.Equal(new StringValues(), entries[4].Value);
}
[Fact]

View File

@ -10,8 +10,10 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Xunit;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
@ -173,16 +175,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
}
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenSettingRawContentLengthToNonNumericValue(string contentLength)
{
var headers = new FrameResponseHeaders();
var exception = Assert.Throws<InvalidOperationException>(() => headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength)));
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
}
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength)
@ -201,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary.Add("Content-Length", contentLength);
Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
Assert.Equal(ParseLong(contentLength), headers.ContentLength);
}
[Theory]
@ -212,17 +204,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary["Content-Length"] = contentLength;
Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
}
[Theory]
[MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader(string contentLength)
{
var headers = new FrameResponseHeaders();
headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength));
Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
Assert.Equal(ParseLong(contentLength), headers.ContentLength);
}
[Theory]
@ -232,7 +214,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var headers = new FrameResponseHeaders();
headers.HeaderContentLength = contentLength;
Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
Assert.Equal(ParseLong(contentLength), headers.ContentLength);
}
[Fact]
@ -244,7 +226,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
dictionary.Remove("Content-Length");
Assert.Equal(null, headers.HeaderContentLengthValue);
Assert.Equal(null, headers.ContentLength);
}
[Fact]
@ -256,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
dictionary.Clear();
Assert.Equal(null, headers.HeaderContentLengthValue);
Assert.Equal(null, headers.ContentLength);
}
private static long ParseLong(string value)

View File

@ -1214,6 +1214,86 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
[Fact]
public void CorrectContentLengthsOutput()
{
using (var pool = new MemoryPool())
{
var block = pool.Lease();
try
{
for (var i = 0u; i <= 9u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)(i + '0'));
Assert.Equal(block.End, block.Start + 1);
Assert.Equal(iter.Index, block.End);
}
for (var i = 10u; i <= 99u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 10) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 2);
Assert.Equal(iter.Index, block.End);
}
for (var i = 100u; i <= 999u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 100) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 100) / 10) + '0'));
Assert.Equal(block.Array[block.Start + 2], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 3);
Assert.Equal(iter.Index, block.End);
}
for (var i = 1000u; i <= 9999u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 1000) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 1000) / 100) + '0'));
Assert.Equal(block.Array[block.Start + 2], (byte)(((i % 100) / 10) + '0'));
Assert.Equal(block.Array[block.Start + 3], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 4);
Assert.Equal(iter.Index, block.End);
}
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(ulong.MaxValue);
var outputBytes = Encoding.ASCII.GetBytes(ulong.MaxValue.ToString("0"));
for (var i = 0; i < outputBytes.Length; i++)
{
Assert.Equal(block.Array[block.Start + i], outputBytes[i]);
}
Assert.Equal(block.End, block.Start + outputBytes.Length);
Assert.Equal(iter.Index, block.End);
}
}
finally
{
pool.Return(block);
}
}
}
private delegate bool GetKnownString(MemoryPoolIterator iter, out string result);
private void TestKnownStringsInterning(string input, string expected, GetKnownString action)

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
@ -19,47 +20,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
return condition ? formatter() : "";
}
static string AppendSwitch(IEnumerable<IGrouping<int, KnownHeader>> values, string className, bool handleUnknown = false) =>
$@"fixed (byte* ptr = &keyBytes[keyOffset])
{{
var pUB = ptr;
var pUL = (ulong*)pUB;
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)
{{
ThrowMultipleContentLengthsException();
}}
else
{{
_contentLength = ParseContentLength(value);
}}
return;" : $@"
if ({header.TestBit()})
{{
_headers._{header.Identifier} = AppendValue(_headers._{header.Identifier}, value);
}}
else
{{{If(className == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{{
{header.SetBit()};
_headers._{header.Identifier} = new StringValues(value);{(header.EnhancedSetter == false ? "" : $@"
_headers._{header.Identifier} = stringValue;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
}}
return;
return;")}
}}
")}}}
break;
")}}}
{(handleUnknown ? $@"
key = new string('\0', keyLength);
fixed(char *keyBuffer = key)
{{
if (!AsciiUtilities.TryGetAsciiString(ptr, keyBuffer, keyLength))
{{
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidCharactersInHeaderName);
}}
}}
": "")}
}}";
")}}}";
class KnownHeader
{
@ -72,7 +67,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
public int BytesCount { get; set; }
public bool EnhancedSetter { get; set; }
public bool PrimaryHeader { get; set; }
public string TestBit() => $"((_bits & {1L << Index}L) != 0)";
public string TestBit() => $"(_bits & {1L << Index}L) != 0";
public string TestTempBit() => $"(tempBits & {1L << Index}L) != 0";
public string TestNotTempBit() => $"(tempBits & ~{1L << Index}L) == 0";
public string TestNotBit() => $"(_bits & {1L << Index}L) == 0";
public string SetBit() => $"_bits |= {1L << Index}L";
public string ClearBit() => $"_bits &= ~{1L << Index}L";
@ -135,7 +133,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
{
"Connection",
"Date",
"Content-Length",
"Content-Type",
"Server",
};
@ -152,7 +149,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
"Via",
"Warning",
"Allow",
"Content-Length",
"Content-Type",
"Content-Encoding",
"Content-Language",
@ -192,19 +188,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
"TE",
"Translate",
"User-Agent",
}).Concat(corsRequestHeaders).Select((header, index) => new KnownHeader
})
.Concat(corsRequestHeaders)
.Select((header, index) => new KnownHeader
{
Name = header,
Index = index,
PrimaryHeader = requestPrimaryHeaders.Contains(header)
}).ToArray();
})
.Concat(new[] { new KnownHeader
{
Name = "Content-Length",
Index = -1,
PrimaryHeader = requestPrimaryHeaders.Contains("Content-Length")
}})
.ToArray();
Debug.Assert(requestHeaders.Length <= 64);
Debug.Assert(requestHeaders.Max(x => x.Index) <= 62);
var enhancedHeaders = new[]
{
"Connection",
"Server",
"Date",
"Transfer-Encoding",
"Content-Length",
"Transfer-Encoding"
};
// http://www.w3.org/TR/cors/#syntax
var corsResponseHeaders = new[]
@ -228,13 +235,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
"Set-Cookie",
"Vary",
"WWW-Authenticate",
}).Concat(corsResponseHeaders).Select((header, index) => new KnownHeader
})
.Concat(corsResponseHeaders)
.Select((header, index) => new KnownHeader
{
Name = header,
Index = index,
EnhancedSetter = enhancedHeaders.Contains(header),
PrimaryHeader = responsePrimaryHeaders.Contains(header)
}).ToArray();
})
.Concat(new[] { new KnownHeader
{
Name = "Content-Length",
Index = -1,
EnhancedSetter = enhancedHeaders.Contains("Content-Length"),
PrimaryHeader = responsePrimaryHeaders.Contains("Content-Length")
}})
.ToArray();
// 63 for reponseHeaders as it steals one bit for Content-Length in CopyTo(ref MemoryPoolIterator output)
Debug.Assert(responseHeaders.Length <= 63);
Debug.Assert(responseHeaders.Max(x => x.Index) <= 62);
var loops = new[]
{
new
@ -269,6 +290,7 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{{
@ -286,61 +308,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private HeaderReferences _headers;
{Each(loop.Headers, header => $@"
public StringValues Header{header.Identifier}
{{
{{{(header.Identifier == "ContentLength" ? $@"
get
{{
if ({header.TestBit()})
StringValues value;
if (_contentLength.HasValue)
{{
return _headers._{header.Identifier};
value = new StringValues(HeaderUtilities.FormatInt64(_contentLength.Value));
}}
return StringValues.Empty;
return value;
}}
set
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{{
_contentLength = ParseContentLength(value);
}}" : $@"
get
{{
StringValues value;
if ({header.TestBit()})
{{
value = _headers._{header.Identifier};
}}
return value;
}}
set
{{
{header.SetBit()};
_headers._{header.Identifier} = value; {(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
}}
}}")}
}}")}
{Each(loop.Headers.Where(header => header.EnhancedSetter), header => $@"
{Each(loop.Headers.Where(header => header.EnhancedSetter), header => $@"
public void SetRaw{header.Identifier}(StringValues value, byte[] raw)
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{{
{header.SetBit()};
_headers._{header.Identifier} = value;
_headers._raw{header.Identifier} = raw;
}}")}
protected override int GetCountFast()
{{
return BitCount(_bits) + (MaybeUnknown?.Count ?? 0);
}}
protected override StringValues GetValueFast(string key)
{{
switch (key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
if ({header.TestBit()})
{{
return _headers._{header.Identifier};
}}
else
{{
ThrowKeyNotFoundException();
}}
}}
")}}}
break;
")}}}
if (MaybeUnknown == null)
{{
ThrowKeyNotFoundException();
}}
return MaybeUnknown[key];
return (_contentLength.HasValue ? 1 : 0 ) + BitCount(_bits) + (MaybeUnknown?.Count ?? 0);
}}
protected override bool TryGetValueFast(string key, out StringValues value)
{{
switch (key.Length)
@ -348,71 +357,83 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
{{{(header.Identifier == "ContentLength" ? @"
if (_contentLength.HasValue)
{
value = HeaderUtilities.FormatInt64(_contentLength.Value);
return true;
}
return false;" : $@"
if ({header.TestBit()})
{{
value = _headers._{header.Identifier};
return true;
}}
else
{{
value = StringValues.Empty;
return false;
}}
}}
")}}}
break;
")}}}
value = StringValues.Empty;
return false;")}
}}")}
}}
break;")}
}}
return MaybeUnknown?.TryGetValue(key, out value) ?? false;
}}
protected override void SetValueFast(string key, StringValues value)
{{
{(loop.ClassName == "FrameResponseHeaders" ? "ValidateHeaderCharacters(value);" : "")}
{{{(loop.ClassName == "FrameResponseHeaders" ? @"
ValidateHeaderCharacters(value);" : "")}
switch (key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{{{(header.Identifier == "ContentLength" ? $@"
_contentLength = ParseContentLength(value.ToString());" : $@"
{header.SetBit()};
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
_headers._raw{header.Identifier} = null;")}")}
return;
}}
")}}}
break;
")}}}
{(loop.ClassName == "FrameResponseHeaders" ? "ValidateHeaderCharacters(key);" : "")}
Unknown[key] = value;
}}")}
}}
break;")}
}}
SetValueUnknown(key, value);
}}
protected override void AddValueFast(string key, StringValues value)
{{
{(loop.ClassName == "FrameResponseHeaders" ? "ValidateHeaderCharacters(value);" : "")}
protected override bool AddValueFast(string key, StringValues value)
{{{(loop.ClassName == "FrameResponseHeaders" ? @"
ValidateHeaderCharacters(value);" : "")}
switch (key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
if ({header.TestBit()})
{{{(header.Identifier == "ContentLength" ? $@"
if (!_contentLength.HasValue)
{{
ThrowDuplicateKeyException();
}}{
If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{header.SetBit()};
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
return;
}}
")}}}
break;
")}}}
{(loop.ClassName == "FrameResponseHeaders" ? "ValidateHeaderCharacters(key);" : "")}
_contentLength = ParseContentLength(value);
return true;
}}
return false;" : $@"
if ({header.TestNotBit()})
{{
{header.SetBit()};
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
return true;
}}
return false;")}
}}")}
}}
break;")}
}}
{(loop.ClassName == "FrameResponseHeaders" ? @"
ValidateHeaderCharacters(key);" : "")}
Unknown.Add(key, value);
// Return true, above will throw and exit for false
return true;
}}
protected override bool RemoveFast(string key)
{{
switch (key.Length)
@ -420,74 +441,88 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
{{{(header.Identifier == "ContentLength" ? @"
if (_contentLength.HasValue)
{
_contentLength = null;
return true;
}
return false;" : $@"
if ({header.TestBit()})
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = null;")}
{{
{header.ClearBit()};
_headers._{header.Identifier} = StringValues.Empty;{(header.EnhancedSetter == false ? "" : $@"
_headers._{header.Identifier} = default(StringValues);{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
return true;
}}
else
{{
return false;
}}
}}
")}}}
break;
")}}}
return false;")}
}}")}
}}
break;")}
}}
return MaybeUnknown?.Remove(key) ?? false;
}}
protected override void ClearFast()
{{
{{
MaybeUnknown?.Clear();
{(loop.ClassName == "FrameResponseHeaders" ? "_contentLength = null;" : "")}
if(FrameHeaders.BitCount(_bits) > 12)
_contentLength = null;
var tempBits = _bits;
_bits = 0;
if(FrameHeaders.BitCount(tempBits) > 12)
{{
_headers = default(HeaderReferences);
_bits = 0;
return;
}}
{Each(loop.Headers.OrderBy(h => !h.PrimaryHeader), header => $@"
if ({header.TestBit()})
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength").OrderBy(h => !h.PrimaryHeader), header => $@"
if ({header.TestTempBit()})
{{
_headers._{header.Identifier} = default(StringValues);
{header.ClearBit()};
if(_bits == 0)
if({header.TestNotTempBit()})
{{
return;
}}
tempBits &= ~{1L << header.Index}L;
}}
")}
}}
protected override void CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
protected override bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
{{
if (arrayIndex < 0)
{{
ThrowArgumentException();
return false;
}}
{Each(loop.Headers, header => $@"
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength"), header => $@"
if ({header.TestBit()})
{{
if (arrayIndex == array.Length)
{{
ThrowArgumentException();
return false;
}}
array[arrayIndex] = new KeyValuePair<string, StringValues>(""{header.Name}"", _headers._{header.Identifier});
++arrayIndex;
}}")}
if (_contentLength.HasValue)
{{
if (arrayIndex == array.Length)
{{
return false;
}}
array[arrayIndex] = new KeyValuePair<string, StringValues>(""Content-Length"", HeaderUtilities.FormatInt64(_contentLength.Value));
++arrayIndex;
}}
")}
((ICollection<KeyValuePair<string, StringValues>>)MaybeUnknown)?.CopyTo(array, arrayIndex);
return true;
}}
{(loop.ClassName == "FrameResponseHeaders" ? $@"
protected void CopyToFast(ref MemoryPoolIterator output)
{{
var tempBits = _bits;
{Each(loop.Headers.OrderBy(h => !h.PrimaryHeader), header => $@"
if ({header.TestBit()})
var tempBits = _bits | (_contentLength.HasValue ? {1L << 63}L : 0);
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength").OrderBy(h => !h.PrimaryHeader), header => $@"
if ({header.TestTempBit()})
{{ {(header.EnhancedSetter == false ? "" : $@"
if (_headers._raw{header.Identifier} != null)
{{
@ -507,35 +542,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}}
}}
tempBits &= ~{1L << header.Index}L;
if(tempBits == 0)
if({header.TestNotTempBit()})
{{
return;
}}
}}
")}
}}
" : "")}
tempBits &= ~{1L << header.Index}L;
}}{(header.Identifier == "Server" ? $@"
if ((tempBits & {1L << 63}L) != 0)
{{
output.CopyFrom(_headerBytes, {loop.Headers.First(x => x.Identifier == "ContentLength").BytesOffset}, {loop.Headers.First(x => x.Identifier == "ContentLength").BytesCount});
output.CopyFromNumeric((ulong)ContentLength.Value);
if((tempBits & ~{1L << 63}L) == 0)
{{
return;
}}
tempBits &= ~{1L << 63}L;
}}" : "")}")}
}}" : "")}
{(loop.ClassName == "FrameRequestHeaders" ? $@"
public unsafe void Append(byte[] keyBytes, int keyOffset, int keyLength, string value)
{{
{AppendSwitch(loop.Headers.Where(h => h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName)}
AppendNonPrimaryHeaders(keyBytes, keyOffset, keyLength, value);
fixed (byte* ptr = &keyBytes[keyOffset])
{{
var pUB = ptr;
{AppendSwitch(loop.Headers.Where(h => h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName)}
AppendNonPrimaryHeaders(ptr, keyOffset, keyLength, value);
}}
}}
private unsafe void AppendNonPrimaryHeaders(byte[] keyBytes, int keyOffset, int keyLength, string value)
private unsafe void AppendNonPrimaryHeaders(byte* pKeyBytes, int keyOffset, int keyLength, string value)
{{
string key;
{AppendSwitch(loop.Headers.Where(h => !h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName, true)}
var pUB = pKeyBytes;
{AppendSwitch(loop.Headers.Where(h => !h.PrimaryHeader).GroupBy(x => x.Name.Length), loop.ClassName)}
StringValues existing;
Unknown.TryGetValue(key, out existing);
Unknown[key] = AppendValue(existing, value);
AppendUnknownHeaders(pKeyBytes, keyLength, value);
}}" : "")}
private struct HeaderReferences
{{{Each(loop.Headers, header => @"
{{{Each(loop.Headers.Where(header => header.Identifier != "ContentLength"), header => @"
public StringValues _" + header.Identifier + ";")}
{Each(loop.Headers.Where(header => header.EnhancedSetter), header => @"
public byte[] _raw" + header.Identifier + ";")}
@ -547,14 +593,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{{
switch (_state)
{{
{Each(loop.Headers, header => $@"
case {header.Index}:
goto state{header.Index};
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength"), header => $@"
case {header.Index}:
goto state{header.Index};
")}
case {loop.Headers.Count()}:
goto state{loop.Headers.Count()};
default:
goto state_default;
}}
{Each(loop.Headers, header => $@"
{Each(loop.Headers.Where(header => header.Identifier != "ContentLength"), header => $@"
state{header.Index}:
if ({header.TestBit()})
{{
@ -563,6 +611,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return true;
}}
")}
state{loop.Headers.Count()}:
if (_collection._contentLength.HasValue)
{{
_current = new KeyValuePair<string, StringValues>(""Content-Length"", HeaderUtilities.FormatInt64(_collection._contentLength.Value));
_state = {loop.Headers.Count() + 1};
return true;
}}
state_default:
if (!_hasUnknown || !_unknownEnumerator.MoveNext())
{{