Replat on HTTP/3 changes, fixing up minor nits to be compatible with h3-25 (#18912)
This commit is contained in:
parent
f7dc095e9a
commit
6042fab581
|
|
@ -239,6 +239,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
void OnHeader(System.ReadOnlySpan<byte> name, System.ReadOnlySpan<byte> value);
|
||||
void OnHeadersComplete(bool endStream);
|
||||
void OnStaticIndexedHeader(int index);
|
||||
void OnStaticIndexedHeader(int index, System.ReadOnlySpan<byte> value);
|
||||
}
|
||||
public partial interface IHttpRequestLineHandler
|
||||
{
|
||||
|
|
|
|||
|
|
@ -581,4 +581,4 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
|||
<data name="TransportNotFound" xml:space="preserve">
|
||||
<value>Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'.</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -49,5 +49,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
// TODO allow customization of this.
|
||||
if (ServerOptions.EnableAltSvc && _httpVersion < Http.HttpVersion.Http3)
|
||||
{
|
||||
foreach (var option in ServerOptions.ListenOptions)
|
||||
{
|
||||
if (option.Protocols == HttpProtocols.Http3)
|
||||
{
|
||||
responseHeaders.HeaderAltSvc = $"h3-25=\":{option.IPEndPoint.Port}\"; ma=84600";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ServerOptions.AddServerHeader && !responseHeaders.HasServer)
|
||||
{
|
||||
responseHeaders.SetRawServer(Constants.ServerName, _bytesServer);
|
||||
|
|
|
|||
|
|
@ -1375,6 +1375,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class StreamCloseAwaitable : ICriticalNotifyCompletion
|
||||
{
|
||||
private static readonly Action _callbackCompleted = () => { };
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
{
|
||||
internal enum Http3FrameType
|
||||
{
|
||||
Data = 0x0,
|
||||
Headers = 0x1,
|
||||
CancelPush = 0x3,
|
||||
Settings = 0x4,
|
||||
PushPromise = 0x5,
|
||||
GoAway = 0x7,
|
||||
MaxPushId = 0xD,
|
||||
DuplicatePush = 0xE
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal partial class Http3Frame
|
||||
internal partial class Http3RawFrame
|
||||
{
|
||||
public void PrepareData()
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal partial class Http3Frame
|
||||
internal partial class Http3RawFrame
|
||||
{
|
||||
public void PrepareGoAway()
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal partial class Http3Frame
|
||||
internal partial class Http3RawFrame
|
||||
{
|
||||
public void PrepareHeaders()
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal partial class Http3Frame
|
||||
internal partial class Http3RawFrame
|
||||
{
|
||||
public void PrepareSettings()
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal partial class Http3Frame
|
||||
internal partial class Http3RawFrame
|
||||
{
|
||||
public long Length { get; set; }
|
||||
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
// 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.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
{
|
||||
/// <summary>
|
||||
/// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16.
|
||||
/// Either will take up 1, 2, 4, or 8 bytes.
|
||||
/// </summary>
|
||||
internal static class VariableLengthIntegerHelper
|
||||
{
|
||||
private const int TwoByteSubtract = 0x4000;
|
||||
private const uint FourByteSubtract = 0x80000000;
|
||||
private const ulong EightByteSubtract = 0xC000000000000000;
|
||||
private const int OneByteLimit = 64;
|
||||
private const int TwoByteLimit = 16383;
|
||||
private const int FourByteLimit = 1073741823;
|
||||
|
||||
public static long GetInteger(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The first two bits of the first byte represent the length of the
|
||||
// variable length integer
|
||||
// 00 = length 1
|
||||
// 01 = length 2
|
||||
// 10 = length 4
|
||||
// 11 = length 8
|
||||
|
||||
var span = buffer.Slice(0, Math.Min(buffer.Length, 8)).ToSpan();
|
||||
|
||||
var firstByte = span[0];
|
||||
|
||||
if ((firstByte & 0xC0) == 0)
|
||||
{
|
||||
consumed = examined = buffer.GetPosition(1);
|
||||
return firstByte & 0x3F;
|
||||
}
|
||||
else if ((firstByte & 0xC0) == 0x40)
|
||||
{
|
||||
if (span.Length < 2)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
consumed = examined = buffer.GetPosition(2);
|
||||
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(span) - TwoByteSubtract;
|
||||
}
|
||||
else if ((firstByte & 0xC0) == 0x80)
|
||||
{
|
||||
if (span.Length < 4)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
consumed = examined = buffer.GetPosition(4);
|
||||
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(span) - FourByteSubtract;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (span.Length < 8)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
consumed = examined = buffer.GetPosition(8);
|
||||
|
||||
return (long)(BinaryPrimitives.ReadUInt64BigEndian(span) - EightByteSubtract);
|
||||
}
|
||||
}
|
||||
|
||||
public static int WriteInteger(Span<byte> buffer, long longToEncode)
|
||||
{
|
||||
Debug.Assert(buffer.Length >= 8);
|
||||
Debug.Assert(longToEncode < long.MaxValue / 2);
|
||||
|
||||
if (longToEncode < OneByteLimit)
|
||||
{
|
||||
buffer[0] = (byte)longToEncode;
|
||||
return 1;
|
||||
}
|
||||
else if (longToEncode < TwoByteLimit)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)longToEncode);
|
||||
buffer[0] += 0x40;
|
||||
return 2;
|
||||
}
|
||||
else if (longToEncode < FourByteLimit)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)longToEncode);
|
||||
buffer[0] += 0x80;
|
||||
return 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong)longToEncode);
|
||||
buffer[0] += 0xC0;
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
|
@ -20,15 +22,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
public DynamicTable DynamicTable { get; set; }
|
||||
|
||||
public Http3ControlStream SettingsStream { get; set; }
|
||||
public Http3ControlStream ControlStream { get; set; }
|
||||
public Http3ControlStream EncoderStream { get; set; }
|
||||
public Http3ControlStream DecoderStream { get; set; }
|
||||
|
||||
private readonly ConcurrentDictionary<long, Http3Stream> _streams = new ConcurrentDictionary<long, Http3Stream>();
|
||||
|
||||
// To be used by GO_AWAY
|
||||
private long _highestOpenedStreamId; // TODO lock to access
|
||||
//private volatile bool _haveSentGoAway;
|
||||
private volatile bool _haveSentGoAway;
|
||||
private object _sync = new object();
|
||||
|
||||
public Http3Connection(HttpConnectionContext context)
|
||||
{
|
||||
|
|
@ -56,25 +58,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
var streamListenerFeature = Context.ConnectionFeatures.Get<IQuicStreamListenerFeature>();
|
||||
|
||||
// Start other three unidirectional streams here.
|
||||
var settingsStream = CreateSettingsStream(application);
|
||||
var encoderStream = CreateEncoderStream(application);
|
||||
var decoderStream = CreateDecoderStream(application);
|
||||
var controlTask = CreateControlStream(application);
|
||||
var encoderTask = CreateEncoderStream(application);
|
||||
var decoderTask = CreateDecoderStream(application);
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var connectionContext = await streamListenerFeature.AcceptAsync();
|
||||
if (connectionContext == null)
|
||||
if (connectionContext == null || _haveSentGoAway)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
//if (_haveSentGoAway)
|
||||
//{
|
||||
// // error here.
|
||||
//}
|
||||
|
||||
var httpConnectionContext = new HttpConnectionContext
|
||||
{
|
||||
ConnectionId = connectionContext.ConnectionId,
|
||||
|
|
@ -90,16 +87,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
};
|
||||
|
||||
var streamFeature = httpConnectionContext.ConnectionFeatures.Get<IQuicStreamFeature>();
|
||||
var streamId = streamFeature.StreamId;
|
||||
HighestStreamId = streamId;
|
||||
|
||||
if (!streamFeature.CanWrite)
|
||||
{
|
||||
// Unidirectional stream
|
||||
var stream = new Http3ControlStream<TContext>(application, this, httpConnectionContext);
|
||||
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep track of highest stream id seen for GOAWAY
|
||||
var streamId = streamFeature.StreamId;
|
||||
|
||||
HighestStreamId = streamId;
|
||||
|
||||
var http3Stream = new Http3Stream<TContext>(application, this, httpConnectionContext);
|
||||
var stream = http3Stream;
|
||||
_streams[streamId] = http3Stream;
|
||||
|
|
@ -109,20 +110,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
finally
|
||||
{
|
||||
await settingsStream;
|
||||
await encoderStream;
|
||||
await decoderStream;
|
||||
// Abort all streams as connection has shutdown.
|
||||
foreach (var stream in _streams.Values)
|
||||
{
|
||||
stream.Abort(new ConnectionAbortedException(""));
|
||||
stream.Abort(new ConnectionAbortedException("Connection is shutting down."));
|
||||
}
|
||||
|
||||
ControlStream.Abort(new ConnectionAbortedException("Connection is shutting down."));
|
||||
EncoderStream.Abort(new ConnectionAbortedException("Connection is shutting down."));
|
||||
DecoderStream.Abort(new ConnectionAbortedException("Connection is shutting down."));
|
||||
|
||||
await controlTask;
|
||||
await encoderTask;
|
||||
await decoderTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask CreateSettingsStream<TContext>(IHttpApplication<TContext> application)
|
||||
private async ValueTask CreateControlStream<TContext>(IHttpApplication<TContext> application)
|
||||
{
|
||||
var stream = await CreateNewUnidirectionalStreamAsync(application);
|
||||
SettingsStream = stream;
|
||||
ControlStream = stream;
|
||||
await stream.SendStreamIdAsync(id: 0);
|
||||
await stream.SendSettingsFrameAsync();
|
||||
}
|
||||
|
|
@ -146,7 +153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
var connectionContext = await Context.ConnectionFeatures.Get<IQuicCreateStreamFeature>().StartUnidirectionalStreamAsync();
|
||||
var httpConnectionContext = new HttpConnectionContext
|
||||
{
|
||||
ConnectionId = connectionContext.ConnectionId,
|
||||
//ConnectionId = "", TODO getting stream ID from stream that isn't started throws an exception.
|
||||
ConnectionContext = connectionContext,
|
||||
Protocols = Context.Protocols,
|
||||
ServiceContext = Context.ServiceContext,
|
||||
|
|
@ -183,8 +190,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
public void Abort(ConnectionAbortedException ex)
|
||||
{
|
||||
// Send goaway
|
||||
lock (_sync)
|
||||
{
|
||||
if (ControlStream != null)
|
||||
{
|
||||
// TODO need to await this somewhere or allow this to be called elsewhere?
|
||||
ControlStream.SendGoAway(_highestOpenedStreamId).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
_haveSentGoAway = true;
|
||||
|
||||
// Abort currently active streams
|
||||
foreach (var stream in _streams.Values)
|
||||
{
|
||||
stream.Abort(new ConnectionAbortedException("The Http3Connection has been aborted"), Http3ErrorCode.UnexpectedFrame);
|
||||
}
|
||||
// TODO need to figure out if there is server initiated connection close rather than stream close?
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
|
@ -17,10 +19,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
private const int ControlStream = 0;
|
||||
private const int EncoderStream = 2;
|
||||
private const int DecoderStream = 3;
|
||||
|
||||
private Http3FrameWriter _frameWriter;
|
||||
private readonly Http3Connection _http3Connection;
|
||||
private HttpConnectionContext _context;
|
||||
private readonly Http3Frame _incomingFrame = new Http3Frame();
|
||||
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
|
||||
private volatile int _isClosed;
|
||||
private int _gracefulCloseInitiator;
|
||||
|
||||
|
|
@ -53,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
public void Abort(ConnectionAbortedException ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void HandleReadDataRateTimeout()
|
||||
|
|
@ -117,11 +121,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
internal async ValueTask SendStreamIdAsync(int id)
|
||||
internal async ValueTask SendStreamIdAsync(long id)
|
||||
{
|
||||
await _frameWriter.WriteStreamIdAsync(id);
|
||||
}
|
||||
|
||||
internal async ValueTask SendGoAway(long id)
|
||||
{
|
||||
await _frameWriter.WriteGoAway(id);
|
||||
}
|
||||
|
||||
internal async ValueTask SendSettingsFrameAsync()
|
||||
{
|
||||
await _frameWriter.WriteSettingsAsync(null);
|
||||
|
|
@ -172,7 +181,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
if (streamType == ControlStream)
|
||||
{
|
||||
if (_http3Connection.SettingsStream != null)
|
||||
if (_http3Connection.ControlStream != null)
|
||||
{
|
||||
throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
|
||||
}
|
||||
|
|
@ -255,7 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
case Http3FrameType.Settings:
|
||||
return ProcessSettingsFrameAsync(payload);
|
||||
case Http3FrameType.GoAway:
|
||||
return ProcessGoAwayFrameAsync();
|
||||
return ProcessGoAwayFrameAsync(payload);
|
||||
case Http3FrameType.CancelPush:
|
||||
return ProcessCancelPushFrameAsync();
|
||||
case Http3FrameType.MaxPushId:
|
||||
|
|
@ -318,14 +327,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
private ValueTask ProcessGoAwayFrameAsync()
|
||||
private ValueTask ProcessGoAwayFrameAsync(ReadOnlySequence<byte> payload)
|
||||
{
|
||||
if (!_haveReceivedSettingsFrame)
|
||||
{
|
||||
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
|
||||
}
|
||||
// Get highest stream id and write that to the response.
|
||||
return default;
|
||||
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
|
||||
}
|
||||
|
||||
private ValueTask ProcessCancelPushFrameAsync()
|
||||
|
|
@ -334,7 +338,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
{
|
||||
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
|
||||
}
|
||||
// This should just noop.
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
|
|
@ -344,6 +348,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
{
|
||||
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
{
|
||||
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
|
|
@ -369,10 +375,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
public void Tick(DateTimeOffset now)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to kick off the request processing loop by derived classes.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
{
|
||||
|
|
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
| Frame Payload (*) ...
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
internal static bool TryReadFrame(ref ReadOnlySequence<byte> readableBuffer, Http3Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
|
||||
internal static bool TryReadFrame(ref ReadOnlySequence<byte> readableBuffer, Http3RawFrame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
|
||||
{
|
||||
framePayload = ReadOnlySequence<byte>.Empty;
|
||||
var consumed = readableBuffer.Start;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ using System.Buffers;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.QPack;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
|
@ -28,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
private readonly MinDataRate _minResponseDataRate;
|
||||
private readonly MemoryPool<byte> _memoryPool;
|
||||
private readonly IKestrelTrace _log;
|
||||
private readonly Http3Frame _outgoingFrame;
|
||||
private readonly Http3RawFrame _outgoingFrame;
|
||||
private readonly TimingPipeFlusher _flusher;
|
||||
|
||||
// TODO update max frame size
|
||||
|
|
@ -49,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
_minResponseDataRate = minResponseDataRate;
|
||||
_memoryPool = memoryPool;
|
||||
_log = log;
|
||||
_outgoingFrame = new Http3Frame();
|
||||
_outgoingFrame = new Http3RawFrame();
|
||||
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log);
|
||||
_headerEncodingBuffer = new byte[_maxFrameSize];
|
||||
}
|
||||
|
|
@ -164,6 +166,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
internal Task WriteGoAway(long id)
|
||||
{
|
||||
_outgoingFrame.PrepareGoAway();
|
||||
var buffer = _outputWriter.GetSpan(9);
|
||||
buffer[0] = (byte)_outgoingFrame.Type;
|
||||
|
||||
var length = VariableLengthIntegerHelper.WriteInteger(buffer.Slice(1), id);
|
||||
|
||||
_outgoingFrame.Length = length;
|
||||
|
||||
WriteHeaderUnsynchronized();
|
||||
|
||||
return _outputWriter.FlushAsync().AsTask();
|
||||
}
|
||||
|
||||
private void WriteHeaderUnsynchronized()
|
||||
{
|
||||
var headerLength = WriteHeader(_outgoingFrame, _outputWriter);
|
||||
|
|
@ -172,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
_unflushedBytes += headerLength + _outgoingFrame.Length;
|
||||
}
|
||||
|
||||
internal static int WriteHeader(Http3Frame frame, PipeWriter output)
|
||||
internal static int WriteHeader(Http3RawFrame frame, PipeWriter output)
|
||||
{
|
||||
// max size of the header is 16, most likely it will be smaller.
|
||||
var buffer = output.GetSpan(16);
|
||||
|
|
@ -189,7 +206,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
return totalLength;
|
||||
}
|
||||
|
||||
public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
|
||||
public ValueTask<FlushResult> WriteResponseTrailers(HttpResponseTrailers headers)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
|
|
@ -203,7 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
_outgoingFrame.PrepareHeaders();
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var done = _qpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength);
|
||||
FinishWritingHeaders(streamId, payloadLength, done);
|
||||
FinishWritingHeaders(payloadLength, done);
|
||||
}
|
||||
catch (QPackEncodingException)
|
||||
{
|
||||
|
|
@ -239,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
internal void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers)
|
||||
internal void WriteResponseHeaders(int statusCode, IHeaderDictionary headers)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
|
|
@ -253,7 +270,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
_outgoingFrame.PrepareHeaders();
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var done = _qpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength);
|
||||
FinishWritingHeaders(streamId, payloadLength, done);
|
||||
FinishWritingHeaders(payloadLength, done);
|
||||
}
|
||||
catch (QPackEncodingException hex)
|
||||
{
|
||||
|
|
@ -264,7 +281,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
}
|
||||
|
||||
private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
|
||||
private void FinishWritingHeaders(int payloadLength, bool done)
|
||||
{
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
_outgoingFrame.Length = payloadLength;
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWrite
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
{
|
||||
internal class Http3OutputProducer : IHttpOutputProducer, IHttpOutputAborter
|
||||
{
|
||||
private readonly int _streamId;
|
||||
private readonly Http3FrameWriter _frameWriter;
|
||||
private readonly TimingPipeFlusher _flusher;
|
||||
private readonly IKestrelTrace _log;
|
||||
|
|
@ -35,13 +35,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
private IMemoryOwner<byte> _fakeMemoryOwner;
|
||||
|
||||
public Http3OutputProducer(
|
||||
int streamId,
|
||||
Http3FrameWriter frameWriter,
|
||||
MemoryPool<byte> pool,
|
||||
Http3Stream stream,
|
||||
IKestrelTrace log)
|
||||
{
|
||||
_streamId = streamId;
|
||||
_frameWriter = frameWriter;
|
||||
_memoryPool = pool;
|
||||
_stream = stream;
|
||||
|
|
@ -308,7 +306,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
// TODO figure out something to do here.
|
||||
}
|
||||
|
||||
_frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders);
|
||||
_frameWriter.WriteResponseHeaders(statusCode, responseHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +348,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
|
||||
_stream.ResponseTrailers.SetReadOnly();
|
||||
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers);
|
||||
flushResult = await _frameWriter.WriteResponseTrailers(_stream.ResponseTrailers);
|
||||
}
|
||||
else if (readResult.IsCompleted)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ using System.Buffers;
|
|||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.QPack;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
private int _isClosed;
|
||||
private int _gracefulCloseInitiator;
|
||||
private readonly HttpConnectionContext _context;
|
||||
private readonly Http3Frame _incomingFrame = new Http3Frame();
|
||||
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
|
||||
|
||||
private readonly Http3Connection _http3Connection;
|
||||
private bool _receivedHeaders;
|
||||
|
|
@ -53,16 +54,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
Reset();
|
||||
|
||||
_http3Output = new Http3OutputProducer(
|
||||
0, // TODO streamid
|
||||
_frameWriter,
|
||||
context.MemoryPool,
|
||||
this,
|
||||
context.ServiceContext.Log);
|
||||
RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize?
|
||||
Output = _http3Output;
|
||||
QPackDecoder = new QPackDecoder(_context.ServiceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize);
|
||||
}
|
||||
|
||||
public QPackDecoder QPackDecoder { get; set; } = new QPackDecoder(10000, 10000);
|
||||
public QPackDecoder QPackDecoder { get; }
|
||||
|
||||
public PipeReader Input => _context.Transport.Input;
|
||||
|
||||
|
|
@ -76,6 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode)
|
||||
{
|
||||
// TODO something with request aborted?
|
||||
}
|
||||
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
|
|
@ -83,6 +85,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
OnHeadersComplete();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
var knownHeader = H3StaticTable.Instance[index];
|
||||
OnHeader(knownHeader.Name, knownHeader.Value);
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
var knownHeader = H3StaticTable.Instance[index];
|
||||
OnHeader(knownHeader.Name, value);
|
||||
}
|
||||
|
||||
public void HandleReadDataRateTimeout()
|
||||
{
|
||||
Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond);
|
||||
|
|
@ -114,6 +128,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application)
|
||||
{
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (_isClosed == 0)
|
||||
|
|
@ -139,23 +155,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
return;
|
||||
}
|
||||
}
|
||||
catch (Http3StreamErrorException)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
Input.AdvanceTo(consumed, examined);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO
|
||||
error = ex;
|
||||
Log.LogWarning(0, ex, "Stream threw an exception.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await RequestBodyPipe.Writer.CompleteAsync();
|
||||
var streamError = error as ConnectionAbortedException
|
||||
?? new ConnectionAbortedException("The stream has completed.", error);
|
||||
try
|
||||
{
|
||||
_frameWriter.Complete();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_frameWriter.Abort(streamError);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Input.Complete();
|
||||
_context.Transport.Input.CancelPendingRead();
|
||||
await RequestBodyPipe.Writer.CompleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
{
|
||||
if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path]))
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -296,8 +326,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
// - We'll need to find some concrete scenarios to warrant unblocking this.
|
||||
if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(
|
||||
// CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(
|
||||
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -325,7 +355,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
var requestLineLength = _methodText.Length + Scheme.Length + hostText.Length + path.Length;
|
||||
if (requestLineLength > ServerOptions.Limits.MaxRequestLineSize)
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -346,8 +376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
|
||||
if (Method == Http.HttpMethod.None)
|
||||
{
|
||||
// TODO
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
{
|
||||
if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0)
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -395,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText))
|
||||
{
|
||||
// RST replaces 400
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -407,7 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
// Must start with a leading slash
|
||||
if (pathSegment.Length == 0 || pathSegment[0] != '/')
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +466,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
|||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
|
||||
// TODO change HTTP/2 specific messages to include HTTP/3
|
||||
Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http.QPack;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Net.Http.QPack;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
{
|
||||
|
|
@ -317,11 +318,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
_state = State.Ready;
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index)
|
||||
private System.Net.Http.QPack.HeaderField GetHeader(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _s ? StaticTable.Instance[index] : _dynamicTable[index];
|
||||
return _s ? H3StaticTable.Instance[index] : _dynamicTable[index];
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
// 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.Core.Internal.Http3.QPack
|
||||
{
|
||||
internal readonly struct HeaderField
|
||||
{
|
||||
public HeaderField(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
Name = new byte[name.Length];
|
||||
name.CopyTo(Name);
|
||||
|
||||
Value = new byte[value.Length];
|
||||
value.CopyTo(Value);
|
||||
}
|
||||
|
||||
public byte[] Name { get; }
|
||||
|
||||
public byte[] Value { get; }
|
||||
|
||||
public int Length => GetLength(Name.Length, Value.Length);
|
||||
|
||||
public static int GetLength(int nameLength, int valueLength) => nameLength + valueLength;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// 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.Runtime.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
{
|
||||
[Serializable]
|
||||
internal class QPackDecodingException : Exception
|
||||
{
|
||||
public QPackDecodingException()
|
||||
{
|
||||
}
|
||||
|
||||
public QPackDecodingException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public QPackDecodingException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected QPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,504 +0,0 @@
|
|||
// 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 System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
{
|
||||
internal class QPackEncoder
|
||||
{
|
||||
private IEnumerator<KeyValuePair<string, string>> _enumerator;
|
||||
|
||||
// TODO these all need to be updated!
|
||||
|
||||
/*
|
||||
* 0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| 1 | S | Index (6+) |
|
||||
+---+---+-----------------------+
|
||||
*/
|
||||
public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
EncodeHeaderBlockPrefix(destination, out bytesWritten);
|
||||
destination = destination.Slice(bytesWritten);
|
||||
|
||||
return IntegerEncoder.Encode(index, 6, destination, out bytesWritten);
|
||||
}
|
||||
|
||||
public static bool EncodeIndexHeaderFieldWithPostBaseIndex(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Encodes a "Literal Header Field without Indexing".</summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithNameReference(int index, string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
EncodeHeaderBlockPrefix(destination, out bytesWritten);
|
||||
destination = destination.Slice(bytesWritten);
|
||||
|
||||
return IntegerEncoder.Encode(index, 6, destination, out bytesWritten);
|
||||
}
|
||||
|
||||
/*
|
||||
* 0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| 0 | 1 | N | S |Name Index (4+)|
|
||||
+---+---+---+---+---------------+
|
||||
| H | Value Length (7+) |
|
||||
+---+---------------------------+
|
||||
| Value String (Length bytes) |
|
||||
+-------------------------------+
|
||||
*/
|
||||
public static bool EncodeLiteralHeaderFieldWithPostBaseNameReference(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool EncodeLiteralHeaderFieldWithoutNameReference(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* 0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| Required Insert Count (8+) |
|
||||
+---+---------------------------+
|
||||
| S | Delta Base (7+) |
|
||||
+---+---------------------------+
|
||||
| Compressed Headers ...
|
||||
+-------------------------------+
|
||||
*
|
||||
*/
|
||||
private static bool EncodeHeaderBlockPrefix(Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
int length;
|
||||
bytesWritten = 0;
|
||||
// Required insert count as first int
|
||||
if (!IntegerEncoder.Encode(0, 8, destination, out length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesWritten += length;
|
||||
destination = destination.Slice(length);
|
||||
|
||||
// Delta base
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination[0] = 0x00;
|
||||
if (!IntegerEncoder.Encode(0, 7, destination, out length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesWritten += length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool EncodeLiteralHeaderName(string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-5.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | String Data (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if (!destination.IsEmpty)
|
||||
{
|
||||
destination[0] = 0; // TODO: Use Huffman encoding
|
||||
if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
|
||||
{
|
||||
Debug.Assert(integerLength >= 1);
|
||||
|
||||
destination = destination.Slice(integerLength);
|
||||
if (value.Length <= destination.Length)
|
||||
{
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
destination[i] = (byte)((uint)(c - 'A') <= ('Z' - 'A') ? c | 0x20 : c);
|
||||
}
|
||||
|
||||
bytesWritten = integerLength + value.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EncodeStringLiteralValue(string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (value.Length <= destination.Length)
|
||||
{
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
if ((c & 0xFF80) != 0)
|
||||
{
|
||||
throw new HttpRequestException("");
|
||||
}
|
||||
|
||||
destination[i] = (byte)c;
|
||||
}
|
||||
|
||||
bytesWritten = value.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool EncodeStringLiteral(string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-5.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | String Data (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if (!destination.IsEmpty)
|
||||
{
|
||||
destination[0] = 0; // TODO: Use Huffman encoding
|
||||
if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
|
||||
{
|
||||
Debug.Assert(integerLength >= 1);
|
||||
|
||||
if (EncodeStringLiteralValue(value, destination.Slice(integerLength), out int valueLength))
|
||||
{
|
||||
bytesWritten = integerLength + valueLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool EncodeStringLiterals(string[] values, string separator, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
|
||||
if (values.Length == 0)
|
||||
{
|
||||
return EncodeStringLiteral("", destination, out bytesWritten);
|
||||
}
|
||||
else if (values.Length == 1)
|
||||
{
|
||||
return EncodeStringLiteral(values[0], destination, out bytesWritten);
|
||||
}
|
||||
|
||||
if (!destination.IsEmpty)
|
||||
{
|
||||
int valueLength = 0;
|
||||
|
||||
// Calculate length of all parts and separators.
|
||||
foreach (string part in values)
|
||||
{
|
||||
valueLength = checked((int)(valueLength + part.Length));
|
||||
}
|
||||
|
||||
valueLength = checked((int)(valueLength + (values.Length - 1) * separator.Length));
|
||||
|
||||
if (IntegerEncoder.Encode(valueLength, 7, destination, out int integerLength))
|
||||
{
|
||||
Debug.Assert(integerLength >= 1);
|
||||
|
||||
int encodedLength = 0;
|
||||
for (int j = 0; j < values.Length; j++)
|
||||
{
|
||||
if (j != 0 && !EncodeStringLiteralValue(separator, destination.Slice(integerLength), out encodedLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
integerLength += encodedLength;
|
||||
|
||||
if (!EncodeStringLiteralValue(values[j], destination.Slice(integerLength), out encodedLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
integerLength += encodedLength;
|
||||
}
|
||||
|
||||
bytesWritten = integerLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion;
|
||||
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
|
||||
/// </summary>
|
||||
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index)
|
||||
{
|
||||
Span<byte> span = stackalloc byte[256];
|
||||
bool success = EncodeLiteralHeaderFieldWithPostBaseNameReference(index, span, out int length);
|
||||
Debug.Assert(success, $"Stack-allocated space was too small for index '{index}'.");
|
||||
return span.Slice(0, length).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion;
|
||||
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
|
||||
/// </summary>
|
||||
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name)
|
||||
{
|
||||
Span<byte> span = stackalloc byte[256];
|
||||
bool success = EncodeLiteralHeaderFieldWithoutIndexingNewName(name, span, out int length);
|
||||
Debug.Assert(success, $"Stack-allocated space was too small for \"{name}\".");
|
||||
return span.Slice(0, length).ToArray();
|
||||
}
|
||||
|
||||
private static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span<byte> span, out int length)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>Encodes a "Literal Header Field without Indexing" to a new array.</summary>
|
||||
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index, string value)
|
||||
{
|
||||
Span<byte> span =
|
||||
#if DEBUG
|
||||
stackalloc byte[4]; // to validate growth algorithm
|
||||
#else
|
||||
stackalloc byte[512];
|
||||
#endif
|
||||
while (true)
|
||||
{
|
||||
if (EncodeLiteralHeaderFieldWithNameReference(index, value, span, out int length))
|
||||
{
|
||||
return span.Slice(0, length).ToArray();
|
||||
}
|
||||
|
||||
// This is a rare path, only used once per HTTP/2 connection and only
|
||||
// for very long host names. Just allocate rather than complicate
|
||||
// the code with ArrayPool usage. In practice we should never hit this,
|
||||
// as hostnames should be <= 255 characters.
|
||||
span = new byte[span.Length * 2];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO these are fairly hard coded for the first two bytes to be zero.
|
||||
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
buffer[0] = 0;
|
||||
buffer[1] = 0;
|
||||
|
||||
return Encode(buffer.Slice(2), out length);
|
||||
}
|
||||
|
||||
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix
|
||||
buffer[0] = 0;
|
||||
buffer[1] = 0;
|
||||
|
||||
var statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2));
|
||||
var done = Encode(buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, out var headersLength);
|
||||
length = statusCodeLength + headersLength + 2;
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
public bool Encode(Span<byte> buffer, out int length)
|
||||
{
|
||||
return Encode(buffer, throwIfNoneEncoded: true, out length);
|
||||
}
|
||||
|
||||
private bool Encode(Span<byte> buffer, bool throwIfNoneEncoded, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength))
|
||||
{
|
||||
if (length == 0 && throwIfNoneEncoded)
|
||||
{
|
||||
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
length += headerLength;
|
||||
} while (_enumerator.MoveNext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length)
|
||||
{
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeNameString(name, buffer.Slice(i), out var nameLength, lowercase: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeValueString(value, buffer.Slice(i), out var valueLength, lowercase: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += valueLength;
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeValueString(string s, Span<byte> buffer, out int length, bool lowercase)
|
||||
{
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[0] = 0;
|
||||
|
||||
if (!IntegerEncoder.Encode(s.Length, 7, buffer, out var nameLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
// TODO: use huffman encoding
|
||||
for (var j = 0; j < s.Length; j++)
|
||||
{
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0));
|
||||
}
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeNameString(string s, Span<byte> buffer, out int length, bool lowercase)
|
||||
{
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[0] = 0x30;
|
||||
|
||||
if (!IntegerEncoder.Encode(s.Length, 3, buffer, out var nameLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
// TODO: use huffman encoding
|
||||
for (var j = 0; j < s.Length; j++)
|
||||
{
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0));
|
||||
}
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int EncodeStatusCode(int statusCode, Span<byte> buffer)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case 200:
|
||||
case 204:
|
||||
case 206:
|
||||
case 304:
|
||||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
// TODO this isn't safe, some index can be larger than 64. Encoded here!
|
||||
buffer[0] = (byte)(0xC0 | StaticTable.Instance.StatusIndex[statusCode]);
|
||||
return 1;
|
||||
default:
|
||||
// Send as Literal Header Field Without Indexing - Indexed Name
|
||||
buffer[0] = 0x08;
|
||||
|
||||
var statusBytes = StatusCodes.ToStatusBytes(statusCode);
|
||||
buffer[1] = (byte)statusBytes.Length;
|
||||
((ReadOnlySpan<byte>)statusBytes).CopyTo(buffer.Slice(2));
|
||||
|
||||
return 2 + statusBytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// 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.Core.Internal.Http3.QPack
|
||||
{
|
||||
internal sealed class QPackEncodingException : Exception
|
||||
{
|
||||
public QPackEncodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
public QPackEncodingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
}
|
||||
else
|
||||
{
|
||||
factory = _transportFactories.Last();
|
||||
foreach (var transportFactory in _transportFactories)
|
||||
{
|
||||
if (!(transportFactory is IMultiplexedConnectionListenerFactory))
|
||||
{
|
||||
factory = transportFactory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Http2\**\*.cs" LinkBase="Shared\Http2\" />
|
||||
<Compile Include="$(SharedSourceRoot)Http3\**\*.cs" LinkBase="Shared\Http3\" />
|
||||
<Compile Include="$(SharedSourceRoot)ServerInfrastructure\**\*.cs" LinkBase="Shared\" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\TaskToApm.cs" Link="Internal\TaskToApm.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -523,6 +523,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Query = query.GetAsciiStringNonNullCharacters();
|
||||
PathEncoded = pathEncoded;
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
// Doesn't put empty blocks in between every byte
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
|
||||
using System.Net.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
|
|||
public async ValueTask<ConnectionContext> AcceptAsync()
|
||||
{
|
||||
var stream = await _connection.AcceptStreamAsync();
|
||||
try
|
||||
{
|
||||
// Because the stream is wrapped with a quic connection provider,
|
||||
// we need to check a property to check if this is null
|
||||
// Will be removed once the provider abstraction is removed.
|
||||
_ = stream.CanRead;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QuicStreamContext(stream, this, _context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
|
|||
public async ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// Because the stream is wrapped with a quic connection provider,
|
||||
// we need to check a property to check if this is null
|
||||
// Will be removed once the provider abstraction is removed.
|
||||
_ = quicConnection.LocalEndPoint;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QuicConnectionContext(quicConnection, _context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,8 +95,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
|
|||
try
|
||||
{
|
||||
// Spawn send and receive logic
|
||||
var receiveTask = DoReceive();
|
||||
var sendTask = DoSend();
|
||||
// Streams may or may not have reading/writing, so only start tasks accordingly
|
||||
var receiveTask = Task.CompletedTask;
|
||||
var sendTask = Task.CompletedTask;
|
||||
|
||||
if (_stream.CanRead)
|
||||
{
|
||||
receiveTask = DoReceive();
|
||||
}
|
||||
|
||||
if (_stream.CanWrite)
|
||||
{
|
||||
sendTask = DoSend();
|
||||
}
|
||||
|
||||
// Now wait for both to complete
|
||||
await receiveTask;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private struct Adapter : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
public HttpParserBenchmark RequestHandler;
|
||||
|
|
@ -96,6 +106,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
|
|
@ -11,4 +11,10 @@
|
|||
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="msquic.dll" Condition="Exists('msquic.dll')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Http3SampleApp
|
|||
public static void Main(string[] args)
|
||||
{
|
||||
var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true);
|
||||
|
||||
var hostBuilder = new HostBuilder()
|
||||
.ConfigureLogging((_, factory) =>
|
||||
{
|
||||
|
|
@ -30,17 +31,27 @@ namespace Http3SampleApp
|
|||
{
|
||||
options.Certificate = cert;
|
||||
options.RegistrationName = "Quic";
|
||||
options.Alpn = "h3-24";
|
||||
options.Alpn = "h3-25";
|
||||
options.IdleTimeout = TimeSpan.FromHours(1);
|
||||
})
|
||||
.ConfigureKestrel((context, options) =>
|
||||
{
|
||||
var basePort = 5555;
|
||||
|
||||
var basePort = 443;
|
||||
options.EnableAltSvc = true;
|
||||
options.Listen(IPAddress.Any, basePort, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
listenOptions.Protocols = HttpProtocols.Http3;
|
||||
listenOptions.UseHttps(httpsOptions =>
|
||||
{
|
||||
httpsOptions.ServerCertificate = cert;
|
||||
});
|
||||
});
|
||||
options.Listen(IPAddress.Any, basePort, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps(httpsOptions =>
|
||||
{
|
||||
httpsOptions.ServerCertificate = cert;
|
||||
});
|
||||
listenOptions.Protocols = HttpProtocols.Http3;
|
||||
});
|
||||
})
|
||||
.UseStartup<Startup>();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ namespace QuicSampleApp
|
|||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
//var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true);
|
||||
var hostBuilder = new WebHostBuilder()
|
||||
.ConfigureLogging((_, factory) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1285,6 +1285,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
return bufferSize ?? 0;
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal class Http2FrameWithPayload : Http2Frame
|
||||
{
|
||||
public Http2FrameWithPayload() : base()
|
||||
|
|
|
|||
|
|
@ -30,5 +30,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var responseData = await requestStream.ExpectDataAsync();
|
||||
Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestHeadersMaxRequestHeaderFieldSize_EndsStream()
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "Custom"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
new KeyValuePair<string, string>("test", new string('a', 10000))
|
||||
};
|
||||
|
||||
var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication);
|
||||
|
||||
var doneWithHeaders = await requestStream.SendHeadersAsync(headers);
|
||||
await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world"));
|
||||
|
||||
// TODO figure out how to test errors for request streams that would be set on the Quic Stream.
|
||||
await requestStream.ExpectReceiveEndOfStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.QPack;
|
||||
using System.Reflection;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -86,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
internal async ValueTask<Http3RequestStream> InitializeConnectionAndStreamsAsync(RequestDelegate application)
|
||||
{
|
||||
await InitializeConnectionAsync(application);
|
||||
|
||||
|
||||
var controlStream1 = await CreateControlStream(0);
|
||||
var controlStream2 = await CreateControlStream(2);
|
||||
var controlStream3 = await CreateControlStream(3);
|
||||
|
|
@ -177,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
internal async ValueTask<Http3ControlStream> CreateControlStream(int id)
|
||||
{
|
||||
var stream = new Http3ControlStream(this, _connection);
|
||||
_acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext);
|
||||
_acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext);
|
||||
await stream.WriteStreamIdAsync(id);
|
||||
return stream;
|
||||
}
|
||||
|
|
@ -226,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
private readonly byte[] _headerEncodingBuffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize];
|
||||
private QPackEncoder _qpackEncoder = new QPackEncoder();
|
||||
private QPackDecoder _qpackDecoder = new QPackDecoder(10000, 10000);
|
||||
private QPackDecoder _qpackDecoder = new QPackDecoder(8192);
|
||||
private long _bytesReceived;
|
||||
protected readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
|
@ -238,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
|
||||
|
||||
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
|
||||
|
||||
|
||||
ConnectionContext = new DefaultConnectionContext();
|
||||
ConnectionContext.Transport = _pair.Transport;
|
||||
ConnectionContext.Features.Set<IQuicStreamFeature>(this);
|
||||
|
|
@ -247,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
public async Task<bool> SendHeadersAsync(IEnumerable<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http3Frame();
|
||||
var frame = new Http3RawFrame();
|
||||
frame.PrepareHeaders();
|
||||
var buffer = _headerEncodingBuffer.AsMemory();
|
||||
var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length);
|
||||
|
|
@ -261,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
internal async Task SendDataAsync(Memory<byte> data)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http3Frame();
|
||||
var frame = new Http3RawFrame();
|
||||
frame.PrepareData();
|
||||
frame.Length = data.Length;
|
||||
Http3FrameWriter.WriteHeader(frame, outputWriter);
|
||||
|
|
@ -321,6 +322,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
}
|
||||
}
|
||||
|
||||
internal async Task ExpectReceiveEndOfStream()
|
||||
{
|
||||
var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
|
||||
Assert.True(result.IsCompleted);
|
||||
}
|
||||
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
|
|
@ -329,9 +336,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
public void OnHeadersComplete(bool endHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
var knownHeader = H3StaticTable.Instance[index];
|
||||
_decodedHeaders[((Span<byte>)knownHeader.Name).GetAsciiStringNonNullCharacters()] = ((Span<byte>)knownHeader.Value).GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_decodedHeaders[((Span<byte>)H3StaticTable.Instance[index].Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
internal class Http3FrameWithPayload : Http3Frame
|
||||
internal class Http3FrameWithPayload : Http3RawFrame
|
||||
{
|
||||
public Http3FrameWithPayload() : base()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using System.Text;
|
|||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal static class StaticTable
|
||||
internal static class H2StaticTable
|
||||
{
|
||||
// Index of status code into s_staticDecoderTable
|
||||
private static readonly Dictionary<int, int> s_statusIndex = new Dictionary<int, int>
|
||||
|
|
@ -470,9 +470,9 @@ namespace System.Net.Http.HPack
|
|||
{
|
||||
try
|
||||
{
|
||||
return index <= StaticTable.Count
|
||||
? StaticTable.Get(index - 1)
|
||||
: _dynamicTable[index - StaticTable.Count - 1];
|
||||
return index <= H2StaticTable.Count
|
||||
? H2StaticTable.Get(index - 1)
|
||||
: _dynamicTable[index - H2StaticTable.Count - 1];
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace System.Net.Http.HPack
|
|||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
buffer[0] = (byte)(0x80 | StaticTable.StatusIndex[statusCode]);
|
||||
buffer[0] = (byte)(0x80 | H2StaticTable.StatusIndex[statusCode]);
|
||||
return 1;
|
||||
default:
|
||||
// Send as Literal Header Field Without Indexing - Indexed Name
|
||||
|
|
|
|||
|
|
@ -319,7 +319,8 @@ namespace System.Net.Http.HPack
|
|||
// Note that if lastDecodeBits is 3 or more, then we will only get 5 bits (or less)
|
||||
// from src[i]. Thus we need to read 5 bytes here to ensure that we always have
|
||||
// at least 30 bits available for decoding.
|
||||
// TODO ISSUE 31751: Rework this as part of Huffman perf improvements
|
||||
// TODO https://github.com/dotnet/runtime/issues/1506:
|
||||
// Rework this as part of Huffman perf improvements
|
||||
uint next = (uint)(src[i] << 24 + lastDecodedBits);
|
||||
next |= (i + 1 < src.Length ? (uint)(src[i + 1] << 16 + lastDecodedBits) : 0);
|
||||
next |= (i + 2 < src.Length ? (uint)(src[i + 2] << 8 + lastDecodedBits) : 0);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ namespace System.Net.Http.HPack
|
|||
{
|
||||
internal static class IntegerEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum bytes required to encode a 32-bit int, regardless of prefix length.
|
||||
/// </summary>
|
||||
public const int MaxInt32EncodedLength = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Encodes an integer into one or more bytes.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ namespace System.Net.Http
|
|||
#endif
|
||||
interface IHttpHeadersHandler
|
||||
{
|
||||
void OnStaticIndexedHeader(int index);
|
||||
void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value);
|
||||
void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value);
|
||||
void OnHeadersComplete(bool endStream);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
The code in this directory is shared between dotnet/runtime and dotnet/aspnetcore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories.
|
||||
The code in this directory is shared between dotnet/runtime and aspnet/AspNetCore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories.
|
||||
|
||||
dotnet/runtime code paths:
|
||||
- runtime\src\libraries\Common\src\System\Net\Http\Http2
|
||||
- runtime\src\libraries\Common\tests\Tests\System\Net\Http2
|
||||
dotnet/aspnetcore code paths:
|
||||
|
||||
aspnet/AspNetCore code paths:
|
||||
- AspNetCore\src\Shared\Http2
|
||||
- AspNetCore\src\Shared\test\Shared.Tests\Http2
|
||||
|
||||
## Copying code
|
||||
To copy code from dotnet/runtime to dotnet/aspnetcore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd.
|
||||
To copy code from dotnet/aspnetcore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd.
|
||||
- To copy code from dotnet/runtime to aspnet/AspNetCore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd.
|
||||
- To copy code from aspnet/AspNetCore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd.
|
||||
|
||||
## Building dotnet/runtime code:
|
||||
- https://github.com/dotnet/runtime/blob/master/docs/libraries/building/windows-instructions.md
|
||||
- https://github.com/dotnet/runtime/blob/master/docs/libraries/project-docs/developer-guide.md
|
||||
- https://github.com/dotnet/runtime/tree/master/docs/workflow
|
||||
- Run libraries.cmd from the root once: `PS D:\github\runtime> .\libraries.cmd`
|
||||
- Build the individual projects:
|
||||
- `PS D:\github\dotnet\src\libraries\Common\tests> dotnet msbuild /t:rebuild`
|
||||
|
|
@ -23,14 +23,14 @@ To copy code from dotnet/aspnetcore to dotnet/runtime, set RUNTIME_REPO to the r
|
|||
- `PS D:\github\runtime\src\libraries\Common\tests> dotnet msbuild /t:rebuildandtest`
|
||||
- `PS D:\github\runtime\src\libraries\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest`
|
||||
|
||||
## Building dotnet/aspnetcore code:
|
||||
- https://github.com/dotnet/aspnetcore/blob/master/docs/BuildFromSource.md
|
||||
## Building aspnet/AspNetCore code:
|
||||
- https://github.com/aspnet/AspNetCore/blob/master/docs/BuildFromSource.md
|
||||
- Run restore in the root once: `PS D:\github\AspNetCore> .\restore.cmd`
|
||||
- Activate to use the repo local runtime: `PS D:\github\AspNetCore> . .\activate.ps1`
|
||||
- Build the individual projects:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet msbuild`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\src> dotnet msbuild`
|
||||
|
||||
### Running dotnet/aspnetcore tests:
|
||||
### Running aspnet/AspNetCore tests:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet test`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\test> dotnet test`
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,16 @@ namespace Microsoft.AspNetCore.Http2Cat
|
|||
Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode);
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal class Http2FrameWithPayload : Http2Frame
|
||||
{
|
||||
public Http2FrameWithPayload() : base()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%ASPNETCORE_REPO%)
|
||||
|
||||
IF [%remote_repo%] == [] (
|
||||
echo The 'ASPNETCORE_REPO' environment variable or command line parameter is not set, aborting.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ASPNETCORE_REPO: %remote_repo%
|
||||
|
||||
robocopy . %remote_repo%\src\Shared\Http3 /MIR
|
||||
robocopy .\..\..\..\..\..\tests\Tests\System\Net\Http3\ %remote_repo%\src\Shared\test\Shared.Tests\Http3 /MIR
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
remote_repo="$1"
|
||||
else
|
||||
remote_repo="$ASPNETCORE_REPO"
|
||||
fi
|
||||
|
||||
if [[ -z "$remote_repo" ]]; then
|
||||
echo The 'ASPNETCORE_REPO' environment variable or command line parameter is not set, aborting.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
echo "ASPNETCORE_REPO: $remote_repo"
|
||||
|
||||
rsync -av --delete ./ "$remote_repo"/src/Shared/Http3
|
||||
rsync -av --delete ./../../../../../tests/Tests/System/Net/Http3/ "$remote_repo"/src/Shared/test/Shared.Tests/Http3
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%RUNTIME_REPO%)
|
||||
|
||||
IF [%remote_repo%] == [] (
|
||||
echo The 'RUNTIME_REPO' environment variable or command line parameter is not set, aborting.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo RUNTIME_REPO: %remote_repo%
|
||||
|
||||
robocopy . %remote_repo%\src\libraries\Common\src\System\Net\Http\Http3 /MIR
|
||||
robocopy .\..\test\Shared.Tests\Http3 %remote_repo%\src\libraries\Common\tests\Tests\System\Net\Http3 /MIR
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
remote_repo="$1"
|
||||
else
|
||||
remote_repo="$RUNTIME_REPO"
|
||||
fi
|
||||
|
||||
if [[ -z "$remote_repo" ]]; then
|
||||
echo The 'RUNTIME_REPO' environment variable or command line parameter is not set, aborting.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
echo "RUNTIME_REPO: $remote_repo"
|
||||
|
||||
rsync -av --delete ./ "$remote_repo"/src/libraries/Common/src/System/Net/Http/Http3
|
||||
rsync -av --delete ./../test/Shared.Tests/Http3/ "$remote_repo"/src/libraries/Common/tests/Tests/System/Net/Http3
|
||||
|
|
@ -1,101 +1,92 @@
|
|||
// 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.
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal enum Http3ErrorCode : uint
|
||||
internal enum Http3ErrorCode : long
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP_NO_ERROR (0x100):
|
||||
/// H3_NO_ERROR (0x100):
|
||||
/// No error. This is used when the connection or stream needs to be closed, but there is no error to signal.
|
||||
/// </summary>
|
||||
NoError = 0x100,
|
||||
/// <summary>
|
||||
/// HTTP_GENERAL_PROTOCOL_ERROR (0x101):
|
||||
/// H3_GENERAL_PROTOCOL_ERROR (0x101):
|
||||
/// Peer violated protocol requirements in a way which doesn’t match a more specific error code,
|
||||
/// or endpoint declines to use the more specific error code.
|
||||
/// </summary>
|
||||
ProtocolError = 0x101,
|
||||
/// <summary>
|
||||
/// HTTP_INTERNAL_ERROR (0x102):
|
||||
/// H3_INTERNAL_ERROR (0x102):
|
||||
/// An internal error has occurred in the HTTP stack.
|
||||
/// </summary>
|
||||
InternalError = 0x102,
|
||||
/// <summary>
|
||||
/// HTTP_STREAM_CREATION_ERROR (0x103):
|
||||
/// H3_STREAM_CREATION_ERROR (0x103):
|
||||
/// The endpoint detected that its peer created a stream that it will not accept.
|
||||
/// </summary>
|
||||
StreamCreationError = 0x103,
|
||||
/// <summary>
|
||||
/// HTTP_CLOSED_CRITICAL_STREAM (0x104):
|
||||
/// H3_CLOSED_CRITICAL_STREAM (0x104):
|
||||
/// A stream required by the connection was closed or reset.
|
||||
/// </summary>
|
||||
ClosedCriticalStream = 0x104,
|
||||
/// <summary>
|
||||
/// HTTP_UNEXPECTED_FRAME (0x105):
|
||||
/// H3_FRAME_UNEXPECTED (0x105):
|
||||
/// A frame was received which was not permitted in the current state.
|
||||
/// </summary>
|
||||
UnexpectedFrame = 0x105,
|
||||
/// <summary>
|
||||
/// HTTP_FRAME_ERROR (0x106):
|
||||
/// H3_FRAME_ERROR (0x106):
|
||||
/// A frame that fails to satisfy layout requirements or with an invalid size was received.
|
||||
/// </summary>
|
||||
FrameError = 0x106,
|
||||
/// <summary>
|
||||
/// HTTP_EXCESSIVE_LOAD (0x107):
|
||||
/// H3_EXCESSIVE_LOAD (0x107):
|
||||
/// The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load.
|
||||
/// </summary>
|
||||
ExcessiveLoad = 0x107,
|
||||
/// <summary>
|
||||
/// HTTP_WRONG_STREAM (0x108):
|
||||
/// A frame was received on a stream where it is not permitted.
|
||||
/// </summary>
|
||||
WrongStream = 0x108,
|
||||
/// <summary>
|
||||
/// HTTP_ID_ERROR (0x109):
|
||||
/// H3_ID_ERROR (0x109):
|
||||
/// A Stream ID, Push ID, or Placeholder ID was used incorrectly, such as exceeding a limit, reducing a limit, or being reused.
|
||||
/// </summary>
|
||||
IdError = 0x109,
|
||||
IdError = 0x108,
|
||||
/// <summary>
|
||||
/// HTTP_SETTINGS_ERROR (0x10A):
|
||||
/// H3_SETTINGS_ERROR (0x10A):
|
||||
/// An endpoint detected an error in the payload of a SETTINGS frame: a duplicate setting was detected,
|
||||
/// a client-only setting was sent by a server, or a server-only setting by a client.
|
||||
/// </summary>
|
||||
SettingsError = 0x10a,
|
||||
SettingsError = 0x109,
|
||||
/// <summary>
|
||||
/// HTTP_MISSING_SETTINGS (0x10B):
|
||||
/// H3_MISSING_SETTINGS (0x10B):
|
||||
/// No SETTINGS frame was received at the beginning of the control stream.
|
||||
/// </summary>
|
||||
MissingSettings = 0x10b,
|
||||
MissingSettings = 0x10a,
|
||||
/// <summary>
|
||||
/// HTTP_REQUEST_REJECTED (0x10C):
|
||||
/// H3_REQUEST_REJECTED (0x10C):
|
||||
/// A server rejected a request without performing any application processing.
|
||||
/// </summary>
|
||||
RequestRejected = 0x10c,
|
||||
RequestRejected = 0x10b,
|
||||
/// <summary>
|
||||
/// HTTP_REQUEST_CANCELLED (0x10D):
|
||||
/// H3_REQUEST_CANCELLED (0x10D):
|
||||
/// The request or its response (including pushed response) is cancelled.
|
||||
/// </summary>
|
||||
RequestCancelled = 0x10d,
|
||||
RequestCancelled = 0x10c,
|
||||
/// <summary>
|
||||
/// HTTP_REQUEST_INCOMPLETE (0x10E):
|
||||
/// H3_REQUEST_INCOMPLETE (0x10E):
|
||||
/// The client’s stream terminated without containing a fully-formed request.
|
||||
/// </summary>
|
||||
RequestIncomplete = 0x10e,
|
||||
RequestIncomplete = 0x10d,
|
||||
/// <summary>
|
||||
/// HTTP_EARLY_RESPONSE (0x10F):
|
||||
/// The remainder of the client’s request is not needed to produce a response. For use in STOP_SENDING only.
|
||||
/// </summary>
|
||||
EarlyResponse = 0x10f,
|
||||
/// <summary>
|
||||
/// HTTP_CONNECT_ERROR (0x110):
|
||||
/// H3_CONNECT_ERROR (0x110):
|
||||
/// The connection established in response to a CONNECT request was reset or abnormally closed.
|
||||
/// </summary>
|
||||
ConnectError = 0x110,
|
||||
ConnectError = 0x10f,
|
||||
/// <summary>
|
||||
/// HTTP_VERSION_FALLBACK (0x111):
|
||||
/// H3_VERSION_FALLBACK (0x111):
|
||||
/// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1.
|
||||
/// </summary>
|
||||
VersionFallback = 0x111,
|
||||
VersionFallback = 0x110,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal static partial class Http3Frame
|
||||
{
|
||||
public const int MaximumEncodedFrameEnvelopeLength = 1 + VariableLengthIntegerHelper.MaximumEncodedLength; // Frame type + payload length.
|
||||
|
||||
/// <summary>
|
||||
/// Reads two variable-length integers.
|
||||
/// </summary>
|
||||
public static bool TryReadIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead)
|
||||
{
|
||||
if (VariableLengthIntegerHelper.TryRead(buffer, out a, out int aLength))
|
||||
{
|
||||
buffer = buffer.Slice(aLength);
|
||||
if (VariableLengthIntegerHelper.TryRead(buffer, out b, out int bLength))
|
||||
{
|
||||
bytesRead = aLength + bLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
b = 0;
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | Type (i) ...
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | Length (i) ...
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | Frame Payload (*) ...
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
public static bool TryWriteFrameEnvelope(Http3FrameType frameType, long payloadLength, Span<byte> buffer, out int bytesWritten)
|
||||
{
|
||||
Debug.Assert(VariableLengthIntegerHelper.GetByteCount((long)frameType) == 1, $"{nameof(TryWriteFrameEnvelope)} assumes {nameof(frameType)} will fit within a single byte varint.");
|
||||
|
||||
if (buffer.Length != 0)
|
||||
{
|
||||
buffer[0] = (byte)frameType;
|
||||
buffer = buffer.Slice(1);
|
||||
|
||||
if (VariableLengthIntegerHelper.TryWrite(buffer, payloadLength, out int payloadLengthEncodedLength))
|
||||
{
|
||||
bytesWritten = payloadLengthEncodedLength + 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal enum Http3FrameType : long
|
||||
{
|
||||
Data = 0x0,
|
||||
Headers = 0x1,
|
||||
CancelPush = 0x3,
|
||||
Settings = 0x4,
|
||||
PushPromise = 0x5,
|
||||
GoAway = 0x7,
|
||||
MaxPushId = 0xD,
|
||||
DuplicatePush = 0xE
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16.
|
||||
/// A variable-length integer can use 1, 2, 4, or 8 bytes.
|
||||
/// </summary>
|
||||
internal static class VariableLengthIntegerHelper
|
||||
{
|
||||
public const int MaximumEncodedLength = 8;
|
||||
|
||||
// The high 4 bits indicate the length of the integer.
|
||||
// 00 = length 1
|
||||
// 01 = length 2
|
||||
// 10 = length 4
|
||||
// 11 = length 8
|
||||
private const byte LengthMask = 0xC0;
|
||||
private const byte InitialOneByteLengthMask = 0x00;
|
||||
private const byte InitialTwoByteLengthMask = 0x40;
|
||||
private const byte InitialFourByteLengthMask = 0x80;
|
||||
private const byte InitialEightByteLengthMask = 0xC0;
|
||||
|
||||
// Bits to subtract to remove the length.
|
||||
private const uint TwoByteLengthMask = 0x4000;
|
||||
private const uint FourByteLengthMask = 0x80000000;
|
||||
private const ulong EightByteLengthMask = 0xC000000000000000;
|
||||
|
||||
public const uint OneByteLimit = (1U << 6) - 1;
|
||||
private const uint TwoByteLimit = (1U << 16) - 1;
|
||||
private const uint FourByteLimit = (1U << 30) - 1;
|
||||
private const long EightByteLimit = (1L << 62) - 1;
|
||||
|
||||
public static bool TryRead(ReadOnlySpan<byte> buffer, out long value, out int bytesRead)
|
||||
{
|
||||
if (buffer.Length != 0)
|
||||
{
|
||||
byte firstByte = buffer[0];
|
||||
|
||||
switch (firstByte & LengthMask)
|
||||
{
|
||||
case InitialOneByteLengthMask:
|
||||
value = firstByte;
|
||||
bytesRead = 1;
|
||||
return true;
|
||||
case InitialTwoByteLengthMask:
|
||||
if (BinaryPrimitives.TryReadUInt16BigEndian(buffer, out ushort serializedShort))
|
||||
{
|
||||
value = serializedShort - TwoByteLengthMask;
|
||||
bytesRead = 2;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case InitialFourByteLengthMask:
|
||||
if (BinaryPrimitives.TryReadUInt32BigEndian(buffer, out uint serializedInt))
|
||||
{
|
||||
value = serializedInt - FourByteLengthMask;
|
||||
bytesRead = 4;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default: // InitialEightByteLengthMask
|
||||
Debug.Assert((firstByte & LengthMask) == InitialEightByteLengthMask);
|
||||
if (BinaryPrimitives.TryReadUInt64BigEndian(buffer, out ulong serializedLong))
|
||||
{
|
||||
value = (long)(serializedLong - EightByteLengthMask);
|
||||
Debug.Assert(value >= 0 && value <= EightByteLimit, "Serialized values are within [0, 2^62).");
|
||||
|
||||
bytesRead = 8;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryRead(ref SequenceReader<byte> reader, out long value)
|
||||
{
|
||||
// Hot path: we probably have the entire integer in one unbroken span.
|
||||
if (TryRead(reader.UnreadSpan, out value, out int bytesRead))
|
||||
{
|
||||
reader.Advance(bytesRead);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cold path: copy to a temporary buffer before calling span-based read.
|
||||
return TryReadSlow(ref reader, out value);
|
||||
|
||||
static bool TryReadSlow(ref SequenceReader<byte> reader, out long value)
|
||||
{
|
||||
ReadOnlySpan<byte> span = reader.CurrentSpan;
|
||||
|
||||
if (reader.TryPeek(out byte firstByte))
|
||||
{
|
||||
int length =
|
||||
(firstByte & LengthMask) switch
|
||||
{
|
||||
InitialOneByteLengthMask => 1,
|
||||
InitialTwoByteLengthMask => 2,
|
||||
InitialFourByteLengthMask => 4,
|
||||
_ => 8 // LengthEightByte
|
||||
};
|
||||
|
||||
Span<byte> temp = (stackalloc byte[8])[..length];
|
||||
if (reader.TryCopyTo(temp))
|
||||
{
|
||||
bool result = TryRead(temp, out value, out int bytesRead);
|
||||
Debug.Assert(result == true);
|
||||
Debug.Assert(bytesRead == length);
|
||||
|
||||
reader.Advance(bytesRead);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static long GetInteger(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
var reader = new SequenceReader<byte>(buffer);
|
||||
if (TryRead(ref reader, out long value))
|
||||
{
|
||||
consumed = examined = buffer.GetPosition(reader.Consumed);
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
consumed = default;
|
||||
examined = buffer.End;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryWrite(Span<byte> buffer, long longToEncode, out int bytesWritten)
|
||||
{
|
||||
Debug.Assert(longToEncode >= 0);
|
||||
Debug.Assert(longToEncode <= EightByteLimit);
|
||||
|
||||
if (longToEncode < OneByteLimit)
|
||||
{
|
||||
if (buffer.Length != 0)
|
||||
{
|
||||
buffer[0] = (byte)longToEncode;
|
||||
bytesWritten = 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (longToEncode < TwoByteLimit)
|
||||
{
|
||||
if (BinaryPrimitives.TryWriteUInt16BigEndian(buffer, (ushort)((uint)longToEncode | TwoByteLengthMask)))
|
||||
{
|
||||
bytesWritten = 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (longToEncode < FourByteLimit)
|
||||
{
|
||||
if (BinaryPrimitives.TryWriteUInt32BigEndian(buffer, (uint)longToEncode | FourByteLengthMask))
|
||||
{
|
||||
bytesWritten = 4;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else // EightByteLimit
|
||||
{
|
||||
if (BinaryPrimitives.TryWriteUInt64BigEndian(buffer, (ulong)longToEncode | EightByteLengthMask))
|
||||
{
|
||||
bytesWritten = 8;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int WriteInteger(Span<byte> buffer, long longToEncode)
|
||||
{
|
||||
bool res = TryWrite(buffer, longToEncode, out int bytesWritten);
|
||||
Debug.Assert(res == true);
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
public static int GetByteCount(long value)
|
||||
{
|
||||
Debug.Assert(value >= 0);
|
||||
Debug.Assert(value <= EightByteLimit);
|
||||
|
||||
return
|
||||
value < OneByteLimit ? 1 :
|
||||
value < TwoByteLimit ? 2 :
|
||||
value < FourByteLimit ? 4 :
|
||||
8; // EightByteLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal enum Http3SettingType : long
|
||||
{
|
||||
/// <summary>
|
||||
/// SETTINGS_QPACK_MAX_TABLE_CAPACITY
|
||||
/// The maximum dynamic table size. The default is 0.
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-5
|
||||
/// </summary>
|
||||
QPackMaxTableCapacity = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// SETTINGS_MAX_HEADER_LIST_SIZE
|
||||
/// The maximum size of headers. The default is unlimited.
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-7.2.4.1
|
||||
/// </summary>
|
||||
MaxHeaderListSize = 0x6,
|
||||
|
||||
/// <summary>
|
||||
/// SETTINGS_QPACK_BLOCKED_STREAMS
|
||||
/// The maximum number of request streams that can be blocked waiting for QPack instructions. The default is 0.
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-5
|
||||
/// </summary>
|
||||
QPackBlockedStreams = 0x7
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Unidirectional stream types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bidirectional streams are always a request stream.
|
||||
/// </remarks>
|
||||
internal enum Http3StreamType : long
|
||||
{
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-6.2.1
|
||||
/// </summary>
|
||||
Control = 0x00,
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-6.2.2
|
||||
/// </summary>
|
||||
Push = 0x01,
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.2
|
||||
/// </summary>
|
||||
QPackEncoder = 0x02,
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.2
|
||||
/// </summary>
|
||||
QPackDecoder = 0x03
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
// 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.
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
internal class StaticTable
|
||||
// TODO: make class static.
|
||||
internal class H3StaticTable
|
||||
{
|
||||
private static readonly StaticTable _instance = new StaticTable();
|
||||
|
||||
private readonly Dictionary<int, int> _statusIndex = new Dictionary<int, int>
|
||||
{
|
||||
[103] = 24,
|
||||
|
|
@ -31,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
|
||||
private readonly Dictionary<HttpMethod, int> _methodIndex = new Dictionary<HttpMethod, int>
|
||||
{
|
||||
// TODO connect is intenral to system.net.http
|
||||
// TODO connect is internal to system.net.http
|
||||
[HttpMethod.Delete] = 16,
|
||||
[HttpMethod.Get] = 17,
|
||||
[HttpMethod.Head] = 18,
|
||||
|
|
@ -40,16 +39,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
[HttpMethod.Put] = 21,
|
||||
};
|
||||
|
||||
private StaticTable()
|
||||
private H3StaticTable()
|
||||
{
|
||||
}
|
||||
|
||||
public static StaticTable Instance => _instance;
|
||||
public static H3StaticTable Instance { get; } = new H3StaticTable();
|
||||
|
||||
public int Count => _staticTable.Length;
|
||||
|
||||
public HeaderField this[int index] => _staticTable[index];
|
||||
|
||||
// TODO: just use Dictionary directly to avoid interface dispatch.
|
||||
public IReadOnlyDictionary<int, int> StatusIndex => _statusIndex;
|
||||
public IReadOnlyDictionary<HttpMethod, int> MethodIndex => _methodIndex;
|
||||
|
||||
|
|
@ -158,5 +158,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
|
||||
private static HeaderField CreateHeaderField(string name, string value)
|
||||
=> new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value));
|
||||
|
||||
public const int Authority = 0;
|
||||
public const int PathSlash = 1;
|
||||
public const int Age0 = 2;
|
||||
public const int ContentDisposition = 3;
|
||||
public const int ContentLength0 = 4;
|
||||
public const int Cookie = 5;
|
||||
public const int Date = 6;
|
||||
public const int ETag = 7;
|
||||
public const int IfModifiedSince = 8;
|
||||
public const int IfNoneMatch = 9;
|
||||
public const int LastModified = 10;
|
||||
public const int Link = 11;
|
||||
public const int Location = 12;
|
||||
public const int Referer = 13;
|
||||
public const int SetCookie = 14;
|
||||
public const int MethodConnect = 15;
|
||||
public const int MethodDelete = 16;
|
||||
public const int MethodGet = 17;
|
||||
public const int MethodHead = 18;
|
||||
public const int MethodOptions = 19;
|
||||
public const int MethodPost = 20;
|
||||
public const int MethodPut = 21;
|
||||
public const int SchemeHttps = 23;
|
||||
public const int Status103 = 24;
|
||||
public const int Status200 = 25;
|
||||
public const int Status304 = 26;
|
||||
public const int Status404 = 27;
|
||||
public const int Status503 = 28;
|
||||
public const int AcceptAny = 29;
|
||||
public const int AcceptEncodingGzipDeflateBr = 31;
|
||||
public const int AcceptRangesBytes = 32;
|
||||
public const int AccessControlAllowHeadersCacheControl = 33;
|
||||
public const int AccessControlAllowOriginAny = 35;
|
||||
public const int CacheControlMaxAge0 = 36;
|
||||
public const int ContentEncodingBr = 42;
|
||||
public const int ContentTypeApplicationDnsMessage = 44;
|
||||
public const int RangeBytes0ToAll = 55;
|
||||
public const int StrictTransportSecurityMaxAge31536000 = 56;
|
||||
public const int VaryAcceptEncoding = 59;
|
||||
public const int XContentTypeOptionsNoSniff = 61;
|
||||
public const int Status100 = 63;
|
||||
public const int Status204 = 64;
|
||||
public const int Status206 = 65;
|
||||
public const int Status302 = 66;
|
||||
public const int Status400 = 67;
|
||||
public const int Status403 = 68;
|
||||
public const int Status421 = 69;
|
||||
public const int Status425 = 70;
|
||||
public const int Status500 = 71;
|
||||
public const int AcceptLanguage = 72;
|
||||
public const int AccessControlAllowCredentials = 73;
|
||||
public const int AccessControlAllowMethodsGet = 76;
|
||||
public const int AccessControlExposeHeadersContentLength = 79;
|
||||
public const int AltSvcClear = 83;
|
||||
public const int Authorization = 84;
|
||||
public const int ContentSecurityPolicyAllNone = 85;
|
||||
public const int IfRange = 89;
|
||||
public const int Origin = 90;
|
||||
public const int Server = 92;
|
||||
public const int UpgradeInsecureRequests1 = 94;
|
||||
public const int UserAgent = 95;
|
||||
public const int XFrameOptionsDeny = 97;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
internal readonly struct HeaderField
|
||||
{
|
||||
public HeaderField(byte[] name, byte[] value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public byte[] Name { get; }
|
||||
|
||||
public byte[] Value { get; }
|
||||
|
||||
public int Length => Name.Length + Value.Length;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
// 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.
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Numerics;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
internal class QPackDecoder
|
||||
internal class QPackDecoder : IDisposable
|
||||
{
|
||||
private enum State
|
||||
{
|
||||
Ready,
|
||||
RequiredInsertCount,
|
||||
RequiredInsertCountDone,
|
||||
RequiredInsertCountContinue,
|
||||
Base,
|
||||
BaseContinue,
|
||||
CompressedHeaders,
|
||||
HeaderFieldIndex,
|
||||
HeaderNameIndex,
|
||||
|
|
@ -48,8 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
//+---+---+---+---+---+---+---+---+
|
||||
//| 1 | S | Index(6+) |
|
||||
//+---+---+-----------------------+
|
||||
private const byte IndexedHeaderFieldMask = 0x80;
|
||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
||||
private const byte IndexedHeaderStaticMask = 0x40;
|
||||
private const byte IndexedHeaderStaticRepresentation = 0x40;
|
||||
private const byte IndexedHeaderFieldPrefixMask = 0x3F;
|
||||
|
|
@ -60,7 +60,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
//| 0 | 0 | 0 | 1 | Index(4+) |
|
||||
//+---+---+---+---+---------------+
|
||||
private const byte PostBaseIndexMask = 0xF0;
|
||||
private const byte PostBaseIndexRepresentation = 0x10;
|
||||
private const int PostBaseIndexPrefix = 4;
|
||||
|
||||
//0 1 2 3 4 5 6 7
|
||||
|
|
@ -71,9 +70,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
//+---+---------------------------+
|
||||
//| Value String(Length bytes) |
|
||||
//+-------------------------------+
|
||||
private const byte LiteralHeaderFieldMask = 0xC0;
|
||||
private const byte LiteralHeaderFieldRepresentation = 0x40;
|
||||
private const byte LiteralHeaderFieldNMask = 0x20;
|
||||
private const byte LiteralHeaderFieldStaticMask = 0x10;
|
||||
private const byte LiteralHeaderFieldPrefixMask = 0x0F;
|
||||
private const int LiteralHeaderFieldPrefix = 4;
|
||||
|
|
@ -86,9 +82,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
//+---+---------------------------+
|
||||
//| Value String(Length bytes) |
|
||||
//+-------------------------------+
|
||||
private const byte LiteralHeaderFieldPostBaseMask = 0xF0;
|
||||
private const byte LiteralHeaderFieldPostBaseRepresentation = 0x00;
|
||||
private const byte LiteralHeaderFieldPostBaseNMask = 0x08;
|
||||
private const byte LiteralHeaderFieldPostBasePrefixMask = 0x07;
|
||||
private const int LiteralHeaderFieldPostBasePrefix = 3;
|
||||
|
||||
|
|
@ -102,9 +95,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
//+---+---------------------------+
|
||||
//| Value String(Length bytes) |
|
||||
//+-------------------------------+
|
||||
private const byte LiteralHeaderFieldWithoutNameReferenceMask = 0xE0;
|
||||
private const byte LiteralHeaderFieldWithoutNameReferenceRepresentation = 0x20;
|
||||
private const byte LiteralHeaderFieldWithoutNameReferenceNMask = 0x10;
|
||||
private const byte LiteralHeaderFieldWithoutNameReferenceHuffmanMask = 0x08;
|
||||
private const byte LiteralHeaderFieldWithoutNameReferencePrefixMask = 0x07;
|
||||
private const int LiteralHeaderFieldWithoutNameReferencePrefix = 3;
|
||||
|
|
@ -112,24 +102,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
private const int StringLengthPrefix = 7;
|
||||
private const byte HuffmanMask = 0x80;
|
||||
|
||||
private State _state = State.Ready;
|
||||
// TODO break out dynamic table entirely.
|
||||
private long _maxDynamicTableSize;
|
||||
private DynamicTable _dynamicTable;
|
||||
private const int DefaultStringBufferSize = 64;
|
||||
|
||||
private readonly int _maxHeadersLength;
|
||||
private State _state = State.RequiredInsertCount;
|
||||
|
||||
// TODO idk what these are for.
|
||||
private byte[] _stringOctets;
|
||||
private byte[] _headerNameOctets;
|
||||
private byte[] _headerValueOctets;
|
||||
private int _requiredInsertCount;
|
||||
//private int _insertCount;
|
||||
private int _base;
|
||||
|
||||
// s is used for whatever s is in each field. This has multiple definition
|
||||
private bool _s;
|
||||
private bool _n;
|
||||
private bool _huffman;
|
||||
private bool _index;
|
||||
private int? _index;
|
||||
|
||||
private byte[] _headerName;
|
||||
private int _headerNameLength;
|
||||
|
|
@ -138,37 +122,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
private int _stringIndex;
|
||||
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
|
||||
|
||||
// Decoders are on the http3stream now, each time we see a header block
|
||||
public QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize)
|
||||
: this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { }
|
||||
private static ArrayPool<byte> Pool => ArrayPool<byte>.Shared;
|
||||
|
||||
|
||||
// For testing.
|
||||
internal QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable)
|
||||
private static void ReturnAndGetNewPooledArray(ref byte[] buffer, int newSize)
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
_dynamicTable = dynamicTable;
|
||||
byte[] old = buffer;
|
||||
buffer = null;
|
||||
|
||||
_stringOctets = new byte[maxRequestHeaderFieldSize];
|
||||
_headerNameOctets = new byte[maxRequestHeaderFieldSize];
|
||||
_headerValueOctets = new byte[maxRequestHeaderFieldSize];
|
||||
Pool.Return(old, clearArray: true);
|
||||
buffer = Pool.Rent(newSize);
|
||||
}
|
||||
|
||||
public QPackDecoder(int maxHeadersLength)
|
||||
{
|
||||
_maxHeadersLength = maxHeadersLength;
|
||||
|
||||
// TODO: make allocation lazy? with static entries it's possible no buffers will be needed.
|
||||
_stringOctets = Pool.Rent(DefaultStringBufferSize);
|
||||
_headerNameOctets = Pool.Rent(DefaultStringBufferSize);
|
||||
_headerValueOctets = Pool.Rent(DefaultStringBufferSize);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_stringOctets != null)
|
||||
{
|
||||
Pool.Return(_stringOctets, true);
|
||||
_stringOctets = null;
|
||||
}
|
||||
|
||||
if (_headerNameOctets != null)
|
||||
{
|
||||
Pool.Return(_headerNameOctets, true);
|
||||
_headerNameOctets = null;
|
||||
}
|
||||
|
||||
if (_headerValueOctets != null)
|
||||
{
|
||||
Pool.Return(_headerValueOctets, true);
|
||||
_headerValueOctets = null;
|
||||
}
|
||||
}
|
||||
|
||||
// sequence will probably be a header block instead.
|
||||
public void Decode(in ReadOnlySequence<byte> headerBlock, IHttpHeadersHandler handler)
|
||||
{
|
||||
// TODO I need to get the RequiredInsertCount and DeltaBase
|
||||
// These are always present in the header block
|
||||
// TODO need to figure out if I have read an entire header block.
|
||||
|
||||
// (I think this can be done based on length outside of this)
|
||||
foreach (var segment in headerBlock)
|
||||
foreach (ReadOnlyMemory<byte> segment in headerBlock)
|
||||
{
|
||||
var span = segment.Span;
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
OnByte(span[i], handler);
|
||||
}
|
||||
Decode(segment.Span, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public void Decode(ReadOnlySpan<byte> headerBlock, IHttpHeadersHandler handler)
|
||||
{
|
||||
foreach (byte b in headerBlock)
|
||||
{
|
||||
OnByte(b, handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,120 +186,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
int prefixInt;
|
||||
switch (_state)
|
||||
{
|
||||
case State.Ready:
|
||||
case State.RequiredInsertCount:
|
||||
if (_integerDecoder.BeginTryDecode(b, RequiredInsertCountPrefix, out intResult))
|
||||
{
|
||||
OnRequiredInsertCount(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.RequiredInsertCount;
|
||||
_state = State.RequiredInsertCountContinue;
|
||||
}
|
||||
break;
|
||||
case State.RequiredInsertCount:
|
||||
case State.RequiredInsertCountContinue:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnRequiredInsertCount(intResult);
|
||||
}
|
||||
break;
|
||||
case State.RequiredInsertCountDone:
|
||||
case State.Base:
|
||||
prefixInt = ~BaseMask & b;
|
||||
|
||||
_s = (b & BaseMask) == BaseMask;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode(b, BasePrefix, out intResult))
|
||||
{
|
||||
OnBase(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.Base;
|
||||
_state = State.BaseContinue;
|
||||
}
|
||||
break;
|
||||
case State.Base:
|
||||
case State.BaseContinue:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnBase(intResult);
|
||||
}
|
||||
break;
|
||||
case State.CompressedHeaders:
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
||||
switch (BitOperations.LeadingZeroCount(b))
|
||||
{
|
||||
prefixInt = IndexedHeaderFieldPrefixMask & b;
|
||||
_s = (b & IndexedHeaderStaticMask) == IndexedHeaderStaticRepresentation;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, IndexedHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & PostBaseIndexMask) == PostBaseIndexRepresentation)
|
||||
{
|
||||
prefixInt = ~PostBaseIndexMask & b;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, PostBaseIndexPrefix, out intResult))
|
||||
{
|
||||
OnPostBaseIndex(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.PostBaseIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldMask) == LiteralHeaderFieldRepresentation)
|
||||
{
|
||||
_index = true;
|
||||
// Represents whether an intermediary is permitted to add this header to the dynamic header table on
|
||||
// subsequent hops.
|
||||
// if n is set, the encoded header must always be encoded with a literal representation
|
||||
case 24: // Indexed Header Field
|
||||
prefixInt = IndexedHeaderFieldPrefixMask & b;
|
||||
|
||||
_n = (LiteralHeaderFieldNMask & b) == LiteralHeaderFieldNMask;
|
||||
_s = (LiteralHeaderFieldStaticMask & b) == LiteralHeaderFieldStaticMask;
|
||||
prefixInt = b & LiteralHeaderFieldPrefixMask;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldPostBaseMask) == LiteralHeaderFieldPostBaseRepresentation)
|
||||
{
|
||||
_index = true;
|
||||
_n = (LiteralHeaderFieldPostBaseNMask & b) == LiteralHeaderFieldPostBaseNMask;
|
||||
prefixInt = b & LiteralHeaderFieldPostBasePrefixMask;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPostBasePrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderNamePostBase(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndexPostBase;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithoutNameReferenceMask) == LiteralHeaderFieldWithoutNameReferenceRepresentation)
|
||||
{
|
||||
_index = false;
|
||||
_n = (LiteralHeaderFieldWithoutNameReferenceNMask & b) == LiteralHeaderFieldWithoutNameReferenceNMask;
|
||||
_huffman = (b & LiteralHeaderFieldWithoutNameReferenceHuffmanMask) != 0;
|
||||
prefixInt = b & LiteralHeaderFieldWithoutNameReferencePrefixMask;
|
||||
bool useStaticTable = (b & IndexedHeaderStaticMask) == IndexedHeaderStaticRepresentation;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult))
|
||||
{
|
||||
OnStringLength(intResult, State.HeaderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
if (!useStaticTable)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
}
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, IndexedHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
break;
|
||||
case 25: // Literal Header Field With Name Reference
|
||||
useStaticTable = (LiteralHeaderFieldStaticMask & b) == LiteralHeaderFieldStaticMask;
|
||||
|
||||
if (!useStaticTable)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
}
|
||||
|
||||
prefixInt = b & LiteralHeaderFieldPrefixMask;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
break;
|
||||
case 26: // Literal Header Field Without Name Reference
|
||||
_huffman = (b & LiteralHeaderFieldWithoutNameReferenceHuffmanMask) != 0;
|
||||
prefixInt = b & LiteralHeaderFieldWithoutNameReferencePrefixMask;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult))
|
||||
{
|
||||
OnStringLength(intResult, State.HeaderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
break;
|
||||
case 27: // Indexed Header Field With Post-Base Index
|
||||
prefixInt = ~PostBaseIndexMask & b;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, PostBaseIndexPrefix, out intResult))
|
||||
{
|
||||
OnPostBaseIndex(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.PostBaseIndex;
|
||||
}
|
||||
break;
|
||||
default: // Literal Header Field With Post-Base Name Reference (at least 4 zeroes, maybe more)
|
||||
prefixInt = b & LiteralHeaderFieldPostBasePrefixMask;
|
||||
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPostBasePrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderNamePostBase(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndexPostBase;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case State.HeaderNameLength:
|
||||
// huffman has already been processed.
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnStringLength(intResult, nextState: State.HeaderName);
|
||||
|
|
@ -373,7 +379,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
{
|
||||
if (length > _stringOctets.Length)
|
||||
{
|
||||
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/);
|
||||
if (length > _maxHeadersLength)
|
||||
{
|
||||
throw new QPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength));
|
||||
}
|
||||
|
||||
ReturnAndGetNewPooledArray(ref _stringOctets, length);
|
||||
}
|
||||
|
||||
_stringLength = length;
|
||||
|
|
@ -385,20 +396,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
{
|
||||
OnString(nextState: State.CompressedHeaders);
|
||||
|
||||
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
|
||||
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
||||
Span<byte> headerNameSpan;
|
||||
Span<byte> headerValueSpan = _headerValueOctets.AsSpan(0, _headerValueLength);
|
||||
|
||||
if (_index is int index)
|
||||
{
|
||||
Debug.Assert(index >= 0 && index <= H3StaticTable.Instance.Count, $"The index should be a valid static index here. {nameof(QPackDecoder)} should have previously thrown if it read a dynamic index.");
|
||||
handler.OnStaticIndexedHeader(index, headerValueSpan);
|
||||
_index = null;
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
headerNameSpan = _headerNameOctets.AsSpan(0, _headerNameLength);
|
||||
}
|
||||
|
||||
handler.OnHeader(headerNameSpan, headerValueSpan);
|
||||
|
||||
if (_index)
|
||||
{
|
||||
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnString(State nextState)
|
||||
{
|
||||
int Decode(byte[] dst)
|
||||
int Decode(ref byte[] dst)
|
||||
{
|
||||
if (_huffman)
|
||||
{
|
||||
|
|
@ -406,6 +425,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
}
|
||||
else
|
||||
{
|
||||
if (dst.Length < _stringLength)
|
||||
{
|
||||
ReturnAndGetNewPooledArray(ref dst, _stringLength);
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
||||
return _stringLength;
|
||||
}
|
||||
|
|
@ -415,17 +439,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
{
|
||||
if (_state == State.HeaderName)
|
||||
{
|
||||
_headerNameLength = Decode(ref _headerNameOctets);
|
||||
_headerName = _headerNameOctets;
|
||||
_headerNameLength = Decode(_headerNameOctets);
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerValueLength = Decode(_headerValueOctets);
|
||||
_headerValueLength = Decode(ref _headerValueOctets);
|
||||
}
|
||||
}
|
||||
catch (HuffmanDecodingException ex)
|
||||
{
|
||||
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex);
|
||||
throw new QPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex);
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
|
|
@ -434,80 +458,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
|
|||
|
||||
private void OnIndexedHeaderName(int index)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_index = index;
|
||||
_state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderNamePostBase(int index)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
// TODO update with postbase index
|
||||
var header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_state = State.HeaderValueLength;
|
||||
// _index = index;
|
||||
// _state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnPostBaseIndex(int intResult, IHttpHeadersHandler handler)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
// TODO
|
||||
_state = State.CompressedHeaders;
|
||||
// _state = State.CompressedHeaders;
|
||||
}
|
||||
|
||||
private void OnBase(int deltaBase)
|
||||
{
|
||||
if (deltaBase != 0)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
}
|
||||
_state = State.CompressedHeaders;
|
||||
if (_s)
|
||||
{
|
||||
_base = _requiredInsertCount - deltaBase - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_base = _requiredInsertCount + deltaBase;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
private void OnRequiredInsertCount(int requiredInsertCount)
|
||||
{
|
||||
_requiredInsertCount = requiredInsertCount;
|
||||
_state = State.RequiredInsertCountDone;
|
||||
// This is just going to noop for now. I don't get this algorithm at all.
|
||||
// var encoderInsertCount = 0;
|
||||
// var maxEntries = _maxDynamicTableSize / HeaderField.RfcOverhead;
|
||||
|
||||
// if (requiredInsertCount != 0)
|
||||
// {
|
||||
// encoderInsertCount = (requiredInsertCount % ( 2 * maxEntries)) + 1;
|
||||
// }
|
||||
|
||||
// // Dude I don't get this algorithm...
|
||||
// var fullRange = 2 * maxEntries;
|
||||
// if (encoderInsertCount == 0)
|
||||
// {
|
||||
|
||||
// }
|
||||
if (requiredInsertCount != 0)
|
||||
{
|
||||
ThrowDynamicTableNotSupported();
|
||||
}
|
||||
_state = State.Base;
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||
{
|
||||
// Indexes start at 0 in QPack
|
||||
var header = GetHeader(index);
|
||||
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
|
||||
handler.OnStaticIndexedHeader(index);
|
||||
_state = State.CompressedHeaders;
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index)
|
||||
private static void ThrowDynamicTableNotSupported()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _s ? StaticTable.Instance[index] : _dynamicTable[index];
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
{
|
||||
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index), */, ex);
|
||||
}
|
||||
throw new QPackDecodingException("No dynamic table support");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
[Serializable]
|
||||
internal sealed class QPackDecodingException : Exception
|
||||
{
|
||||
public QPackDecodingException()
|
||||
{
|
||||
}
|
||||
|
||||
public QPackDecodingException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public QPackDecodingException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
private QPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.HPack;
|
||||
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
internal class QPackEncoder
|
||||
{
|
||||
private IEnumerator<KeyValuePair<string, string>> _enumerator;
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | T | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
//
|
||||
// Note for this method's implementation of above:
|
||||
// - T is constant 1 here, indicating a static table reference.
|
||||
public static bool EncodeStaticIndexedHeaderField(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (!destination.IsEmpty)
|
||||
{
|
||||
destination[0] = 0b11000000;
|
||||
return IntegerEncoder.Encode(index, 6, destination, out bytesWritten);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] EncodeStaticIndexedHeaderFieldToArray(int index)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[IntegerEncoder.MaxInt32EncodedLength];
|
||||
|
||||
bool res = EncodeStaticIndexedHeaderField(index, buffer, out int bytesWritten);
|
||||
Debug.Assert(res == true);
|
||||
|
||||
return buffer.Slice(0, bytesWritten).ToArray();
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.4
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | N | T |Name Index (4+)|
|
||||
// +---+---+---+---+---------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length bytes) |
|
||||
// +-------------------------------+
|
||||
//
|
||||
// Note for this method's implementation of above:
|
||||
// - N is constant 0 here, indicating intermediates (proxies) can compress the header when fordwarding.
|
||||
// - T is constant 1 here, indicating a static table reference.
|
||||
// - H is constant 0 here, as we do not yet perform Huffman coding.
|
||||
public static bool EncodeLiteralHeaderFieldWithStaticNameReference(int index, string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// Requires at least two bytes (one for name reference header, one for value length)
|
||||
if (destination.Length >= 2)
|
||||
{
|
||||
destination[0] = 0b01010000;
|
||||
if (IntegerEncoder.Encode(index, 4, destination, out int headerBytesWritten))
|
||||
{
|
||||
destination = destination.Slice(headerBytesWritten);
|
||||
|
||||
if (EncodeValueString(value, destination, out int valueBytesWritten))
|
||||
{
|
||||
bytesWritten = headerBytesWritten + valueBytesWritten;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes just the name part of a Literal Header Field With Static Name Reference. Must call <see cref="EncodeValueString(string, Span{byte}, out int)"/> after to encode the header's value.
|
||||
/// </summary>
|
||||
public static byte[] EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(int index)
|
||||
{
|
||||
Span<byte> temp = stackalloc byte[IntegerEncoder.MaxInt32EncodedLength];
|
||||
|
||||
temp[0] = 0b01110000;
|
||||
bool res = IntegerEncoder.Encode(index, 4, temp, out int headerBytesWritten);
|
||||
Debug.Assert(res == true);
|
||||
|
||||
return temp.Slice(0, headerBytesWritten).ToArray();
|
||||
}
|
||||
|
||||
public static byte[] EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(int index, string value)
|
||||
{
|
||||
Span<byte> temp = value.Length < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength * 2] : new byte[value.Length + IntegerEncoder.MaxInt32EncodedLength * 2];
|
||||
bool res = EncodeLiteralHeaderFieldWithStaticNameReference(index, value, temp, out int bytesWritten);
|
||||
Debug.Assert(res == true);
|
||||
return temp.Slice(0, bytesWritten).ToArray();
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.6
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | N | H |NameLen(3+)|
|
||||
// +---+---+---+---+---+-----------+
|
||||
// | Name String (Length bytes) |
|
||||
// +---+---------------------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length bytes) |
|
||||
// +-------------------------------+
|
||||
//
|
||||
// Note for this method's implementation of above:
|
||||
// - N is constant 0 here, indicating intermediates (proxies) can compress the header when fordwarding.
|
||||
// - H is constant 0 here, as we do not yet perform Huffman coding.
|
||||
public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(value, destination.Slice(nameLength), out int valueLength))
|
||||
{
|
||||
bytesWritten = nameLength + valueLength;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a Literal Header Field Without Name Reference, building the value by concatenating a collection of strings with separators.
|
||||
/// </summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, ReadOnlySpan<string> values, string valueSeparator, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(values, valueSeparator, destination.Slice(nameLength), out int valueLength))
|
||||
{
|
||||
bytesWritten = nameLength + valueLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes just the value part of a Literawl Header Field Without Static Name Reference. Must call <see cref="EncodeValueString(string, Span{byte}, out int)"/> after to encode the header's value.
|
||||
/// </summary>
|
||||
public static byte[] EncodeLiteralHeaderFieldWithoutNameReferenceToArray(string name)
|
||||
{
|
||||
Span<byte> temp = name.Length < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength] : new byte[name.Length + IntegerEncoder.MaxInt32EncodedLength];
|
||||
|
||||
bool res = EncodeNameString(name, temp, out int nameLength);
|
||||
Debug.Assert(res == true);
|
||||
|
||||
return temp.Slice(0, nameLength).ToArray();
|
||||
}
|
||||
|
||||
public static byte[] EncodeLiteralHeaderFieldWithoutNameReferenceToArray(string name, string value)
|
||||
{
|
||||
Span<byte> temp = (name.Length + value.Length) < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength * 2] : new byte[name.Length + value.Length + IntegerEncoder.MaxInt32EncodedLength * 2];
|
||||
|
||||
bool res = EncodeLiteralHeaderFieldWithoutNameReference(name, value, temp, out int bytesWritten);
|
||||
Debug.Assert(res == true);
|
||||
|
||||
return temp.Slice(0, bytesWritten).ToArray();
|
||||
}
|
||||
|
||||
private static bool EncodeValueString(string s, Span<byte> buffer, out int length)
|
||||
{
|
||||
if (buffer.Length != 0)
|
||||
{
|
||||
buffer[0] = 0;
|
||||
if (IntegerEncoder.Encode(s.Length, 7, buffer, out int nameLength))
|
||||
{
|
||||
buffer = buffer.Slice(nameLength);
|
||||
if (buffer.Length >= s.Length)
|
||||
{
|
||||
EncodeValueStringPart(s, buffer);
|
||||
|
||||
length = nameLength + s.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a value by concatenating a collection of strings, separated by a separator string.
|
||||
/// </summary>
|
||||
public static bool EncodeValueString(ReadOnlySpan<string> values, string separator, Span<byte> buffer, out int length)
|
||||
{
|
||||
if (values.Length == 1)
|
||||
{
|
||||
return EncodeValueString(values[0], buffer, out length);
|
||||
}
|
||||
|
||||
if (values.Length == 0)
|
||||
{
|
||||
// TODO: this will be called with a string array from HttpHeaderCollection. Can we ever get a 0-length array from that? Assert if not.
|
||||
return EncodeValueString(string.Empty, buffer, out length);
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
int valueLength = separator.Length * (values.Length - 1);
|
||||
for (int i = 0; i < values.Length; ++i)
|
||||
{
|
||||
valueLength += values[i].Length;
|
||||
}
|
||||
|
||||
buffer[0] = 0;
|
||||
if (IntegerEncoder.Encode(valueLength, 7, buffer, out int nameLength))
|
||||
{
|
||||
buffer = buffer.Slice(nameLength);
|
||||
if (buffer.Length >= valueLength)
|
||||
{
|
||||
string value = values[0];
|
||||
EncodeValueStringPart(value, buffer);
|
||||
buffer = buffer.Slice(value.Length);
|
||||
|
||||
for (int i = 1; i < values.Length; ++i)
|
||||
{
|
||||
EncodeValueStringPart(separator, buffer);
|
||||
buffer = buffer.Slice(separator.Length);
|
||||
|
||||
value = values[i];
|
||||
EncodeValueStringPart(value, buffer);
|
||||
buffer = buffer.Slice(value.Length);
|
||||
}
|
||||
|
||||
length = nameLength + valueLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void EncodeValueStringPart(string s, Span<byte> buffer)
|
||||
{
|
||||
Debug.Assert(buffer.Length >= s.Length);
|
||||
|
||||
for (int i = 0; i < s.Length; ++i)
|
||||
{
|
||||
char ch = s[i];
|
||||
|
||||
if (ch > 127)
|
||||
{
|
||||
throw new QPackEncodingException("ASCII header value.");
|
||||
}
|
||||
|
||||
buffer[i] = (byte)ch;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EncodeNameString(string s, Span<byte> buffer, out int length)
|
||||
{
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
if (buffer.Length != 0)
|
||||
{
|
||||
buffer[0] = 0x30;
|
||||
|
||||
if (IntegerEncoder.Encode(s.Length, 3, buffer, out int nameLength))
|
||||
{
|
||||
buffer = buffer.Slice(nameLength);
|
||||
|
||||
if (buffer.Length >= s.Length)
|
||||
{
|
||||
for (int i = 0; i < s.Length; ++i)
|
||||
{
|
||||
int ch = s[i];
|
||||
Debug.Assert(ch <= 127, "HttpHeaders prevents adding non-ASCII header names.");
|
||||
|
||||
if ((uint)(ch - 'A') <= 'Z' - 'A')
|
||||
{
|
||||
ch |= toLowerMask;
|
||||
}
|
||||
|
||||
buffer[i] = (byte)ch;
|
||||
}
|
||||
|
||||
length = nameLength + s.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* 0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| Required Insert Count (8+) |
|
||||
+---+---------------------------+
|
||||
| S | Delta Base (7+) |
|
||||
+---+---------------------------+
|
||||
| Compressed Headers ...
|
||||
+-------------------------------+
|
||||
*
|
||||
*/
|
||||
private static bool EncodeHeaderBlockPrefix(Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
int length;
|
||||
bytesWritten = 0;
|
||||
// Required insert count as first int
|
||||
if (!IntegerEncoder.Encode(0, 8, destination, out length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesWritten += length;
|
||||
destination = destination.Slice(length);
|
||||
|
||||
// Delta base
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination[0] = 0x00;
|
||||
if (!IntegerEncoder.Encode(0, 7, destination, out length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesWritten += length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO these are fairly hard coded for the first two bytes to be zero.
|
||||
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
|
||||
bool hasValue = _enumerator.MoveNext();
|
||||
Debug.Assert(hasValue == true);
|
||||
|
||||
buffer[0] = 0;
|
||||
buffer[1] = 0;
|
||||
|
||||
return Encode(buffer.Slice(2), out length);
|
||||
}
|
||||
|
||||
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
|
||||
bool hasValue = _enumerator.MoveNext();
|
||||
Debug.Assert(hasValue == true);
|
||||
|
||||
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix
|
||||
buffer[0] = 0;
|
||||
buffer[1] = 0;
|
||||
|
||||
int statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2));
|
||||
bool done = Encode(buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, out int headersLength);
|
||||
length = statusCodeLength + headersLength + 2;
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
public bool Encode(Span<byte> buffer, out int length)
|
||||
{
|
||||
return Encode(buffer, throwIfNoneEncoded: true, out length);
|
||||
}
|
||||
|
||||
private bool Encode(Span<byte> buffer, bool throwIfNoneEncoded, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (!EncodeLiteralHeaderFieldWithoutNameReference(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out int headerLength))
|
||||
{
|
||||
if (length == 0 && throwIfNoneEncoded)
|
||||
{
|
||||
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
length += headerLength;
|
||||
} while (_enumerator.MoveNext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: use H3StaticTable?
|
||||
private int EncodeStatusCode(int statusCode, Span<byte> buffer)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case 200:
|
||||
case 204:
|
||||
case 206:
|
||||
case 304:
|
||||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
// TODO this isn't safe, some index can be larger than 64. Encoded here!
|
||||
buffer[0] = (byte)(0xC0 | H3StaticTable.Instance.StatusIndex[statusCode]);
|
||||
return 1;
|
||||
default:
|
||||
// Send as Literal Header Field Without Indexing - Indexed Name
|
||||
buffer[0] = 0x08;
|
||||
|
||||
ReadOnlySpan<byte> statusBytes = StatusCodes.ToStatusBytes(statusCode);
|
||||
buffer[1] = (byte)statusBytes.Length;
|
||||
statusBytes.CopyTo(buffer.Slice(2));
|
||||
|
||||
return 2 + statusBytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace System.Net.Http.QPack
|
||||
{
|
||||
[Serializable]
|
||||
internal sealed class QPackEncodingException : Exception
|
||||
{
|
||||
public QPackEncodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
public QPackEncodingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
private QPackEncodingException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
The code in this directory is shared between dotnet/runtime and aspnet/AspNetCore. This contains HTTP/3 protocol infrastructure such as a QPACK implementation. Any changes to this dir need to be checked into both repositories.
|
||||
|
||||
dotnet/runtime code paths:
|
||||
- runtime\src\libraries\Common\src\System\Net\Http\Http3
|
||||
- runtime\src\libraries\Common\tests\Tests\System\Net\Http3
|
||||
aspnet/AspNetCore code paths:
|
||||
- AspNetCore\src\Shared\Http3
|
||||
- AspNetCore\src\Shared\test\Shared.Tests\Http3
|
||||
|
||||
## Copying code
|
||||
To copy code from dotnet/runtime to aspnet/AspNetCore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd.
|
||||
To copy code from aspnet/AspNetCore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd.
|
||||
|
||||
## Building dotnet/runtime code:
|
||||
- https://github.com/dotnet/runtime/blob/master/docs/libraries/building/windows-instructions.md
|
||||
- https://github.com/dotnet/runtime/blob/master/docs/libraries/project-docs/developer-guide.md
|
||||
- Run libraries.cmd from the root once: `PS D:\github\runtime> .\libraries.cmd`
|
||||
- Build the individual projects:
|
||||
- `PS D:\github\dotnet\src\libraries\Common\tests> dotnet msbuild /t:rebuild`
|
||||
- `PS D:\github\dotnet\src\libraries\System.Net.Http\src> dotnet msbuild /t:rebuild`
|
||||
|
||||
### Running dotnet/runtime tests:
|
||||
- `PS D:\github\runtime\src\libraries\Common\tests> dotnet msbuild /t:rebuildandtest`
|
||||
- `PS D:\github\runtime\src\libraries\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest`
|
||||
|
||||
## Building aspnet/AspNetCore code:
|
||||
- https://github.com/aspnet/AspNetCore/blob/master/docs/BuildFromSource.md
|
||||
- Run restore in the root once: `PS D:\github\AspNetCore> .\restore.cmd`
|
||||
- Activate to use the repo local runtime: `PS D:\github\AspNetCore> . .\activate.ps1`
|
||||
- Build the individual projects:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet msbuild`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\src> dotnet msbuild`
|
||||
|
||||
### Running aspnet/AspNetCore tests:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet test`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\test> dotnet test`
|
||||
|
|
@ -106,6 +106,18 @@ namespace System.Net.Http.Unit.Tests.HPack
|
|||
_decodedHeaders[headerName] = headerValue;
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
|
||||
{
|
||||
// Not yet implemented for HPACK.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// Not yet implemented for HPACK.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in New Issue