Skip to content

Commit

Permalink
Merge pull request dotnet/corefx#34090 from geoffkizer/http2-settings
Browse files Browse the repository at this point in the history
HTTP2: Improve SETTINGS handling and other small changes

Commit migrated from dotnet/corefx@50ace0b
  • Loading branch information
geoffkizer authored Dec 15, 2018
2 parents a07b92f + 2d8dabc commit 2a5d5f4
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 132 deletions.
64 changes: 64 additions & 0 deletions src/libraries/Common/tests/System/Net/Http/Http2Frames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Buffers.Binary;
using System.Collections.Generic;

namespace System.Net.Test.Common
{
Expand Down Expand Up @@ -37,6 +38,16 @@ public enum FrameFlags : byte
ValidBits = 0b00101101
}

public enum SettingId : ushort
{
HeaderTableSize = 0x1,
EnablePush = 0x2,
MaxConcurrentStreams = 0x3,
InitialWindowSize = 0x4,
MaxFrameSize = 0x5,
MaxHeaderListSize = 0x6
}

public class Frame
{
public int Length;
Expand Down Expand Up @@ -332,4 +343,57 @@ public override string ToString()
return base.ToString() + $"\nUpdateSize: {UpdateSize}";
}
}

public struct SettingsEntry
{
public SettingId SettingId;
public uint Value;
}

public class SettingsFrame : Frame
{
public List<SettingsEntry> Entries;

public SettingsFrame(params SettingsEntry[] entries) :
base(entries.Length * 6, FrameType.Settings, FrameFlags.None, 0)
{
Entries = new List<SettingsEntry>(entries);
}

public static SettingsFrame ReadFrom(Frame header, ReadOnlySpan<byte> buffer)
{
var entries = new List<SettingsEntry>();

while (buffer.Length > 0)
{
SettingId id = (SettingId)BinaryPrimitives.ReadUInt16BigEndian(buffer);
buffer = buffer.Slice(2);
uint value = BinaryPrimitives.ReadUInt32BigEndian(buffer);
buffer = buffer.Slice(4);

entries.Add(new SettingsEntry { SettingId = id, Value = value });
}

return new SettingsFrame(entries.ToArray());
}

public override void WriteTo(Span<byte> buffer)
{
base.WriteTo(buffer);
buffer = buffer.Slice(Frame.FrameHeaderLength);

foreach (SettingsEntry entry in Entries)
{
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)entry.SettingId);
buffer = buffer.Slice(2);
BinaryPrimitives.WriteUInt32BigEndian(buffer, entry.Value);
buffer = buffer.Slice(4);
}
}

public override string ToString()
{
return base.ToString() + $"\nEntry Count: {Entries.Count}";
}
}
}
36 changes: 29 additions & 7 deletions src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class Http2LoopbackServer : IDisposable
private Stream _connectionStream;
private Http2Options _options;
private Uri _uri;
private bool _ignoreSettingsAck;

public Uri Address
{
Expand Down Expand Up @@ -113,6 +114,12 @@ public async Task<Frame> ReadFrameAsync(TimeSpan timeout)
}
}

if (_ignoreSettingsAck && header.Type == FrameType.Settings && header.Flags == FrameFlags.Ack)
{
_ignoreSettingsAck = false;
return await ReadFrameAsync(timeout);
}

// Construct the correct frame type and return it.
switch (header.Type)
{
Expand Down Expand Up @@ -176,27 +183,42 @@ public async Task<string> AcceptConnectionAsync()
}

// Accept connection and handle connection setup
public async Task EstablishConnectionAsync()
public async Task EstablishConnectionAsync(params SettingsEntry[] settingsEntries)
{
await AcceptConnectionAsync();

// Receive the initial client settings frame.
Frame receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30));
Assert.Equal(FrameType.Settings, receivedFrame.Type);
Assert.Equal(FrameFlags.None, receivedFrame.Flags);
Assert.Equal(0, receivedFrame.StreamId);

// Receive the initial client window update frame.
receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30));
Assert.Equal(FrameType.WindowUpdate, receivedFrame.Type);
Assert.Equal(FrameFlags.None, receivedFrame.Flags);
Assert.Equal(0, receivedFrame.StreamId);

