Handle multiple tokens in Connection header (#1170).

This commit is contained in:
Cesar Blum Silveira 2016-10-24 14:57:24 -07:00
parent d64b4c7acb
commit 1ffad5ca38
8 changed files with 309 additions and 21 deletions

View File

@ -0,0 +1,16 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
[Flags]
public enum ConnectionOptions
{
None = 0,
Close = 1,
KeepAlive = 2,
Upgrade = 4
}
}

View File

@ -259,18 +259,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
get { return HasResponseStarted; }
}
bool IHttpUpgradeFeature.IsUpgradableRequest
{
get
{
StringValues values;
if (RequestHeaders.TryGetValue("Connection", out values))
{
return values.Any(value => value.IndexOf("upgrade", StringComparison.OrdinalIgnoreCase) != -1);
}
return false;
}
}
bool IHttpUpgradeFeature.IsUpgradableRequest => _upgrade;
bool IFeatureCollection.IsReadOnly => false;

View File

@ -60,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
protected bool _upgrade;
private bool _canHaveBody;
private bool _autoChunk;
protected Exception _applicationException;
@ -810,13 +811,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
var responseHeaders = FrameResponseHeaders;
var hasConnection = responseHeaders.HasConnection;
var connectionOptions = hasConnection ? FrameHeaders.ParseConnection(responseHeaders.HeaderConnection) : ConnectionOptions.None;
var end = SocketOutput.ProducingStart();
if (_keepAlive && hasConnection)
{
var connectionValue = responseHeaders.HeaderConnection.ToString();
_keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase);
_keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
}
// Set whether response can have body

View File

@ -4,7 +4,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@ -265,6 +264,106 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return parsed;
}
public static unsafe ConnectionOptions ParseConnection(StringValues connection)
{
var connectionOptions = ConnectionOptions.None;
foreach (var value in connection)
{
fixed (char* ptr = value)
{
var ch = ptr;
var tokenEnd = ch;
var end = ch + value.Length;
while (ch < end)
{
while (tokenEnd < end && *tokenEnd != ',')
{
tokenEnd++;
}
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
var tokenLength = tokenEnd - ch;
if (tokenLength >= 9 && (*ch | 0x20) == 'k')
{
if ((*++ch | 0x20) == 'e' &&
(*++ch | 0x20) == 'e' &&
(*++ch | 0x20) == 'p' &&
*++ch == '-' &&
(*++ch | 0x20) == 'a' &&
(*++ch | 0x20) == 'l' &&
(*++ch | 0x20) == 'i' &&
(*++ch | 0x20) == 'v' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd || *ch == ',')
{
connectionOptions |= ConnectionOptions.KeepAlive;
}
}
}
else if (tokenLength >= 7 && (*ch | 0x20) == 'u')
{
if ((*++ch | 0x20) == 'p' &&
(*++ch | 0x20) == 'g' &&
(*++ch | 0x20) == 'r' &&
(*++ch | 0x20) == 'a' &&
(*++ch | 0x20) == 'd' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd || *ch == ',')
{
connectionOptions |= ConnectionOptions.Upgrade;
}
}
}
else if (tokenLength >= 5 && (*ch | 0x20) == 'c')
{
if ((*++ch | 0x20) == 'l' &&
(*++ch | 0x20) == 'o' &&
(*++ch | 0x20) == 's' &&
(*++ch | 0x20) == 'e')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd || *ch == ',')
{
connectionOptions |= ConnectionOptions.Close;
}
}
}
tokenEnd++;
ch = tokenEnd;
}
}
}
return connectionOptions;
}
private static void ThrowInvalidContentLengthException(string value)
{
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");

View File

@ -86,6 +86,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this);
_keepAlive = messageBody.RequestKeepAlive;
_upgrade = messageBody.RequestUpgrade;
InitializeStreams(messageBody);

