blob: 8caf4f49df46026b9d8b7b62303b498377000a57 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// 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:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:http2/src/connection_preface.dart';
import 'package:http2/src/frames/frames.dart';
import 'package:http2/src/hpack/hpack.dart';
import 'package:http2/src/settings/settings.dart';
import 'package:http2/transport.dart';
import 'package:test/test.dart';
/// The flow-control settings, checked frame by frame against a hand-driven
/// peer: every behaviour here is a statement about what goes on the wire, not
/// about timing.
const _data = 0x0;
const _headers = 0x1;
const _settings = 0x4;
const _ping = 0x6;
const _windowUpdate = 0x8;
const _flagAck = 0x1;
const _flagEndStream = 0x1;
const _flagEndHeaders = 0x4;
const _settingInitialWindowSize = 0x4;
const _settingMaxFrameSize = 0x5;
typedef _Frame = ({int type, int flags, int stream, Uint8List payload});
/// A peer that speaks raw HTTP/2 frames over plain TCP (h2c): it records
/// every frame the client sends and writes whatever frames a test asks for.
class _RawPeer {
_RawPeer._(this._server);
final ServerSocket _server;
late Socket _socket;
Socket? _clientSocket;
final frames = <_Frame>[];
final _buffer = BytesBuilder();
var _prefaceSeen = false;
final _connected = Completer<void>();
final _changed = StreamController<void>.broadcast();
static Future<_RawPeer> start() async {
final peer = _RawPeer._(
await ServerSocket.bind(InternetAddress.loopbackIPv4, 0),
);
peer._server.listen((socket) {
peer._socket = socket;
peer._connected.complete();
socket.listen(peer._onBytes);
});
return peer;
}
Future<ClientTransportConnection> connect(ClientSettings settings) async {
_clientSocket = await Socket.connect(
InternetAddress.loopbackIPv4,
_server.port,
);
final client = ClientTransportConnection.viaSocket(
_clientSocket!,
settings: settings,
);
await _connected.future;
return client;
}
void _onBytes(List<int> bytes) {
_buffer.add(bytes);
var all = _buffer.takeBytes();
if (!_prefaceSeen) {
if (all.length < 24) {
_buffer.add(all);
return;
}
all = all.sublist(24); // PRI * HTTP/2.0 ... preface
_prefaceSeen = true;
}
var offset = 0;
while (all.length - offset >= 9) {
final length =
(all[offset] << 16) | (all[offset + 1] << 8) | all[offset + 2];
if (all.length - offset < 9 + length) break;
frames.add((
type: all[offset + 3],
flags: all[offset + 4],
stream: ByteData.sublistView(all, offset + 5).getUint32(0) & 0x7fffffff,
payload: Uint8List.fromList(
all.sublist(offset + 9, offset + 9 + length),
),
));
offset += 9 + length;
}
_buffer.add(all.sublist(offset));
_changed.add(null);
}
var _pings = 0;
/// Resolves once the client has answered a PING sent now. It reads frames
/// in order and answers a PING as it reads it, so every frame it wrote
/// before has arrived by then: a deterministic point to check that some
/// frame was NOT sent.
Future<void> barrier() async {
final id = ++_pings;
send(_ping, 0, 0, (ByteData(8)..setUint32(4, id)).buffer.asUint8List());
await until(
() => frames.any(
(f) =>
f.type == _ping &&
f.flags & _flagAck != 0 &&
ByteData.sublistView(f.payload).getUint32(4) == id,
),
);
}
/// Resolves once [test] holds for the frames seen so far.
Future<void> until(bool Function() test) async {
while (!test()) {
await _changed.stream.first.timeout(const Duration(seconds: 5));
}
}
void send(int type, int flags, int stream, List<int> payload) {
final header =
ByteData(9)
..setUint8(0, payload.length >> 16)
..setUint16(1, payload.length & 0xffff)
..setUint8(3, type)
..setUint8(4, flags)
..setUint32(5, stream);
_socket
..add(header.buffer.asUint8List())
..add(payload);
}
/// The server's own (empty) SETTINGS.
void sendSettings() => send(_settings, 0, 0, const []);
void ackSettings() => send(_settings, _flagAck, 0, const []);
/// `:status: 200` -- HPACK static table index 8.
void sendOkHeaders(int stream, {bool endStream = false}) => send(
_headers,
_flagEndHeaders | (endStream ? _flagEndStream : 0),
stream,
const [0x88],
);
Future<void> close() async {
await _changed.close();
_clientSocket?.destroy();
_socket.destroy();
await _server.close();
}
}
Map<int, int> _settingsOf(_Frame frame) {
final view = ByteData.sublistView(frame.payload);
return {
for (var i = 0; i + 6 <= frame.payload.length; i += 6)
view.getUint16(i): view.getUint32(i + 2),
};
}
int _increment(_Frame frame) =>
ByteData.sublistView(frame.payload).getUint32(0) & 0x7fffffff;
List<Header> _get(String path) => [
Header.ascii(':method', 'GET'),
Header.ascii(':path', path),
Header.ascii(':scheme', 'http'),
Header.ascii(':authority', 'peer'),
];
void main() {
late _RawPeer peer;
late ClientTransportConnection client;
var connected = false;
/// Large windows and frames, as a client moving bulk data would choose.
const tuned = ClientSettings(
streamWindowSize: 6 << 20,
connectionWindowSize: 16 << 20,
maxFrameSize: 1 << 20,
);
Future<void> connect(ClientSettings settings) async {
peer = await _RawPeer.start();
client = await peer.connect(settings);
connected = true;
await peer.until(() => peer.frames.any((f) => f.type == _settings));
}
tearDown(() async {
if (!connected) return;
connected = false;
await client.terminate();
await peer.close();
});
test(
'the defaults send no frame size and grant no connection window',
() async {
await connect(const ClientSettings());
peer.sendSettings();
await peer.barrier();
expect(
_settingsOf(peer.frames.first).keys,
isNot(contains(_settingMaxFrameSize)),
);
expect(peer.frames.where((f) => f.type == _windowUpdate), isEmpty);
},
);
test('the initial SETTINGS carry the stream window and frame size, and '
'the connection window is raised on stream 0 right after', () async {
await connect(tuned);
await peer.until(() => peer.frames.any((f) => f.type == _windowUpdate));
final settings = peer.frames.first;
expect(settings.type, _settings);
expect(
_settingsOf(settings),
containsPair(_settingInitialWindowSize, 6 << 20),
);
expect(_settingsOf(settings), containsPair(_settingMaxFrameSize, 1 << 20));
final raise = peer.frames[1];
expect(raise.type, _windowUpdate);
expect(raise.stream, 0);
expect(_increment(raise), (16 << 20) - 65535);
});
test('before the SETTINGS are acknowledged, a stream takes one DATA frame '
'larger than both 16 KiB and 65535 bytes: frame size and stream window '
'count from the moment they were sent', () async {
await connect(tuned);
// No ACK is ever sent.
peer.sendSettings();
final stream = client.makeRequest(_get('/one'), endStream: true);
await peer.until(() => peer.frames.any((f) => f.type == _headers));
peer
..sendOkHeaders(1)
..send(_data, _flagEndStream, 1, Uint8List(100000));
var received = 0;
await stream.incomingMessages.forEach((m) {
if (m is DataStreamMessage) received += m.bytes.length;
});
expect(received, 100000);
});
test('the ACK of our SETTINGS never changes what we SEND on an open '
'stream; checked with a window below 65535, which is not pre-applied, '
'so the ACK alone carries the change', () async {
await connect(const ClientSettings(streamWindowSize: 1000));
// The peer grants the connection plenty, so only the stream window can
// hold the upload back.
peer
..sendSettings()
..send(_windowUpdate, 0, 0, [0x00, 0x10, 0x00, 0x00]);
final stream = client.makeRequest([
Header.ascii(':method', 'POST'),
Header.ascii(':path', '/upload'),
Header.ascii(':scheme', 'http'),
Header.ascii(':authority', 'peer'),
]);
stream.sendData(Uint8List(200000), endStream: true);
int sent() => peer.frames
.where((f) => f.type == _data && f.stream == 1)
.fold<int>(0, (n, f) => n + f.payload.length);
await peer.until(() => sent() >= 65535);
peer.ackSettings();
// The first PING is read after the ACK, possibly in the same read; the
// second only once the first is answered, so after everything the ACK
// set off.
await peer.barrier();
await peer.barrier();
expect(sent(), 65535);
// The peer grants the stream another default window: all of it must be
// usable, so the ACK of our own smaller window did not shrink the send
// window.
peer.send(_windowUpdate, 0, 1, [0x00, 0x00, 0xff, 0xff]);
await peer.until(() => sent() > 65535);
await peer.barrier();
await peer.barrier();
expect(sent(), 2 * 65535);
});
test('with large windows, 4 MiB received costs one WINDOW_UPDATE, not two '
'per DATA frame', () async {
await connect(tuned);
peer
..sendSettings()
..ackSettings();
final stream = client.makeRequest(_get('/big'), endStream: true);
await peer.until(() => peer.frames.any((f) => f.type == _headers));
final updatesBefore =
peer.frames.where((f) => f.type == _windowUpdate).length;
peer.sendOkHeaders(1);
const frames = (4 << 20) ~/ 16384;
for (var i = 0; i < frames; i++) {
peer.send(
_data,
i == frames - 1 ? _flagEndStream : 0,
1,
Uint8List(16384),
);
}
var received = 0;
await stream.incomingMessages.forEach((m) {
if (m is DataStreamMessage) received += m.bytes.length;
});
await peer.barrier();
expect(received, 4 << 20);
// The stream's half window (3 MiB) was crossed once; the connection's
// (8 MiB) never. Frame-by-frame acknowledgement sends 512 here.
final updates =
peer.frames
.where((f) => f.type == _windowUpdate)
.skip(updatesBefore)
.toList();
expect([for (final u in updates) u.stream], [1]);
expect(_increment(updates.single), greaterThanOrEqualTo(3 << 20));
});
test('settings outside the ranges of the protocol are refused before the '
'connection is set up', () {
for (final settings in [
const ClientSettings(maxFrameSize: (1 << 14) - 1),
const ClientSettings(maxFrameSize: 1 << 24),
const ClientSettings(streamWindowSize: 1 << 31),
const ClientSettings(connectionWindowSize: 65534),
const ClientSettings(connectionWindowSize: 1 << 31),
]) {
final outgoing = StreamController<List<int>>();
expect(
() => ClientTransportConnection.viaStreams(
const Stream<List<int>>.empty(),
outgoing,
settings: settings,
),
throwsArgumentError,
);
unawaited(outgoing.close());
}
});
test('a server with a stream window below 65535 accepts a request sent '
'before the client read its SETTINGS: until then the client may send '
'within the default window (RFC 9113 sections 3.4 and 6.9.2)', () async {
final clientOut = StreamController<List<int>>();
final fromServer = StreamController<List<int>>();
Stream<List<int>> toServer() async* {
yield CONNECTION_PREFACE;
yield* clientOut.stream;
}
final server = ServerTransportConnection.viaStreams(
toServer(),
fromServer.sink,
settings: const ServerSettings(streamWindowSize: 1000),
);
final goaways = <GoawayFrame>[];
FrameReader(fromServer.stream, ActiveSettings()).startDecoding().listen((
frame,
) {
if (frame is GoawayFrame) goaways.add(frame);
}, onError: (_) {});
// SETTINGS, HEADERS and 65535 bytes of DATA in one go, without reading
// the server's SETTINGS.
FrameWriter(HPackEncoder(), clientOut.sink, ActiveSettings())
..writeSettingsFrame([])
..writeHeadersFrame(1, [
Header.ascii(':method', 'POST'),
Header.ascii(':path', '/upload'),
Header.ascii(':scheme', 'http'),
Header.ascii(':authority', 'peer'),
], endStream: false)
..writeDataFrame(1, Uint8List(65535), endStream: true);
var received = 0;
final stream = await server.incomingStreams.first;
await stream.incomingMessages.forEach((m) {
if (m is DataStreamMessage) received += m.bytes.length;
});
expect(received, 65535);
expect(goaways, isEmpty);
await server.terminate();
});
}