// Send the initial server settings frame.
Frame emptySettings = new Frame(0, FrameType.Settings, FrameFlags.None, 0);
await WriteFrameAsync(emptySettings).ConfigureAwait(false);
SettingsFrame settingsFrame = new SettingsFrame(settingsEntries);
await WriteFrameAsync(settingsFrame).ConfigureAwait(false);

// Send the client settings frame ACK.
Frame settingsAck = new Frame(0, FrameType.Settings, FrameFlags.Ack, 0);
await WriteFrameAsync(settingsAck).ConfigureAwait(false);

// Receive the server settings frame ACK.
receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30));
Assert.Equal(FrameType.Settings, receivedFrame.Type);
Assert.True(receivedFrame.AckFlag);
// The client will send us a SETTINGS ACK eventually, but not necessarily right away.
// To simplify frame processing, set this flag to true so we will ignore the next SETTINGS ACK in ReadNextFrame.
_ignoreSettingsAck = true;
}

public async Task<int> ReadRequestHeaderAsync()
{
// Receive HEADERS frame for request.
Frame frame = await ReadFrameAsync(TimeSpan.FromSeconds(30));
Assert.Equal(FrameType.Headers, frame.Type);
Assert.Equal(FrameFlags.EndHeaders | FrameFlags.EndStream, frame.Flags);
return frame.StreamId;
}

public async Task SendDefaultResponseAsync(int streamId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ internal sealed partial class Http2Connection : HttpConnectionBase, IDisposable

private const int InitialWindowSize = 65535;

// We don't really care about limiting control flow at the connection level.
// We limit it per stream, and the user controls how many streams are created.
// So set the connection window size to a large value.
private const int ConnectionWindowSize = 64 * 1024 * 1024;

public Http2Connection(HttpConnectionPool pool, SslStream stream)
{
_pool = pool;
Expand All @@ -68,36 +73,39 @@ public Http2Connection(HttpConnectionPool pool, SslStream stream)

public async Task SetupAsync()
{
_outgoingBuffer.EnsureAvailableSpace(s_http2ConnectionPreface.Length +
FrameHeader.Size + (FrameHeader.SettingLength * 2) +
FrameHeader.Size + FrameHeader.WindowUpdateLength);

// Send connection preface
_outgoingBuffer.EnsureAvailableSpace(s_http2ConnectionPreface.Length);
s_http2ConnectionPreface.AsSpan().CopyTo(_outgoingBuffer.AvailableSpan);
_outgoingBuffer.Commit(s_http2ConnectionPreface.Length);

// Send empty settings frame
_outgoingBuffer.EnsureAvailableSpace(FrameHeader.Size);
WriteFrameHeader(new FrameHeader(0, FrameType.Settings, FrameFlags.None, 0));
// Send SETTINGS frame
WriteFrameHeader(new FrameHeader(FrameHeader.SettingLength * 2, FrameType.Settings, FrameFlags.None, 0));

// TODO: ISSUE 31295: We should disable PUSH_PROMISE here.
// First setting: Disable push promise
BinaryPrimitives.WriteUInt16BigEndian(_outgoingBuffer.AvailableSpan, (ushort)SettingId.EnablePush);
_outgoingBuffer.Commit(2);
BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, 0);
_outgoingBuffer.Commit(4);

// TODO: ISSUE 31298: We should send a connection-level WINDOW_UPDATE to allow
// a large amount of data to be received on the connection.
// We don't care that much about connection-level flow control, we'll manage it per-stream.
// Second setting: Set header table size to 0 to disable dynamic header compression
BinaryPrimitives.WriteUInt16BigEndian(_outgoingBuffer.AvailableSpan, (ushort)SettingId.HeaderTableSize);
_outgoingBuffer.Commit(2);
BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, 0);
_outgoingBuffer.Commit(4);

// Send initial connection-level WINDOW_UPDATE
WriteFrameHeader(new FrameHeader(FrameHeader.WindowUpdateLength, FrameType.WindowUpdate, FrameFlags.None, 0));
BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, (ConnectionWindowSize - InitialWindowSize));
_outgoingBuffer.Commit(4);

await _stream.WriteAsync(_outgoingBuffer.ActiveMemory).ConfigureAwait(false);
_outgoingBuffer.Discard(_outgoingBuffer.ActiveMemory.Length);

_expectingSettingsAck = true;

// Receive the initial SETTINGS frame from the peer.
FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false);
if (frameHeader.Type != FrameType.Settings || frameHeader.AckFlag)
{
throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError);
}

