blob: 4882562338b6da43727dc87e9f16cef4735055fe [file] [log] [blame]
// Copyright (c) 2015, 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 'package:http2/src/hpack/hpack.dart';
import 'package:test/test.dart';
void main() {
group('hpack', () {
group('hpack-spec-decoder', () {
test('C.3 request without huffman encoding', () {
var context = HPackContext();
List<Header> headers;
// First request
headers = context.decoder.decode([
0x82,
0x86,
0x84,
0x41,
0x0f,
0x77,
0x77,
0x77,
0x2e,
0x65,
0x78,
0x61,
0x6d,
0x70,
0x6c,
0x65,
0x2e,
0x63,
0x6f,
0x6d,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'http'));
expect(headers[2], isHeader(':path', '/'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
// Second request
headers = context.decoder.decode([
0x82,
0x86,
0x84,
0xbe,
0x58,
0x08,
0x6e,
0x6f,
0x2d,
0x63,
0x61,
0x63,
0x68,
0x65,
]);
expect(headers, hasLength(5));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'http'));
expect(headers[2], isHeader(':path', '/'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
expect(headers[4], isHeader('cache-control', 'no-cache'));
// Third request
headers = context.decoder.decode([
0x82,
0x87,
0x85,
0xbf,
0x40,
0x0a,
0x63,
0x75,
0x73,
0x74,
0x6f,
0x6d,
0x2d,
0x6b,
0x65,
0x79,
0x0c,
0x63,
0x75,
0x73,
0x74,
0x6f,
0x6d,
0x2d,
0x76,
0x61,
0x6c,
0x75,
0x65,
]);
expect(headers, hasLength(5));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'https'));
expect(headers[2], isHeader(':path', '/index.html'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
expect(headers[4], isHeader('custom-key', 'custom-value'));
});
test('C.4 request with huffman encoding', () {
var context = HPackContext();
List<Header> headers;
// First request
headers = context.decoder.decode([
0x82,
0x86,
0x84,
0x41,
0x8c,
0xf1,
0xe3,
0xc2,
0xe5,
0xf2,
0x3a,
0x6b,
0xa0,
0xab,
0x90,
0xf4,
0xff,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'http'));
expect(headers[2], isHeader(':path', '/'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
// Second request
headers = context.decoder.decode([
0x82,
0x86,
0x84,
0xbe,
0x58,
0x86,
0xa8,
0xeb,
0x10,
0x64,
0x9c,
0xbf,
]);
expect(headers, hasLength(5));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'http'));
expect(headers[2], isHeader(':path', '/'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
expect(headers[4], isHeader('cache-control', 'no-cache'));
// Third request
headers = context.decoder.decode([
0x82,
0x87,
0x85,
0xbf,
0x40,
0x88,
0x25,
0xa8,
0x49,
0xe9,
0x5b,
0xa9,
0x7d,
0x7f,
0x89,
0x25,
0xa8,
0x49,
0xe9,
0x5b,
0xb8,
0xe8,
0xb4,
0xbf,
]);
expect(headers, hasLength(5));
expect(headers[0], isHeader(':method', 'GET'));
expect(headers[1], isHeader(':scheme', 'https'));
expect(headers[2], isHeader(':path', '/index.html'));
expect(headers[3], isHeader(':authority', 'www.example.com'));
expect(headers[4], isHeader('custom-key', 'custom-value'));
});
test('C.5 response without huffman encoding', () {
var context = HPackContext();
List<Header> headers;
// First response
headers = context.decoder.decode([
0x48,
0x03,
0x33,
0x30,
0x32,
0x58,
0x07,
0x70,
0x72,
0x69,
0x76,
0x61,
0x74,
0x65,
0x61,
0x1d,
0x4d,
0x6f,
0x6e,
0x2c,
0x20,
0x32,
0x31,
0x20,
0x4f,
0x63,
0x74,
0x20,
0x32,
0x30,
0x31,
0x33,
0x20,
0x32,
0x30,
0x3a,
0x31,
0x33,
0x3a,
0x32,
0x31,
0x20,
0x47,
0x4d,
0x54,
0x6e,
0x17,
0x68,
0x74,
0x74,
0x70,
0x73,
0x3a,
0x2f,
0x2f,
0x77,
0x77,
0x77,
0x2e,
0x65,
0x78,
0x61,
0x6d,
0x70,
0x6c,
0x65,
0x2e,
0x63,
0x6f,
0x6d,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':status', '302'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:21 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
// Second response
headers = context.decoder.decode([
0x48,
0x03,
0x33,
0x30,
0x37,
0xc1,
0xc0,
0xbf,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':status', '307'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:21 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
// Third response
headers = context.decoder.decode([
0x88,
0xc1,
0x61,
0x1d,
0x4d,
0x6f,
0x6e,
0x2c,
0x20,
0x32,
0x31,
0x20,
0x4f,
0x63,
0x74,
0x20,
0x32,
0x30,
0x31,
0x33,
0x20,
0x32,
0x30,
0x3a,
0x31,
0x33,
0x3a,
0x32,
0x32,
0x20,
0x47,
0x4d,
0x54,
0xc0,
0x5a,
0x04,
0x67,
0x7a,
0x69,
0x70,
0x77,
0x38,
0x66,
0x6f,
0x6f,
0x3d,
0x41,
0x53,
0x44,
0x4a,
0x4b,
0x48,
0x51,
0x4b,
0x42,
0x5a,
0x58,
0x4f,
0x51,
0x57,
0x45,
0x4f,
0x50,
0x49,
0x55,
0x41,
0x58,
0x51,
0x57,
0x45,
0x4f,
0x49,
0x55,
0x3b,
0x20,
0x6d,
0x61,
0x78,
0x2d,
0x61,
0x67,
0x65,
0x3d,
0x33,
0x36,
0x30,
0x30,
0x3b,
0x20,
0x76,
0x65,
0x72,
0x73,
0x69,
0x6f,
0x6e,
0x3d,
0x31,
]);
expect(headers, hasLength(6));
expect(headers[0], isHeader(':status', '200'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:22 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
expect(headers[4], isHeader('content-encoding', 'gzip'));
expect(
headers[5],
isHeader(
'set-cookie',
'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1',
),
);
});
test('C.6 response with huffman encoding', () {
var context = HPackContext();
List<Header> headers;
// First response
headers = context.decoder.decode([
0x48,
0x82,
0x64,
0x02,
0x58,
0x85,
0xae,
0xc3,
0x77,
0x1a,
0x4b,
0x61,
0x96,
0xd0,
0x7a,
0xbe,
0x94,
0x10,
0x54,
0xd4,
0x44,
0xa8,
0x20,
0x05,
0x95,
0x04,
0x0b,
0x81,
0x66,
0xe0,
0x82,
0xa6,
0x2d,
0x1b,
0xff,
0x6e,
0x91,
0x9d,
0x29,
0xad,
0x17,
0x18,
0x63,
0xc7,
0x8f,
0x0b,
0x97,
0xc8,
0xe9,
0xae,
0x82,
0xae,
0x43,
0xd3,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':status', '302'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:21 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
// Second response
headers = context.decoder.decode([
0x48,
0x83,
0x64,
0x0e,
0xff,
0xc1,
0xc0,
0xbf,
]);
expect(headers, hasLength(4));
expect(headers[0], isHeader(':status', '307'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:21 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
// Third response
headers = context.decoder.decode([
0x88,
0xc1,
0x61,
0x96,
0xd0,
0x7a,
0xbe,
0x94,
0x10,
0x54,
0xd4,
0x44,
0xa8,
0x20,
0x05,
0x95,
0x04,
0x0b,
0x81,
0x66,
0xe0,
0x84,
0xa6,
0x2d,
0x1b,
0xff,
0xc0,
0x5a,
0x83,
0x9b,
0xd9,
0xab,
0x77,
0xad,
0x94,
0xe7,
0x82,
0x1d,
0xd7,
0xf2,
0xe6,
0xc7,
0xb3,
0x35,
0xdf,
0xdf,
0xcd,
0x5b,
0x39,
0x60,
0xd5,
0xaf,
0x27,
0x08,
0x7f,
0x36,
0x72,
0xc1,
0xab,
0x27,
0x0f,
0xb5,
0x29,
0x1f,
0x95,
0x87,
0x31,
0x60,
0x65,
0xc0,
0x03,
0xed,
0x4e,
0xe5,
0xb1,
0x06,
0x3d,
0x50,
0x07,
]);
expect(headers, hasLength(6));
expect(headers[0], isHeader(':status', '200'));
expect(headers[1], isHeader('cache-control', 'private'));
expect(headers[2], isHeader('date', 'Mon, 21 Oct 2013 20:13:22 GMT'));
expect(headers[3], isHeader('location', 'https://www.example.com'));
expect(headers[4], isHeader('content-encoding', 'gzip'));
expect(
headers[5],
isHeader(
'set-cookie',
'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1',
),
);
});
});
group('negative-decoder-tests', () {
test('invalid-integer-encoding', () {
var context = HPackContext();
expect(
() => context.decoder.decode([1 << 6, 0xff]),
throwsA(isHPackDecodingException),
);
});
test('index-out-of-table-size', () {
var context = HPackContext();
expect(
() => context.decoder.decode([0x7f]),
throwsA(isHPackDecodingException),
);
});
test('invalid-update-dynamic-table-size', () {
var context = HPackContext();
expect(
() => context.decoder.decode([0x3f]),
throwsA(isHPackDecodingException),
);
});
test('update-dynamic-table-size-too-high', () {
var context = HPackContext();
// Tries to set dynamic table to 4097 (max is 4096 by default)
var bytes = TestHelper.newInteger(0x20, 5, 4097);
expect(
() => context.decoder.decode(bytes),
throwsA(isHPackDecodingException),
);
});
});
group('custom decoder tests', () {
const char0 = 0x30;
const char1 = 0x31;
const char2 = 0x31;
const char3 = 0x31;
const charA = 0x61;
const charB = 0x62;
const charC = 0x63;
const charD = 0x64;
test('update-dynamic-table-size-too-high', () {
var context = HPackContext();
// Sets dynamic table to 4096
expect(
context.decoder.decode(TestHelper.newInteger(0x20, 5, 4096)),
<void>[],
);
});
test('dynamic table entry', () {
List<Header> headers;
var context = HPackContext();
var buffer = <int>[];
buffer.addAll(TestHelper.insertIntoDynamicTable(2048, char0, charA));
buffer.addAll(TestHelper.insertIntoDynamicTable(2048, char1, charB));
buffer.addAll(TestHelper.dynamicTableLookup(0));
buffer.addAll(TestHelper.dynamicTableLookup(1));
buffer.addAll(TestHelper.dynamicTableLookup(0));
buffer.addAll(TestHelper.dynamicTableLookup(1));
buffer.addAll(TestHelper.insertIntoDynamicTable(1024, char2, charC));
buffer.addAll(TestHelper.insertIntoDynamicTable(1024, char3, charD));
buffer.addAll(TestHelper.dynamicTableLookup(0));
buffer.addAll(TestHelper.dynamicTableLookup(1));
buffer.addAll(TestHelper.dynamicTableLookup(2));
headers = context.decoder.decode(buffer);
expect(headers, hasLength(11));
TestHelper.expectHeader(headers[0], 2048, char0, charA);
TestHelper.expectHeader(headers[1], 2048, char1, charB);
TestHelper.expectHeader(headers[2], 2048, char1, charB);
TestHelper.expectHeader(headers[3], 2048, char0, charA);
TestHelper.expectHeader(headers[4], 2048, char1, charB);
TestHelper.expectHeader(headers[5], 2048, char0, charA);
TestHelper.expectHeader(headers[6], 1024, char2, charC);
TestHelper.expectHeader(headers[7], 1024, char3, charD);
TestHelper.expectHeader(headers[8], 1024, char1, charD);
TestHelper.expectHeader(headers[9], 1024, char0, charC);
TestHelper.expectHeader(headers[10], 2048, char1, charB);
// We're reducing now the size by 1 byte, which should evict the last
// entry.
headers = context.decoder.decode(
TestHelper.setDynamicTableSize(4096 - 1),
);
expect(headers, hasLength(0));
headers = context.decoder.decode(TestHelper.dynamicTableLookup(0));
expect(headers, hasLength(1));
TestHelper.expectHeader(headers[0], 1024, char1, charD);
headers = context.decoder.decode(TestHelper.dynamicTableLookup(1));
expect(headers, hasLength(1));
TestHelper.expectHeader(headers[0], 1024, char0, charC);
// Since we reduce the size by 1 byte, the last entry must be gone now.
expect(
() => context.decoder.decode(TestHelper.dynamicTableLookup(2)),
throwsA(isHPackDecodingException),
);
});
});
group('encoder-tests', () {
test('simple-encoding', () {
var context = HPackContext();
var headers = [Header.ascii('key', 'value')];
expect(context.encoder.encode(headers), [
0x00,
0x03,
0x6b,
0x65,
0x79,
0x05,
0x76,
0x61,
0x6c,
0x75,
0x65,
]);
});
test('simple-encoding-long-value', () {
var context = HPackContext();
var headers = [
Header([0x42], List.filled(300, 0x84)),
];
expect(
context.decoder.decode(context.encoder.encode(headers)).first,
equalsHeader(headers.first),
);
expect(context.encoder.encode(headers), [
// Literal Header Field with Incremental Indexing - Indexed Name
0x00,
// Key: Length
0x01,
// Key: Bytes
0x42,
// Value: (first 7 bits + rest)
0x7f, 0xad, 0x01,
// Value: Bytes
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
]);
});
});
});
}
class TestHelper {
static List<int> setDynamicTableSize(int newSize) {
return TestHelper.newInteger(0x20, 5, newSize);
}
static List<int> newInteger(int currentByte, int prefixBits, int value) {
assert((currentByte & ((1 << prefixBits) - 1)) == 0);
var buffer = <int>[];
if (value < ((1 << prefixBits) - 1)) {
currentByte |= value;
buffer.add(currentByte);
} else {
// Length encodeded.
currentByte |= (1 << prefixBits) - 1;
value -= (1 << prefixBits) - 1;
buffer.add(currentByte);
var done = false;
while (!done) {
currentByte = value & 0x7f;
value = value >> 7;
done = value == 0;
if (!done) currentByte |= 0x80;
buffer.add(currentByte);
}
}
return buffer;
}
static List<int> insertIntoDynamicTable(int n, int nameChar, int valueChar) {
// NOTE: size(header) = 32 + header.name.length + header.value.length.
var buffer = <int>[];
// Literal indexed (will be put into dynamic table)
buffer.addAll([0x40]);
var name = [nameChar];
buffer.addAll(newInteger(0, 7, name.length));
buffer.addAll(name);
var value = List.filled(n - 32 - name.length, valueChar);
buffer.addAll(newInteger(0, 7, value.length));
buffer.addAll(value);
return buffer;
}
static List<int> dynamicTableLookup(int index) {
// There are 62 entries in the static table.
return newInteger(0x80, 7, 62 + index);
}
static void expectHeader(Header h, int len, int nameChar, int valueChar) {
var data = h.value;
expect(data, hasLength(len - 32 - 1));
for (var i = 0; i < data.length; i++) {
expect(data[i], valueChar);
}
}
}
/// A matcher for HuffmannDecodingExceptions.
const Matcher isHPackDecodingException = TypeMatcher<HPackDecodingException>();
class _HeaderMatcher extends Matcher {
final Header header;
_HeaderMatcher(this.header);
@override
Description describe(Description description) => description.add('Header');
@override
bool matches(Object? item, Map matchState) {
return item is Header &&
_compareLists(item.name, header.name) &&
_compareLists(item.value, header.value);
}
bool _compareLists(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}
Matcher isHeader(String name, String value) =>
_HeaderMatcher(Header.ascii(name, value));
Matcher equalsHeader(Header header) => _HeaderMatcher(header);