Skip to content
Open
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
3 changes: 3 additions & 0 deletions pkgs/http2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
- Replenish the connection-level receive window and release active stream slots
when a stream with buffered unconsumed `DATA` frames is terminated, reset, or
its incoming subscription is cancelled.
- Ignore `PRIORITY` and unknown extension frames on open, idle, and closed
streams, and treat `SETTINGS`, `PING`, and `GOAWAY` frames with a non-zero
stream ID as a connection-level `PROTOCOL_ERROR`.

## 3.1.0

Expand Down
10 changes: 10 additions & 0 deletions pkgs/http2/lib/src/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,16 @@ abstract class Connection {
'Cannot handle frame type ${frame.runtimeType} with stream-id 0.',
);
}
} else if (frame is SettingsFrame ||
frame is PingFrame ||
frame is GoawayFrame) {
// RFC 9113 sections 6.5, 6.7, and 6.8: SETTINGS, PING, and GOAWAY frames
// always apply to the connection as a whole; a non-zero stream identifier
// must be treated as a connection error of type PROTOCOL_ERROR.
throw ProtocolException(
'Cannot handle frame type ${frame.runtimeType} with non-zero '
'stream-id ${frame.header.streamId}.',
);
} else {
_streams.processStreamFrame(_state, frame);
}
Expand Down
13 changes: 12 additions & 1 deletion pkgs/http2/lib/src/streams/stream_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -745,14 +745,18 @@ class StreamHandler extends Object with TerminatableMixin, ClosableMixin {
// unused or closed parent stream.
//
// As long as we do not handle stream priorities, we can safely ignore
// such frames on idle streams.
// such frames on idle or closed streams.
//
// NOTE: Firefox for example sends [PriorityFrame]s even without
// opening any streams (e.g. streams 3,5,7,9,11 [PriorityFrame]s and
// stream 13 is the first real stream opened by a [HeadersFrame].
//
// TODO: When implementing priorities for HTTP/2 streams, these frames
// need to be taken into account.
} else if (frame is UnknownFrame) {
// RFC 9113 section 4.1: "Implementations MUST ignore and discard
// frames of unknown types." That includes frames an extension sends
// on streams this end considers idle or closed.
} else if (frame is PushPromiseFrame) {
throw ProtocolException(
'Cannot push on a non-existent stream '
Expand Down Expand Up @@ -794,6 +798,13 @@ class StreamHandler extends Object with TerminatableMixin, ClosableMixin {
_handleWindowUpdate(stream, frame);
} else if (frame is RstStreamFrame) {
_handleRstFrame(stream, frame);
} else if (frame is PriorityFrame) {
// RFC 9113 section 6.3: PRIORITY frames are deprecated and carry
// nothing this end acts on; they can be sent on a stream in any
// state.
} else if (frame is UnknownFrame) {
// RFC 9113 section 4.1: "Implementations MUST ignore and discard
// frames of unknown types."
} else {
throw ProtocolException(
'Unsupported frame type ${frame.runtimeType}.',
Expand Down
153 changes: 153 additions & 0 deletions pkgs/http2/test/server_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,159 @@ void main() {

await Future.wait([serverFun(), clientFun()]);
});

test(
'ignores-priority-and-unknown-frames-on-open-and-idle-streams',
() async {
final streams = ClientErrorStreams();
final server = streams.serverConnection;
final clientReader = streams.clientConnectionFrameReader;
final encoder = HPackEncoder();

Future<Frame> nextFrame() async {
expect(await clientReader.moveNext(), isTrue);
return clientReader.current;
}

Future<void> serverFun() async {
final it = StreamIterator(server.incomingStreams);
expect(await it.moveNext(), isTrue);
final stream = it.current;
final messages = await stream.incomingMessages.toList();
expect(messages, hasLength(2));
expect(messages[0], isA<HeadersStreamMessage>());
expect(messages[1], isA<DataStreamMessage>());
stream.sendHeaders([
Header.ascii(':status', '200'),
], endStream: true);
expect(await it.moveNext(), isFalse);
await server.finish();
}

Future<void> clientFun() async {
streams.writeConnectionPreface();
expect(await nextFrame(), isA<SettingsFrame>());
streams.writeRawFrame(
type: FrameType.SETTINGS,
flags: SettingsFrame.FLAG_ACK,
streamId: 0,
payload: const [],
);
streams.writeRawFrame(
type: FrameType.SETTINGS,
flags: 0,
streamId: 0,
payload: const [],
);
expect(await nextFrame(), isA<SettingsFrame>());

streams.writeRawFrame(
type: FrameType.HEADERS,
flags: HeadersFrame.FLAG_END_HEADERS,
streamId: 1,
payload: encoder.encode([Header.ascii('a', 'b')]),
);
// Send PRIORITY on open stream 1 and unknown frame (0xfa) on open
// stream 1 and idle stream 5.
streams.writeRawFrame(
type: FrameType.PRIORITY,
flags: 0,
streamId: 1,
payload: const [0, 0, 0, 0, 16],
);
streams.writeRawFrame(
type: 0xfa,
flags: 0,
streamId: 1,
payload: const [1, 2],
);
streams.writeRawFrame(
type: 0xfa,
flags: 0,
streamId: 5,
payload: const [],
);
streams.writeRawFrame(
type: FrameType.DATA,
flags: DataFrame.FLAG_END_STREAM,
streamId: 1,
payload: const [42],
);

expect(await nextFrame(), isA<WindowUpdateFrame>());
expect(await nextFrame(), isA<WindowUpdateFrame>());
expect(await nextFrame(), isA<HeadersFrame>());

streams.writeRawFrame(
type: FrameType.GOAWAY,
flags: 0,
streamId: 0,
payload: const [0, 0, 0, 1, 0, 0, 0, 0],
);
expect(await clientReader.moveNext(), isFalse);
}

await Future.wait([serverFun(), clientFun()]);
},
);

test('rejects-control-frames-with-nonzero-stream-id', () async {
for (final frameType in [
FrameType.SETTINGS,
FrameType.PING,
FrameType.GOAWAY,
]) {
final streams = ClientErrorStreams();
final server = streams.serverConnection;
final incoming = server.incomingStreams.toList();
final clientReader = streams.clientConnectionFrameReader;

Future<Frame> nextFrame() async {
expect(await clientReader.moveNext(), isTrue);
return clientReader.current;
}

streams.writeConnectionPreface();
expect(await nextFrame(), isA<SettingsFrame>());
streams.writeRawFrame(
type: FrameType.SETTINGS,
flags: SettingsFrame.FLAG_ACK,
streamId: 0,
payload: const [],
);
streams.writeRawFrame(
type: FrameType.SETTINGS,
flags: 0,
streamId: 0,
payload: const [],
);
expect(await nextFrame(), isA<SettingsFrame>());

final payload = switch (frameType) {
FrameType.SETTINGS => const <int>[],
FrameType.PING => const <int>[0, 0, 0, 0, 0, 0, 0, 1],
FrameType.GOAWAY => const <int>[0, 0, 0, 0, 0, 0, 0, 0],
_ => const <int>[],
};
streams.writeRawFrame(
type: frameType,
flags: 0,
streamId: 1,
payload: payload,
);

expect(
await nextFrame(),
isA<GoawayFrame>().having(
(f) => f.errorCode,
'errorCode',
ErrorCode.PROTOCOL_ERROR,
),
);
expect(await clientReader.moveNext(), isFalse);
expect(await incoming, isEmpty);
}
});
});

group('server-errors', () {
Expand Down
Loading