diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs index febe7dc04a83..838b0463ab51 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -383,6 +383,7 @@ internal partial class HttpRequestHeaders : IHeaderDictionary private HeaderReferences _headers; public bool HasConnection => (_bits & 0x2L) != 0; + public bool HasCookie => (_bits & 0x20000L) != 0; public bool HasTransferEncoding => (_bits & 0x20000000000L) != 0; public int HostCount => _headers._Host.Count; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index 635a0b3276fc..472f1c477107 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -48,6 +48,15 @@ public void OnHeadersComplete() Clear(headersToClear); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MergeCookies() + { + if (HasCookie && _headers._Cookie.Count > 1) + { + _headers._Cookie = string.Join("; ", _headers._Cookie.ToArray()); + } + } + protected override void ClearFast() { if (!ReuseHeaderValues) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index c69f40bfa2df..ccdc21f9cf9a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -201,6 +201,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); + // Cookies should be merged into a single string separated by "; " + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5 + HttpRequestHeaders.MergeCookies(); + return true; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 91bb4e65a4c2..503e4120b08f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -907,6 +907,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio // Suppress pseudo headers from the public headers collection. HttpRequestHeaders.ClearPseudoRequestHeaders(); + // Cookies should be merged into a single string separated by "; " + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34#section-4.1.1.2 + HttpRequestHeaders.MergeCookies(); + return true; } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs index 8c44390a9aeb..5471aaf5cd8e 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs @@ -1,25 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; -using System.Buffers.Binary; using System.Diagnostics; -using System.IO; using System.IO.Pipelines; -using System.Linq; using System.Net.Http.HPack; -using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Http2HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2HeadersEnumerator; @@ -36,15 +28,20 @@ public abstract class Http2ConnectionBenchmarkBase private int _currentStreamId; private byte[] _headersBuffer; private DuplexPipe.DuplexPipePair _connectionPair; - private Http2Frame _httpFrame; private int _dataWritten; + private Task _requestProcessingTask; + + private readonly Http2Frame _receiveHttpFrame = new(); + private readonly Http2Frame _sendHttpFrame = new(); protected abstract Task ProcessRequest(HttpContext httpContext); + [Params(0, 1, 3)] + public int NumCookies { get; set; } + public virtual void GlobalSetup() { _memoryPool = PinnedBlockMemoryPoolFactory.Create(); - _httpFrame = new Http2Frame(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); @@ -56,6 +53,16 @@ public virtual void GlobalSetup() _httpRequestHeaders[HeaderNames.Scheme] = new StringValues("http"); _httpRequestHeaders[HeaderNames.Authority] = new StringValues("localhost:80"); + if (NumCookies > 0) + { + var cookies = new string[NumCookies]; + for (var index = 0; index < NumCookies; index++) + { + cookies[index] = $"{index}={index + 1}"; + } + _httpRequestHeaders[HeaderNames.Cookie] = cookies; + } + _headersBuffer = new byte[1024 * 16]; _hpackEncoder = new DynamicHPackEncoder(); @@ -79,7 +86,7 @@ public virtual void GlobalSetup() _currentStreamId = 1; - _ = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory())); + _requestProcessingTask = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory())); _connectionPair.Application.Output.Write(Http2Connection.ClientPreface); _connectionPair.Application.Output.WriteSettings(new Http2PeerSettings @@ -89,12 +96,12 @@ public virtual void GlobalSetup() _connectionPair.Application.Output.FlushAsync().GetAwaiter().GetResult(); // Read past connection setup frames - ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); - Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS); - ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); - Debug.Assert(_httpFrame.Type == Http2FrameType.WINDOW_UPDATE); - ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); - Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS); + ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult(); + Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS); + ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult(); + Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.WINDOW_UPDATE); + ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult(); + Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS); } [Benchmark] @@ -102,32 +109,32 @@ public async Task MakeRequest() { _requestHeadersEnumerator.Initialize(_httpRequestHeaders); _requestHeadersEnumerator.MoveNext(); - _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); + _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _sendHttpFrame); await _connectionPair.Application.Output.FlushAsync(); while (true) { - await ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame); + await ReceiveFrameAsync(_connectionPair.Application.Input); - if (_httpFrame.StreamId != _currentStreamId && _httpFrame.StreamId != 0) + if (_receiveHttpFrame.StreamId != _currentStreamId && _receiveHttpFrame.StreamId != 0) { - throw new Exception($"Unexpected stream ID: {_httpFrame.StreamId}"); + throw new Exception($"Unexpected stream ID: {_receiveHttpFrame.StreamId}"); } - if (_httpFrame.Type == Http2FrameType.DATA) + if (_receiveHttpFrame.Type == Http2FrameType.DATA) { - _dataWritten += _httpFrame.DataPayloadLength; + _dataWritten += _receiveHttpFrame.DataPayloadLength; } if (_dataWritten > 1024 * 32) { - _connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _httpFrame); + _connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _sendHttpFrame); await _connectionPair.Application.Output.FlushAsync(); _dataWritten = 0; } - if ((_httpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM) + if ((_receiveHttpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM) { break; } @@ -136,7 +143,7 @@ public async Task MakeRequest() _currentStreamId += 2; } - internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame frame, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) + internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) { while (true) { @@ -147,7 +154,7 @@ internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame fra try { - if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload)) + if (Http2FrameReader.TryReadFrame(ref buffer, _receiveHttpFrame, maxFrameSize, out var framePayload)) { consumed = examined = framePayload.End; return; @@ -170,9 +177,10 @@ internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame fra } [GlobalCleanup] - public void Dispose() + public async ValueTask DisposeAsync() { _connectionPair.Application.Output.Complete(); + await _requestProcessingTask; _memoryPool?.Dispose(); } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs index 79b647597060..04d395f58ddc 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks; public class Http2ConnectionBenchmark : Http2ConnectionBenchmarkBase { - [Params(0, 128, 1024)] + [Params(0)] public int ResponseDataLength { get; set; } private string _responseData; diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 94981b7b8bd6..c04451b3f913 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -101,6 +101,7 @@ static KnownHeaders() }; var requestHeadersExistence = new[] { + HeaderNames.Cookie, HeaderNames.Connection, HeaderNames.TransferEncoding, }; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index f4dc806cacbb..6778e02a8c67 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -2840,6 +2840,33 @@ public async Task HEADERS_Received_RequestLineLength_StreamError() await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_CookiesMergedIntoOne() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Cookie, "a=0"), + new KeyValuePair(HeaderNames.Cookie, "b=1"), + new KeyValuePair(HeaderNames.Cookie, "c=2"), + }; + + await InitializeConnectionAsync(_readHeadersApplication); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 36, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + Assert.Equal("a=0; b=1; c=2", _receivedHeaders[HeaderNames.Cookie]); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task PRIORITY_Received_StreamIdZero_ConnectionError() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index e7dd6999fe3e..1a9fe56f1dc7 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -130,6 +130,48 @@ await Http3Api.InitializeConnectionAsync(async context => await requestStream.ExpectReceiveEndOfStream(); } + [Fact] + public async Task HEADERS_CookiesMergedIntoOne() + { + var requestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Cookie, "a=0"), + new KeyValuePair(HeaderNames.Cookie, "b=1"), + new KeyValuePair(HeaderNames.Cookie, "c=2"), + }; + + var receivedHeaders = ""; + + await Http3Api.InitializeConnectionAsync(async context => + { + var buffer = new byte[16 * 1024]; + var received = 0; + + // verify that the cookies are all merged into a single string + receivedHeaders = context.Request.Headers[HeaderNames.Cookie]; + + while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await context.Response.Body.WriteAsync(buffer, 0, received); + } + }); + + await Http3Api.CreateControlStream(); + await Http3Api.GetInboundControlStream(); + var requestStream = await Http3Api.CreateRequestStream(); + + await requestStream.SendHeadersAsync(requestHeaders, endStream: true); + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + await requestStream.OnDisposedTask.DefaultTimeout(); + + Assert.Equal("a=0; b=1; c=2", receivedHeaders); + } + [Theory] [InlineData(0, 0)] [InlineData(1, 4)]