View File

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
@ -23,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public bool RequestKeepAlive { get; protected set; }
public bool RequestUpgrade { get; protected set; }
public Task<int> ReadAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
{
var task = PeekAsync(cancellationToken);
@ -231,15 +234,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// see also http://tools.ietf.org/html/rfc2616#section-4.4
var keepAlive = httpVersion != HttpVersion.Http10;
var connection = headers.HeaderConnection.ToString();
if (connection.Length > 0)
var connection = headers.HeaderConnection;
if (connection.Count > 0)
{
if (connection.Equals("upgrade", StringComparison.OrdinalIgnoreCase))
var connectionOptions = FrameHeaders.ParseConnection(connection);
if ((connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade)
{
return new ForRemainingData(context);
return new ForRemainingData(true, context);
}
keepAlive = connection.Equals("keep-alive", StringComparison.OrdinalIgnoreCase);
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
}
var transferEncoding = headers.HeaderTransferEncoding.ToString();
@ -267,9 +272,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private class ForRemainingData : MessageBody
{
public ForRemainingData(Frame context)
public ForRemainingData(bool upgrade, Frame context)
: base(context)
{
RequestUpgrade = upgrade;
}
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)

View File

@ -0,0 +1,155 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class FrameHeadersTests
{
[Theory]
[InlineData("keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive, upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("keep-alive,upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("upgrade, keep-alive", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("upgrade,keep-alive", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("upgrade,,keep-alive", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("keep-alive,", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive,,", ConnectionOptions.KeepAlive)]
[InlineData(",keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(",,keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive, ", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive, ,", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive, , ", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive ,", ConnectionOptions.KeepAlive)]
[InlineData(",keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(", keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(",,keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(", ,keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(",, keep-alive", ConnectionOptions.KeepAlive)]
[InlineData(", , keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("upgrade,", ConnectionOptions.Upgrade)]
[InlineData("upgrade,,", ConnectionOptions.Upgrade)]
[InlineData(",upgrade", ConnectionOptions.Upgrade)]
[InlineData(",,upgrade", ConnectionOptions.Upgrade)]
[InlineData("upgrade, ", ConnectionOptions.Upgrade)]
[InlineData("upgrade, ,", ConnectionOptions.Upgrade)]
[InlineData("upgrade, , ", ConnectionOptions.Upgrade)]
[InlineData("upgrade ,", ConnectionOptions.Upgrade)]
[InlineData(",upgrade", ConnectionOptions.Upgrade)]
[InlineData(", upgrade", ConnectionOptions.Upgrade)]
[InlineData(",,upgrade", ConnectionOptions.Upgrade)]
[InlineData(", ,upgrade", ConnectionOptions.Upgrade)]
[InlineData(",, upgrade", ConnectionOptions.Upgrade)]
[InlineData(", , upgrade", ConnectionOptions.Upgrade)]
[InlineData("close,", ConnectionOptions.Close)]
[InlineData("close,,", ConnectionOptions.Close)]
[InlineData(",close", ConnectionOptions.Close)]
[InlineData(",,close", ConnectionOptions.Close)]
[InlineData("close, ", ConnectionOptions.Close)]
[InlineData("close, ,", ConnectionOptions.Close)]
[InlineData("close, , ", ConnectionOptions.Close)]
[InlineData("close ,", ConnectionOptions.Close)]
[InlineData(",close", ConnectionOptions.Close)]
[InlineData(", close", ConnectionOptions.Close)]
[InlineData(",,close", ConnectionOptions.Close)]
[InlineData(", ,close", ConnectionOptions.Close)]
[InlineData(",, close", ConnectionOptions.Close)]
[InlineData(", , close", ConnectionOptions.Close)]
[InlineData("kupgrade", ConnectionOptions.None)]
[InlineData("keupgrade", ConnectionOptions.None)]
[InlineData("ukeep-alive", ConnectionOptions.None)]
[InlineData("upkeep-alive", ConnectionOptions.None)]
[InlineData("k,upgrade", ConnectionOptions.Upgrade)]
[InlineData("u,keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("ke,upgrade", ConnectionOptions.Upgrade)]
[InlineData("up,keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("close", ConnectionOptions.Close)]
[InlineData("upgrade,close", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
[InlineData("close,upgrade", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
[InlineData("keep-alive2", ConnectionOptions.None)]
[InlineData("keep-alive2,", ConnectionOptions.None)]
[InlineData("keep-alive2 ", ConnectionOptions.None)]
[InlineData("keep-alive2 ,", ConnectionOptions.None)]
[InlineData("keep-alive2,", ConnectionOptions.None)]
[InlineData("upgrade2", ConnectionOptions.None)]
[InlineData("upgrade2,", ConnectionOptions.None)]
[InlineData("upgrade2 ", ConnectionOptions.None)]
[InlineData("upgrade2 ,", ConnectionOptions.None)]
[InlineData("upgrade2,", ConnectionOptions.None)]
[InlineData("close2", ConnectionOptions.None)]
[InlineData("close2,", ConnectionOptions.None)]
[InlineData("close2 ", ConnectionOptions.None)]
[InlineData("close2 ,", ConnectionOptions.None)]
[InlineData("close2,", ConnectionOptions.None)]
[InlineData("keep-alivekeep-alive", ConnectionOptions.None)]
[InlineData("keep-aliveupgrade", ConnectionOptions.None)]
[InlineData("upgradeupgrade", ConnectionOptions.None)]
[InlineData("upgradekeep-alive", ConnectionOptions.None)]
[InlineData("closeclose", ConnectionOptions.None)]
[InlineData("closeupgrade", ConnectionOptions.None)]
[InlineData("upgradeclose", ConnectionOptions.None)]
[InlineData("keep-alive 2", ConnectionOptions.None)]
[InlineData("upgrade 2", ConnectionOptions.None)]
[InlineData("keep-alive 2, close", ConnectionOptions.Close)]
[InlineData("upgrade 2, close", ConnectionOptions.Close)]
[InlineData("close, keep-alive 2", ConnectionOptions.Close)]
[InlineData("close, upgrade 2", ConnectionOptions.Close)]
[InlineData("close 2, upgrade", ConnectionOptions.Upgrade)]
[InlineData("upgrade, close 2", ConnectionOptions.Upgrade)]
[InlineData("k2ep-alive", ConnectionOptions.None)]
[InlineData("ke2p-alive", ConnectionOptions.None)]
[InlineData("u2grade", ConnectionOptions.None)]
[InlineData("up2rade", ConnectionOptions.None)]
[InlineData("c2ose", ConnectionOptions.None)]
[InlineData("cl2se", ConnectionOptions.None)]
[InlineData("k2ep-alive,", ConnectionOptions.None)]
[InlineData("ke2p-alive,", ConnectionOptions.None)]
[InlineData("u2grade,", ConnectionOptions.None)]
[InlineData("up2rade,", ConnectionOptions.None)]
[InlineData("c2ose,", ConnectionOptions.None)]
[InlineData("cl2se,", ConnectionOptions.None)]
[InlineData("k2ep-alive ", ConnectionOptions.None)]
[InlineData("ke2p-alive ", ConnectionOptions.None)]
[InlineData("u2grade ", ConnectionOptions.None)]
[InlineData("up2rade ", ConnectionOptions.None)]
[InlineData("c2ose ", ConnectionOptions.None)]
[InlineData("cl2se ", ConnectionOptions.None)]
[InlineData("k2ep-alive ,", ConnectionOptions.None)]
[InlineData("ke2p-alive ,", ConnectionOptions.None)]
[InlineData("u2grade ,", ConnectionOptions.None)]
[InlineData("up2rade ,", ConnectionOptions.None)]
[InlineData("c2ose ,", ConnectionOptions.None)]
[InlineData("cl2se ,", ConnectionOptions.None)]
public void TestParseConnection(string connection, ConnectionOptions expectedConnectionOptionss)
{
var connectionOptions = FrameHeaders.ParseConnection(connection);
Assert.Equal(expectedConnectionOptionss, connectionOptions);
}
[Theory]
[InlineData("keep-alive", "upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("upgrade", "keep-alive", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("keep-alive", "", ConnectionOptions.KeepAlive)]
[InlineData("", "keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("upgrade", "", ConnectionOptions.Upgrade)]
[InlineData("", "upgrade", ConnectionOptions.Upgrade)]
[InlineData("keep-alive, upgrade", "", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("upgrade, keep-alive", "", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("", "keep-alive, upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("", "upgrade, keep-alive", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("", "", ConnectionOptions.None)]
[InlineData("close", "", ConnectionOptions.Close)]
[InlineData("", "close", ConnectionOptions.Close)]
[InlineData("close", "upgrade", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
[InlineData("upgrade", "close", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
public void TestParseConnectionMultipleValues(string value1, string value2, ConnectionOptions expectedConnectionOptionss)
{
var connection = new StringValues(new[] { value1, value2 });
var connectionOptions = FrameHeaders.ParseConnection(connection);
Assert.Equal(expectedConnectionOptionss, connectionOptions);
}
}
}

View File

@ -286,6 +286,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
[Theory]
[InlineData("keep-alive, upgrade")]
[InlineData("Keep-Alive, Upgrade")]
[InlineData("upgrade, keep-alive")]
[InlineData("Upgrade, Keep-Alive")]
public void ConnectionUpgradeKeepAlive(string headerConnection)
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.FrameContext);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
input.Add("Hello", true);
var buffer = new byte[1024];
Assert.Equal(5, stream.Read(buffer, 0, 1024));
AssertASCII("Hello", new ArraySegment<byte>(buffer, 0, 5));
}
}
private void AssertASCII(string expected, ArraySegment<byte> actual)
{
var encoding = Encoding.ASCII;