// Process the SETTINGS frame. This will send an ACK.
ProcessSettingsFrame(frameHeader);

ProcessIncomingFrames();
}

Expand Down Expand Up @@ -155,9 +163,20 @@ private async void ProcessIncomingFrames()
{
try
{
// Receive the initial SETTINGS frame from the peer.
FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false);
if (frameHeader.Type != FrameType.Settings || frameHeader.AckFlag)
{
throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError);
}

// Process the SETTINGS frame. This will send an ACK.
ProcessSettingsFrame(frameHeader);

// Keep processing frames as they arrive.
while (true)
{
FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false);
frameHeader = await ReadFrameAsync().ConfigureAwait(false);

switch (frameHeader.Type)
{
Expand Down Expand Up @@ -367,10 +386,48 @@ private void ProcessSettingsFrame(FrameHeader frameHeader)
throw new Http2ProtocolException(Http2ProtocolErrorCode.FrameSizeError);
}

// Just eat settings for now
// Parse settings and process the ones we care about.
ReadOnlySpan<byte> settings = _incomingBuffer.ActiveSpan.Slice(0, frameHeader.Length);
while (settings.Length > 0)
{
Debug.Assert((settings.Length % 6) == 0);

ushort settingId = BinaryPrimitives.ReadUInt16BigEndian(settings);
settings = settings.Slice(2);
uint settingValue = BinaryPrimitives.ReadUInt32BigEndian(settings);
settings = settings.Slice(4);

switch ((SettingId)settingId)
{
case SettingId.MaxConcurrentStreams:
// ISSUE 31296: Handle SETTINGS_MAX_CONCURRENT_STREAMS.
break;

case SettingId.InitialWindowSize:
if (settingValue > 0x7FFFFFFF)
{
throw new Http2ProtocolException(Http2ProtocolErrorCode.FlowControlError);
}

// ISSUE 34059: Handle SETTINGS_INITIAL_WINDOW_SIZE.
break;

case SettingId.MaxFrameSize:
if (settingValue < 16384 || settingValue > 16777215)
{
throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError);
}

// We don't actually store this value; we always send frames of the minimum size (16K).
break;

default:
// All others are ignored because we don't care about them.
// Note, per RFC, unknown settings IDs should be ignored.
break;
}
}

// TODO: ISSUE 31296: We should handle SETTINGS_MAX_CONCURRENT_STREAMS.
// Others we don't care about, or are advisory.
_incomingBuffer.Discard(frameHeader.Length);

// Send acknowledgement
Expand Down Expand Up @@ -911,6 +968,7 @@ private struct FrameHeader
public const int Size = 9;
public const int MaxLength = 16384;

public const int SettingLength = 6; // per setting (total SETTINGS length must be a multiple of this)
public const int PriorityInfoLength = 5; // for both PRIORITY frame and priority info within HEADERS
public const int PingLength = 8;
public const int WindowUpdateLength = 4;
Expand All @@ -919,7 +977,6 @@ private struct FrameHeader

public FrameHeader(int length, FrameType type, FrameFlags flags, int streamId)
{
Debug.Assert(length <= MaxLength);
Debug.Assert(streamId >= 0);

Length = length;
Expand Down Expand Up @@ -950,6 +1007,7 @@ public void WriteTo(Span<byte> buffer)
Debug.Assert(buffer.Length >= Size);
Debug.Assert(Type <= FrameType.Last);
Debug.Assert((Flags & FrameFlags.ValidBits) == Flags);
Debug.Assert(Length <= MaxLength);

buffer[0] = (byte)((Length & 0x00FF0000) >> 16);
buffer[1] = (byte)((Length & 0x0000FF00) >> 8);
Expand Down Expand Up @@ -981,6 +1039,16 @@ private enum FrameFlags : byte
ValidBits = 0b00101101
}

private enum SettingId : ushort
{
HeaderTableSize = 0x1,
EnablePush = 0x2,
MaxConcurrentStreams = 0x3,
InitialWindowSize = 0x4,
MaxFrameSize = 0x5,
MaxHeaderListSize = 0x6
}

// Note that this is safe to be called concurrently by multiple threads.

public sealed override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand Down
Loading

0 comments on commit 2a5d5f4

Please sign in to comment.