From 1ffad5ca3832090570197c02cb4ff0ac2d9f6eb5 Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Mon, 24 Oct 2016 14:57:24 -0700 Subject: [PATCH] Handle multiple tokens in Connection header (#1170). --- .../Internal/Http/ConnectionOptions.cs | 16 ++ .../Internal/Http/Frame.FeatureCollection.cs | 13 +- .../Internal/Http/Frame.cs | 5 +- .../Internal/Http/FrameHeaders.cs | 101 +++++++++++- .../Internal/Http/FrameOfT.cs | 1 + .../Internal/Http/MessageBody.cs | 18 +- .../FrameHeadersTests.cs | 155 ++++++++++++++++++ .../MessageBodyTests.cs | 21 +++ 8 files changed, 309 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionOptions.cs create mode 100644 test/Microsoft.AspNetCore.Server.KestrelTests/FrameHeadersTests.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionOptions.cs new file mode 100644 index 0000000000..72d4eeb5d1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ConnectionOptions.cs @@ -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 + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.FeatureCollection.cs index 75c1404ba5..9480147f95 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.FeatureCollection.cs @@ -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; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 8f996d0c71..c6f90a2858 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs index 4d234322a5..d0d3114268 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs @@ -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."); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs index cfdc1fc556..6925c13bea 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs index bff4ba622f..a3e0093089 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs @@ -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 ReadAsync(ArraySegment 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> PeekAsync(CancellationToken cancellationToken) diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameHeadersTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameHeadersTests.cs new file mode 100644 index 0000000000..c8cc648376 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameHeadersTests.cs @@ -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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/MessageBodyTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/MessageBodyTests.cs index 1790cf8094..427189fd1b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/MessageBodyTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/MessageBodyTests.cs @@ -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(buffer, 0, 5)); + } + } + private void AssertASCII(string expected, ArraySegment actual) { var encoding = Encoding.ASCII;