Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkgs/http2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
- Gracefully handle receiving headers on a stream that the client has canceled. (#1799)
- Treat incoming server push streams as connection protocol error when pushes are disabled (SETTINGS_ENABLE_PUSH=0).
- Enforce the locally advertised `SETTINGS_MAX_CONCURRENT_STREAMS` limit on incoming remote streams.
- Add `maxHeaderListSize` setting to `ClientSettings` and `ServerSettings`
(defaulting to 256 KiB), advertise `SETTINGS_MAX_HEADER_LIST_SIZE`, enforce
it during HPACK decoding, and limit CONTINUATION frame buffering with $O(N)$
defragmentation.

## 3.0.0

Expand Down
17 changes: 15 additions & 2 deletions pkgs/http2/lib/src/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ abstract class Connection {
_onInitialPeerSettingsReceived.future;

/// The HPack context for this connection.
final HPackContext _hpackContext = HPackContext();
late final HPackContext _hpackContext;

/// The flow window for this connection of the peer.
final Window _peerWindow = Window();
Expand All @@ -118,7 +118,7 @@ abstract class Connection {
final Window _localWindow = Window();

/// Used for defragmenting PushPromise/Header frames.
final FrameDefragmenter _defragmenter = FrameDefragmenter();
late final FrameDefragmenter _defragmenter;

/// The outgoing frames of this connection;
late FrameWriter _frameWriter;
Expand Down Expand Up @@ -164,6 +164,12 @@ abstract class Connection {
StreamSink<List<int>> outgoing,
Settings settingsObject,
) {
var maxHeaderListSize =
settingsObject.maxHeaderListSize ??
FrameDefragmenter.defaultMaxAccumulatedHeaderBlockBytes;
_hpackContext = HPackContext(maxHeaderListSize: maxHeaderListSize);
_defragmenter = FrameDefragmenter(maxHeaderListSize);

// Setup frame reading.
var incomingFrames =
FrameReader(incoming, acknowledgedSettings).startDecoding();
Expand Down Expand Up @@ -282,6 +288,13 @@ abstract class Connection {
);
}

var maxHeaderListSize =
settings.maxHeaderListSize ??
FrameDefragmenter.defaultMaxAccumulatedHeaderBlockBytes;
settingsList.add(
Setting(Setting.SETTINGS_MAX_HEADER_LIST_SIZE, maxHeaderListSize),
);

if (settings is ClientSettings) {
// By default the server is allowed to do server pushes.
if (!settings.allowServerPushes) {
Expand Down
126 changes: 84 additions & 42 deletions pkgs/http2/lib/src/frames/frame_defragmenter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:typed_data';

import '../sync_errors.dart';

import 'frames.dart';

/// Class used for defragmenting [HeadersFrame]s and [PushPromiseFrame]s.
// TODO: Somehow emit an error if too many continuation frames have been sent
// (since we're buffering all of them).
class FrameDefragmenter {
/// The current incomplete [HeadersFrame] fragment.
HeadersFrame? _headersFrame;
/// Maximum total size, in bytes, of a header block accumulated across a
/// HEADERS/PUSH_PROMISE frame and its CONTINUATION frames. A peer that
/// withholds END_HEADERS could otherwise send unbounded CONTINUATION frames
/// and exhaust memory. 256 KiB is far above any legitimate header block but
/// small enough to bound per-connection buffering.
static const int defaultMaxAccumulatedHeaderBlockBytes = 256 * 1024;

final int maxAccumulatedHeaderBlockBytes;

/// The current incomplete [HeadersFrame] or [PushPromiseFrame].
Frame? _incompleteFrame;

final BytesBuilder _builder = BytesBuilder(copy: false);
int _accumulatedFlags = 0;
int _accumulatedLength = 0;

/// The current incomplete [PushPromiseFrame] fragment.
PushPromiseFrame? _pushPromiseFrame;
FrameDefragmenter([
this.maxAccumulatedHeaderBlockBytes = defaultMaxAccumulatedHeaderBlockBytes,
]);

/// Tries to defragment [frame].
///
Expand All @@ -30,66 +44,94 @@ class FrameDefragmenter {
// TODO: Consider handling continuation frames without preceding
// headers/push-promise frame here instead of the call site?
Frame? tryDefragmentFrame(Frame? frame) {
if (_headersFrame != null) {
if (_incompleteFrame != null) {
if (frame is ContinuationFrame) {
if (_headersFrame!.header.streamId != frame.header.streamId) {
if (_incompleteFrame!.header.streamId != frame.header.streamId) {
_reset();
throw ProtocolException(
'Defragmentation: frames have different stream ids.',
);
}
_headersFrame = _headersFrame!.addBlockContinuation(frame);
_builder.add(frame.headerBlockFragment);
_accumulatedFlags |= frame.header.flags;
_accumulatedLength += frame.headerBlockFragment.length;
_checkAccumulatedSize(_builder.length);

if (frame.hasEndHeadersFlag) {
var frame = _headersFrame;
_headersFrame = null;
return frame;
} else {
return null;
}
} else {
throw ProtocolException(
'Defragmentation: Incomplete frame must be followed by '
'continuation frame.',
);
}
} else if (_pushPromiseFrame != null) {
if (frame is ContinuationFrame) {
if (_pushPromiseFrame!.header.streamId != frame.header.streamId) {
throw ProtocolException(
'Defragmentation: frames have different stream ids.',
var saved = _incompleteFrame!;
var finalData = _builder.takeBytes();
var fh = FrameHeader(
_accumulatedLength,
saved.header.type,
_accumulatedFlags,
saved.header.streamId,
);
}
_pushPromiseFrame = _pushPromiseFrame!.addBlockContinuation(frame);
_reset();

if (frame.hasEndHeadersFlag) {
var frame = _pushPromiseFrame;
_pushPromiseFrame = null;
return frame;
if (saved is HeadersFrame) {
return HeadersFrame(
fh,
saved.padLength,
saved.exclusiveDependency,
saved.streamDependency,
saved.weight,
finalData,
);
} else if (saved is PushPromiseFrame) {
return PushPromiseFrame(
fh,
saved.padLength,
saved.promisedStreamId,
finalData,
);
}
} else {
return null;
}
} else {
_reset();
throw ProtocolException(
'Defragmentation: Incomplete frame must be followed by '
'continuation frame.',
);
}
} else {
if (frame is HeadersFrame) {
if (!frame.hasEndHeadersFlag) {
_headersFrame = frame;
return null;
}
} else if (frame is PushPromiseFrame) {
if (!frame.hasEndHeadersFlag) {
_pushPromiseFrame = frame;
return null;
}
if (frame is HeadersFrame && !frame.hasEndHeadersFlag) {
_startDefragmentation(frame, frame.headerBlockFragment);
return null;
} else if (frame is PushPromiseFrame && !frame.hasEndHeadersFlag) {
_startDefragmentation(frame, frame.headerBlockFragment);
return null;
}
}

// If this frame is not relevant for header defragmentation, we pass it to
// the next stage.
return frame;
}

void _startDefragmentation(Frame frame, List<int> initialFragment) {
_checkAccumulatedSize(initialFragment.length);
_incompleteFrame = frame;
_builder.add(initialFragment);
_accumulatedFlags = frame.header.flags;
_accumulatedLength = frame.header.length;
}

void _checkAccumulatedSize(int accumulatedBytes) {
if (accumulatedBytes > maxAccumulatedHeaderBlockBytes) {
_reset();
throw ProtocolException(
'Defragmentation: accumulated header block exceeds '
'$maxAccumulatedHeaderBlockBytes bytes.',
);
}
}

void _reset() {
_incompleteFrame = null;
_builder.clear();
_accumulatedFlags = 0;
_accumulatedLength = 0;
}
}
70 changes: 0 additions & 70 deletions pkgs/http2/lib/src/frames/frame_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,42 +120,6 @@ class HeadersFrame extends Frame {
bool get hasPaddedFlag => _isFlagSet(header.flags, FLAG_PADDED);
bool get hasPriorityFlag => _isFlagSet(header.flags, FLAG_PRIORITY);

HeadersFrame addBlockContinuation(ContinuationFrame frame) {
var fragment = frame.headerBlockFragment;
var flags = header.flags | frame.header.flags;
var fh = FrameHeader(
header.length + fragment.length,
header.type,
flags,
header.streamId,
);

var mergedHeaderBlockFragment = Uint8List(
headerBlockFragment.length + fragment.length,
);

mergedHeaderBlockFragment.setRange(
0,
headerBlockFragment.length,
headerBlockFragment,
);

mergedHeaderBlockFragment.setRange(
headerBlockFragment.length,
mergedHeaderBlockFragment.length,
fragment,
);

return HeadersFrame(
fh,
padLength,
exclusiveDependency,
streamDependency,
weight,
mergedHeaderBlockFragment,
);
}

@override
Map toJson() =>
super.toJson()..addAll({
Expand Down Expand Up @@ -260,40 +224,6 @@ class PushPromiseFrame extends Frame {
bool get hasEndHeadersFlag => _isFlagSet(header.flags, FLAG_END_HEADERS);
bool get hasPaddedFlag => _isFlagSet(header.flags, FLAG_PADDED);

PushPromiseFrame addBlockContinuation(ContinuationFrame frame) {
var fragment = frame.headerBlockFragment;
var flags = header.flags | frame.header.flags;
var fh = FrameHeader(
header.length + fragment.length,
header.type,
flags,
header.streamId,
);

var mergedHeaderBlockFragment = Uint8List(
headerBlockFragment.length + fragment.length,
);

mergedHeaderBlockFragment.setRange(
0,
headerBlockFragment.length,
headerBlockFragment,
);

mergedHeaderBlockFragment.setRange(
headerBlockFragment.length,
mergedHeaderBlockFragment.length,
fragment,
);

return PushPromiseFrame(
fh,
padLength,
promisedStreamId,
mergedHeaderBlockFragment,
);
}

@override
Map toJson() =>
super.toJson()..addAll({
Expand Down
31 changes: 25 additions & 6 deletions pkgs/http2/lib/src/hpack/hpack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ class HPackDecodingException implements Exception {
/// This is a statefull class, so encoding/decoding changes internal state.
class HPackContext {
final HPackEncoder encoder = HPackEncoder();
final HPackDecoder decoder = HPackDecoder();
final HPackDecoder decoder;

HPackContext({
int maxSendingHeaderTableSize = 4096,
int maxReceivingHeaderTableSize = 4096,
}) {
int? maxHeaderListSize,
}) : decoder = HPackDecoder(maxHeaderListSize: maxHeaderListSize) {
encoder.updateMaxSendingHeaderTableSize(maxSendingHeaderTableSize);
decoder.updateMaxReceivingHeaderTableSize(maxReceivingHeaderTableSize);
}
Expand All @@ -58,10 +59,13 @@ class Header {

/// A stateful HPACK decoder.
class HPackDecoder {
final int? maxHeaderListSize;
late int _maxHeaderTableSize;

final IndexTable _table = IndexTable();

HPackDecoder({this.maxHeaderListSize});

void updateMaxReceivingHeaderTableSize(int newMaximumSize) {
_maxHeaderTableSize = newMaximumSize;
}
Expand Down Expand Up @@ -121,6 +125,21 @@ class HPackDecoder {

try {
var headers = <Header>[];
var totalHeaderListSize = 0;

void addHeader(Header header) {
headers.add(header);
if (maxHeaderListSize != null) {
totalHeaderListSize += header.name.length + header.value.length + 32;
if (totalHeaderListSize > maxHeaderListSize!) {
throw HPackDecodingException(
'Header list size exceeds maximum allowed size of '
'$maxHeaderListSize octets.',
);
}
}
}

while (offset < data.length) {
var byte = data[offset];
var isIndexedField = (byte & 0x80) != 0;
Expand All @@ -133,15 +152,15 @@ class HPackDecoder {
if (isIndexedField) {
var index = readInteger(7);
var field = _table.lookup(index);
headers.add(field);
addHeader(field);
} else if (isIncrementalIndexing) {
var field = readHeaderFieldInternal(readInteger(6));
_table.addHeaderField(field);
headers.add(field);
addHeader(field);
} else if (isWithoutIndexing) {
headers.add(readHeaderFieldInternal(readInteger(4)));
addHeader(readHeaderFieldInternal(readInteger(4)));
} else if (isNeverIndexing) {
headers.add(
addHeader(
readHeaderFieldInternal(readInteger(4), neverIndexed: true),
);
} else if (isDynamicTableSizeUpdate) {
Expand Down
Loading
Loading