Skip to content

Merge HTTP/2 and HTTP/3 request cookies on Kestrel #41591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca2d931
Merge cookies into single string (fixes issue #26461)
Daniel-Genkin-MS-2 May 6, 2022
f3edf8f
Implemented feature for http3 but test timesout
Daniel-Genkin-MS-2 May 6, 2022
e6d6b67
Fixed the test in Http3
Daniel-Genkin-MS-2 May 9, 2022
6ab232c
Improved design with a cast that avoids raw dictionary operations the…
Daniel-Genkin-MS-2 May 9, 2022
5e064da
Addressed PR comments
Daniel-Genkin-MS-2 May 9, 2022
547eb39
removed duplicate blank lines
Daniel-Genkin-MS-2 May 9, 2022
30aab36
Moved cookie merging into generated HttpHeaders file and removed perf…
Daniel-Genkin-MS-2 May 10, 2022
0ce8ac4
Removed redundant space
Daniel-Genkin-MS-2 May 10, 2022
69182cd
Removed comment about string.Join performance
Daniel-Genkin-MS-2 May 10, 2022
c1febae
Implemented some optimizations but with little improvement
Daniel-Genkin-MS-2 May 10, 2022
312955b
unhardcoded the bit comparison for checking if cookies are present
Daniel-Genkin-MS-2 May 11, 2022
d009f47
Removed aggressive optimization flags
Daniel-Genkin-MS-2 May 11, 2022
6787943
As per Stephen's suggestion, I moved the MergeCookies function out of…
Daniel-Genkin-MS-2 May 11, 2022
ec4a483
removed unnecessary brackets. Not sure where they came form.
Daniel-Genkin-MS-2 May 11, 2022
4702f40
Fixed Benchmark (feat. Stephen)
Daniel-Genkin-MS-2 May 11, 2022
5e2e509
Added cookies params to the benchmark
Daniel-Genkin-MS-2 May 12, 2022
3d6de08
Fixed formatting as per VS lightbulbs
Daniel-Genkin-MS-2 May 12, 2022
d0a3169
Store returned instance as per Chris' recommendation
Daniel-Genkin-MS-2 May 12, 2022
ed7452b
Fixed the build error
Daniel-Genkin-MS-2 May 12, 2022
28e767d
Stephen's suggestion to convert the IEnumerator to an array and impli…
Daniel-Genkin-MS-2 May 12, 2022
3d4dc15
Improved performance of the loop that builds cookies
Daniel-Genkin-MS-2 May 12, 2022
404d3ee
Update src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2Connection…
Daniel-Genkin-MS-2 May 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Expand All @@ -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();

Expand All @@ -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
Expand All @@ -89,45 +96,45 @@ 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]
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;
}
Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/Servers/Kestrel/shared/KnownHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ static KnownHeaders()
};
var requestHeadersExistence = new[]
{
HeaderNames.Cookie,
HeaderNames.Connection,
HeaderNames.TransferEncoding,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Cookie, "a=0"),
new KeyValuePair<string, string>(HeaderNames.Cookie, "b=1"),
new KeyValuePair<string, string>(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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,48 @@ await Http3Api.InitializeConnectionAsync(async context =>
await requestStream.ExpectReceiveEndOfStream();
}

[Fact]
public async Task HEADERS_CookiesMergedIntoOne()
{
var requestHeaders = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Cookie, "a=0"),
new KeyValuePair<string, string>(HeaderNames.Cookie, "b=1"),
new KeyValuePair<string, string>(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)]
Expand Down