Replat on HTTP/3 changes, fixing up minor nits to be compatible with h3-25 (#18912)

This commit is contained in:
Justin Kotalik 2020-02-11 11:17:42 -08:00 committed by GitHub
parent f7dc095e9a
commit 6042fab581
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1647 additions and 1068 deletions

View File

@ -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
{

View File

@ -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>

View File

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

View File

@ -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);

View File

@ -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 = () => { };

View File

@ -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
}
}

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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; }

View File

@ -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;
}
}
}
}

View File

@ -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?
}

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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) =>
{

View File

@ -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()

View File

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

View File

@ -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()
{

View File

@ -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>

View File

@ -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)
{

View File

@ -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

View File

@ -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);

View File

@ -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>

View File

@ -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);
}

View File

@ -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`

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 doesnt 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 clients stream terminated without containing a fully-formed request.
/// </summary>
RequestIncomplete = 0x10e,
RequestIncomplete = 0x10d,
/// <summary>
/// HTTP_EARLY_RESPONSE (0x10F):
/// The remainder of the clients 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,
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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`

View File

@ -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]