71f507788b
Static Huffman coding + HPACK encode/decode algorithm (for HTTP2) + auto test. Change-Id: I85d6269076cc1d586d17c87bcdee49c21522ef78 Task-number: QTBUG-50946 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
853 lines
28 KiB
C++
853 lines
28 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2016 The Qt Company Ltd.
|
|
** Copyright (C) 2014 Governikus GmbH & Co. KG.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of the test suite of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
** included in the packaging of this file. Please review the following
|
|
** information to ensure the GNU General Public License requirements will
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include <QtTest/QtTest>
|
|
|
|
#include <QtNetwork/private/bitstreams_p.h>
|
|
#include <QtNetwork/private/hpack_p.h>
|
|
|
|
#include <QtCore/qbytearray.h>
|
|
|
|
#include <cstdlib>
|
|
#include <vector>
|
|
#include <string>
|
|
|
|
QT_USE_NAMESPACE
|
|
|
|
using namespace HPack;
|
|
|
|
class tst_Hpack: public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
tst_Hpack();
|
|
private Q_SLOTS:
|
|
void bitstreamConstruction();
|
|
void bitstreamWrite();
|
|
void bitstreamReadWrite();
|
|
void bitstreamCompression();
|
|
void bitstreamErrors();
|
|
|
|
void lookupTableConstructor();
|
|
|
|
void lookupTableStatic_data();
|
|
void lookupTableStatic();
|
|
void lookupTableDynamic();
|
|
|
|
void hpackEncodeRequest_data();
|
|
void hpackEncodeRequest();
|
|
void hpackDecodeRequest_data();
|
|
void hpackDecodeRequest();
|
|
|
|
void hpackEncodeResponse_data();
|
|
void hpackEncodeResponse();
|
|
void hpackDecodeResponse_data();
|
|
void hpackDecodeResponse();
|
|
|
|
// TODO: more-more-more tests needed!
|
|
|
|
private:
|
|
void hpackEncodeRequest(bool withHuffman);
|
|
void hpackEncodeResponse(bool withHuffman);
|
|
|
|
HttpHeader header1;
|
|
std::vector<uchar> buffer1;
|
|
BitOStream request1;
|
|
|
|
HttpHeader header2;
|
|
std::vector<uchar> buffer2;
|
|
BitOStream request2;
|
|
|
|
HttpHeader header3;
|
|
std::vector<uchar> buffer3;
|
|
BitOStream request3;
|
|
};
|
|
|
|
using StreamError = BitIStream::Error;
|
|
|
|
tst_Hpack::tst_Hpack()
|
|
: request1(buffer1),
|
|
request2(buffer2),
|
|
request3(buffer3)
|
|
{
|
|
}
|
|
|
|
void tst_Hpack::bitstreamConstruction()
|
|
{
|
|
const uchar bytes[] = {0xDE, 0xAD, 0xBE, 0xEF};
|
|
const int size = int(sizeof bytes);
|
|
|
|
// Default ctors:
|
|
std::vector<uchar> buffer;
|
|
{
|
|
const BitOStream out(buffer);
|
|
QVERIFY(out.bitLength() == 0);
|
|
QVERIFY(out.byteLength() == 0);
|
|
|
|
const BitIStream in;
|
|
QVERIFY(in.bitLength() == 0);
|
|
QVERIFY(in.streamOffset() == 0);
|
|
QVERIFY(in.error() == StreamError::NoError);
|
|
}
|
|
|
|
// Create istream with some data:
|
|
{
|
|
BitIStream in(bytes, bytes + size);
|
|
QVERIFY(in.bitLength() == size * 8);
|
|
QVERIFY(in.streamOffset() == 0);
|
|
QVERIFY(in.error() == StreamError::NoError);
|
|
// 'Read' some data back:
|
|
for (int i = 0; i < size; ++i) {
|
|
uchar bitPattern = 0;
|
|
const auto bitsRead = in.peekBits(i * 8, 8, &bitPattern);
|
|
QVERIFY(bitsRead == 8);
|
|
QVERIFY(bitPattern == bytes[i]);
|
|
}
|
|
}
|
|
|
|
// Copy ctors:
|
|
{
|
|
// Ostreams - copy is disabled.
|
|
// Istreams:
|
|
const BitIStream in1;
|
|
const BitIStream in2(in1);
|
|
QVERIFY(in2.bitLength() == in1.bitLength());
|
|
QVERIFY(in2.streamOffset() == in1.streamOffset());
|
|
QVERIFY(in2.error() == StreamError::NoError);
|
|
|
|
const BitIStream in3(bytes, bytes + size);
|
|
const BitIStream in4(in3);
|
|
QVERIFY(in4.bitLength() == in3.bitLength());
|
|
QVERIFY(in4.streamOffset() == in3.streamOffset());
|
|
QVERIFY(in4.error() == StreamError::NoError);
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::bitstreamWrite()
|
|
{
|
|
// Known representations,
|
|
// https://http2.github.io/http2-spec/compression.html.
|
|
// 5.1 Integer Representation
|
|
|
|
// Test bit/byte lengths of the
|
|
// resulting data:
|
|
std::vector<uchar> buffer;
|
|
BitOStream out(buffer);
|
|
out.write(3);
|
|
// 11, fits into 8-bit prefix:
|
|
QVERIFY(out.bitLength() == 8);
|
|
QVERIFY(out.byteLength() == 1);
|
|
QVERIFY(out.begin()[0] == 3);
|
|
|
|
out.clear();
|
|
QVERIFY(out.bitLength() == 0);
|
|
QVERIFY(out.byteLength() == 0);
|
|
|
|
// This number does not fit into 8-bit
|
|
// prefix we'll need 2 bytes:
|
|
out.write(256);
|
|
QVERIFY(out.byteLength() == 2);
|
|
QVERIFY(out.bitLength() == 16);
|
|
QVERIFY(out.begin()[0] == 0xff);
|
|
QVERIFY(out.begin()[1] == 1);
|
|
|
|
out.clear();
|
|
|
|
// See 5.2 String Literal Representation.
|
|
|
|
// We use Huffman code,
|
|
// char 'a' has a prefix code 00011 (5 bits)
|
|
out.write(QByteArray("aaa", 3), true);
|
|
QVERIFY(out.byteLength() == 3);
|
|
QVERIFY(out.bitLength() == 24);
|
|
// Now we must have in our stream:
|
|
// 10000010 | 00011000| 11000111
|
|
const uchar *encoded = out.begin();
|
|
QVERIFY(encoded[0] == 0x82);
|
|
QVERIFY(encoded[1] == 0x18);
|
|
QVERIFY(encoded[2] == 0xC7);
|
|
// TODO: add more tests ...
|
|
}
|
|
|
|
void tst_Hpack::bitstreamReadWrite()
|
|
{
|
|
// We can write into the bit stream:
|
|
// 1) bit patterns
|
|
// 2) integers (see HPACK, 5.1)
|
|
// 3) string (see HPACK, 5.2)
|
|
std::vector<uchar> buffer;
|
|
BitOStream out(buffer);
|
|
out.writeBits(0xf, 3);
|
|
QVERIFY(out.byteLength() == 1);
|
|
QVERIFY(out.bitLength() == 3);
|
|
|
|
// Now, read it back:
|
|
{
|
|
BitIStream in(out.begin(), out.end());
|
|
uchar bitPattern = 0;
|
|
const auto bitsRead = in.peekBits(0, 3, &bitPattern);
|
|
// peekBits pack into the most significant byte/bit:
|
|
QVERIFY(bitsRead == 3);
|
|
QVERIFY((bitPattern >> 5) == 7);
|
|
}
|
|
|
|
const quint32 testInt = 133;
|
|
out.write(testInt);
|
|
|
|
// This integer does not fit into the current 5-bit prefix,
|
|
// so byteLength == 2.
|
|
QVERIFY(out.byteLength() == 2);
|
|
const auto bitLength = out.bitLength();
|
|
QVERIFY(bitLength > 3);
|
|
|
|
// Now, read it back:
|
|
{
|
|
BitIStream in(out.begin(), out.end());
|
|
in.skipBits(3); // Bit pattern
|
|
quint32 value = 0;
|
|
QVERIFY(in.read(&value));
|
|
QVERIFY(in.error() == StreamError::NoError);
|
|
QCOMPARE(value, testInt);
|
|
}
|
|
|
|
const QByteArray testString("ABCDE", 5);
|
|
out.write(testString, true); // Compressed
|
|
out.write(testString, false); // Non-compressed
|
|
QVERIFY(out.byteLength() > 2);
|
|
QVERIFY(out.bitLength() > bitLength);
|
|
|
|
// Now, read it back:
|
|
{
|
|
BitIStream in(out.begin(), out.end());
|
|
in.skipBits(bitLength); // Bit pattern and integer
|
|
QByteArray value;
|
|
// Read compressed string first ...
|
|
QVERIFY(in.read(&value));
|
|
QCOMPARE(value, testString);
|
|
QCOMPARE(in.error(), StreamError::NoError);
|
|
// Now non-compressed ...
|
|
QVERIFY(in.read(&value));
|
|
QCOMPARE(value, testString);
|
|
QCOMPARE(in.error(), StreamError::NoError);
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::bitstreamCompression()
|
|
{
|
|
// Similar to bitstreamReadWrite but
|
|
// writes/reads a lot of mixed strings/integers.
|
|
std::vector<std::string> strings;
|
|
std::vector<quint32> integers;
|
|
std::vector<bool> isA; // integer or string.
|
|
const std::string bytes("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()[]/*");
|
|
const unsigned nValues = 100000;
|
|
|
|
quint64 totalStringBytes = 0;
|
|
std::vector<uchar> buffer;
|
|
BitOStream out(buffer);
|
|
for (unsigned i = 0; i < nValues; ++i) {
|
|
const bool isString = std::rand() % 1000 > 500;
|
|
isA.push_back(isString);
|
|
if (!isString) {
|
|
integers.push_back(std::rand() % 1000);
|
|
out.write(integers.back());
|
|
} else {
|
|
const auto start = std::rand() % (bytes.length() / 2);
|
|
auto end = start * 2;
|
|
if (!end)
|
|
end = bytes.length() / 2;
|
|
strings.push_back(bytes.substr(start, end - start));
|
|
const auto &s = strings.back();
|
|
totalStringBytes += s.size();
|
|
QByteArray data(s.c_str(), int(s.size()));
|
|
const bool compressed(std::rand() % 1000 > 500);
|
|
out.write(data, compressed);
|
|
}
|
|
}
|
|
|
|
qDebug() << "Compressed(?) byte length:" << out.byteLength()
|
|
<< "total string bytes:" << totalStringBytes;
|
|
qDebug() << "total integer bytes (for quint32):" << integers.size() * sizeof(quint32);
|
|
|
|
QVERIFY(out.byteLength() > 0);
|
|
QVERIFY(out.bitLength() > 0);
|
|
|
|
BitIStream in(out.begin(), out.end());
|
|
|
|
for (unsigned i = 0, iS = 0, iI = 0; i < nValues; ++i) {
|
|
if (isA[i]) {
|
|
QByteArray data;
|
|
QVERIFY(in.read(&data));
|
|
QCOMPARE(in.error(), StreamError::NoError);
|
|
QCOMPARE(data.toStdString(), strings[iS]);
|
|
++iS;
|
|
} else {
|
|
quint32 value = 0;
|
|
QVERIFY(in.read(&value));
|
|
QCOMPARE(in.error(), StreamError::NoError);
|
|
QCOMPARE(value, integers[iI]);
|
|
++iI;
|
|
}
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::bitstreamErrors()
|
|
{
|
|
{
|
|
BitIStream in;
|
|
quint32 val = 0;
|
|
QVERIFY(!in.read(&val));
|
|
QCOMPARE(in.error(), StreamError::NotEnoughData);
|
|
}
|
|
{
|
|
// Integer in a stream, that does not fit into quint32.
|
|
const uchar bytes[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
|
BitIStream in(bytes, bytes + sizeof bytes);
|
|
quint32 val = 0;
|
|
QVERIFY(!in.read(&val));
|
|
QCOMPARE(in.error(), StreamError::InvalidInteger);
|
|
}
|
|
{
|
|
const uchar byte = 0x82; // 1 - Huffman compressed, 2 - the (fake) byte length.
|
|
BitIStream in(&byte, &byte + 1);
|
|
QByteArray val;
|
|
QVERIFY(!in.read(&val));
|
|
QCOMPARE(in.error(), StreamError::NotEnoughData);
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::lookupTableConstructor()
|
|
{
|
|
{
|
|
FieldLookupTable nonIndexed(4096, false);
|
|
QVERIFY(nonIndexed.dynamicDataSize() == 0);
|
|
QVERIFY(nonIndexed.numberOfDynamicEntries() == 0);
|
|
QVERIFY(nonIndexed.numberOfStaticEntries() != 0);
|
|
QVERIFY(nonIndexed.numberOfStaticEntries() == nonIndexed.numberOfEntries());
|
|
// Now we add some fake field and verify what 'non-indexed' means ... no search
|
|
// by name.
|
|
QVERIFY(nonIndexed.prependField("custom-key", "custom-value"));
|
|
// 54: 10 + 12 in name/value pair above + 32 required by HPACK specs ...
|
|
QVERIFY(nonIndexed.dynamicDataSize() == 54);
|
|
QVERIFY(nonIndexed.numberOfDynamicEntries() == 1);
|
|
QCOMPARE(nonIndexed.numberOfEntries(), nonIndexed.numberOfStaticEntries() + 1);
|
|
// Should fail to find it (invalid index 0) - search is disabled.
|
|
QVERIFY(nonIndexed.indexOf("custom-key", "custom-value") == 0);
|
|
}
|
|
{
|
|
// "key" + "value" == 8 bytes, + 32 (HPACK's requirement) == 40.
|
|
// Let's ask for a max-size 32 so that entry does not fit:
|
|
FieldLookupTable nonIndexed(32, false);
|
|
QVERIFY(nonIndexed.prependField("key", "value"));
|
|
QVERIFY(nonIndexed.numberOfEntries() == nonIndexed.numberOfStaticEntries());
|
|
QVERIFY(nonIndexed.indexOf("key", "value") == 0);
|
|
}
|
|
{
|
|
FieldLookupTable indexed(4096, true);
|
|
QVERIFY(indexed.dynamicDataSize() == 0);
|
|
QVERIFY(indexed.numberOfDynamicEntries() == 0);
|
|
QVERIFY(indexed.numberOfStaticEntries() != 0);
|
|
QVERIFY(indexed.numberOfStaticEntries() == indexed.numberOfEntries());
|
|
QVERIFY(indexed.prependField("custom-key", "custom-value"));
|
|
QVERIFY(indexed.dynamicDataSize() == 54);
|
|
QVERIFY(indexed.numberOfDynamicEntries() == 1);
|
|
QVERIFY(indexed.numberOfEntries() == indexed.numberOfStaticEntries() + 1);
|
|
QVERIFY(indexed.indexOf("custom-key") == indexed.numberOfStaticEntries() + 1);
|
|
QVERIFY(indexed.indexOf("custom-key", "custom-value") == indexed.numberOfStaticEntries() + 1);
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::lookupTableStatic_data()
|
|
{
|
|
QTest::addColumn<QByteArray>("expectedName");
|
|
QTest::addColumn<QByteArray>("expectedValue");
|
|
|
|
// Some predefined fields to find
|
|
// (they are always defined/required by HPACK).
|
|
QTest::newRow(":authority|") << QByteArray(":authority") << QByteArray("");
|
|
QTest::newRow(":method|GET") << QByteArray(":method") << QByteArray("GET");
|
|
QTest::newRow(":method|POST") << QByteArray(":method") << QByteArray("POST");
|
|
QTest::newRow(":path|/") << QByteArray(":path") << QByteArray("/");
|
|
QTest::newRow(":path|/index.html") << QByteArray(":path") << QByteArray("/index.html");
|
|
QTest::newRow(":scheme|http") << QByteArray(":scheme") << QByteArray("http");
|
|
QTest::newRow(":scheme|https") << QByteArray(":scheme") << QByteArray("https");
|
|
QTest::newRow(":status|200") << QByteArray(":status") << QByteArray("200");
|
|
QTest::newRow(":status|204") << QByteArray(":status") << QByteArray("204");
|
|
QTest::newRow(":status|206") << QByteArray(":status") << QByteArray("206");
|
|
QTest::newRow(":status|304") << QByteArray(":status") << QByteArray("304");
|
|
QTest::newRow(":status|400") << QByteArray(":status") << QByteArray("400");
|
|
QTest::newRow(":status|404") << QByteArray(":status") << QByteArray("404");
|
|
QTest::newRow(":status|500") << QByteArray(":status") << QByteArray("500");
|
|
}
|
|
|
|
void tst_Hpack::lookupTableStatic()
|
|
{
|
|
const FieldLookupTable table(0, false /*all static, no need in 'search index'*/);
|
|
|
|
QFETCH(QByteArray, expectedName);
|
|
QFETCH(QByteArray, expectedValue);
|
|
|
|
const quint32 index = table.indexOf(expectedName, expectedValue);
|
|
QVERIFY(index != 0);
|
|
|
|
QByteArray name, value;
|
|
QVERIFY(table.field(index, &name, &value));
|
|
QCOMPARE(name, expectedName);
|
|
QCOMPARE(value, expectedValue);
|
|
}
|
|
|
|
void tst_Hpack::lookupTableDynamic()
|
|
{
|
|
// HPACK's table size:
|
|
// for every field -> size += field.name.length() + field.value.length() + 32.
|
|
// Let's set some size limit and try to fill table with enough entries to have several
|
|
// items evicted.
|
|
const quint32 tableSize = 8192;
|
|
const char stringData[] = "abcdefghijklmnopABCDEFGHIJKLMNOP0123456789()[]:";
|
|
const quint32 dataSize = sizeof stringData - 1;
|
|
|
|
FieldLookupTable table(tableSize, true);
|
|
|
|
std::vector<QByteArray> fieldsToFind;
|
|
quint32 evicted = 0;
|
|
|
|
while (true) {
|
|
// Strings are repeating way too often, I want to
|
|
// have at least some items really evicted and not found,
|
|
// therefore these weird dances with start/len.
|
|
const quint32 start = std::rand() % (dataSize - 10);
|
|
quint32 len = std::rand() % (dataSize - start);
|
|
if (!len)
|
|
len = 1;
|
|
|
|
const QByteArray val(stringData + start, len);
|
|
fieldsToFind.push_back(val);
|
|
const quint32 entriesBefore = table.numberOfDynamicEntries();
|
|
QVERIFY(table.prependField(val, val));
|
|
QVERIFY(table.indexOf(val));
|
|
QVERIFY(table.indexOf(val) == table.indexOf(val, val));
|
|
QByteArray fieldName, fieldValue;
|
|
table.field(table.indexOf(val), &fieldName, &fieldValue);
|
|
|
|
QVERIFY(val == fieldName);
|
|
QVERIFY(val == fieldValue);
|
|
|
|
if (table.numberOfDynamicEntries() <= entriesBefore) {
|
|
// We had to evict several items ...
|
|
evicted += entriesBefore - table.numberOfDynamicEntries() + 1;
|
|
if (evicted >= 200)
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVERIFY(table.dynamicDataSize() <= tableSize);
|
|
QVERIFY(table.numberOfDynamicEntries() > 0);
|
|
QVERIFY(table.indexOf(fieldsToFind.back())); // We MUST have it in a table!
|
|
|
|
using size_type = std::vector<QByteArray>::size_type;
|
|
for (size_type i = 0, e = fieldsToFind.size(); i < e; ++i) {
|
|
const auto &val = fieldsToFind[i];
|
|
const quint32 index = table.indexOf(val);
|
|
if (!index) {
|
|
QVERIFY(i < size_type(evicted));
|
|
} else {
|
|
QVERIFY(index == table.indexOf(val, val));
|
|
QByteArray fieldName, fieldValue;
|
|
QVERIFY(table.field(index, &fieldName, &fieldValue));
|
|
QVERIFY(val == fieldName);
|
|
QVERIFY(val == fieldValue);
|
|
}
|
|
}
|
|
|
|
table.clearDynamicTable();
|
|
|
|
QVERIFY(table.numberOfDynamicEntries() == 0);
|
|
QVERIFY(table.dynamicDataSize() == 0);
|
|
QVERIFY(table.indexOf(fieldsToFind.back()) == 0);
|
|
|
|
QVERIFY(table.prependField("name1", "value1"));
|
|
QVERIFY(table.prependField("name2", "value2"));
|
|
|
|
QVERIFY(table.indexOf("name1") == table.numberOfStaticEntries() + 2);
|
|
QVERIFY(table.indexOf("name2", "value2") == table.numberOfStaticEntries() + 1);
|
|
QVERIFY(table.indexOf("name1", "value2") == 0);
|
|
QVERIFY(table.indexOf("name2", "value1") == 0);
|
|
QVERIFY(table.indexOf("name3") == 0);
|
|
|
|
QVERIFY(!table.indexIsValid(table.numberOfEntries() + 1));
|
|
|
|
QVERIFY(table.prependField("name1", "value1"));
|
|
QVERIFY(table.numberOfDynamicEntries() == 3);
|
|
table.evictEntry();
|
|
QVERIFY(table.indexOf("name1") != 0);
|
|
table.evictEntry();
|
|
QVERIFY(table.indexOf("name2") == 0);
|
|
QVERIFY(table.indexOf("name1") != 0);
|
|
table.evictEntry();
|
|
QVERIFY(table.dynamicDataSize() == 0);
|
|
QVERIFY(table.numberOfDynamicEntries() == 0);
|
|
QVERIFY(table.indexOf("name1") == 0);
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeRequest_data()
|
|
{
|
|
QTest::addColumn<bool>("compression");
|
|
QTest::newRow("no-string-compression") << false;
|
|
QTest::newRow("with-string-compression") << true;
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeRequest(bool withHuffman)
|
|
{
|
|
// This function uses examples from HPACK specs
|
|
// (see appendix).
|
|
|
|
Encoder encoder(4096, withHuffman);
|
|
// HPACK, C.3.1 First Request
|
|
/*
|
|
:method: GET
|
|
:scheme: http
|
|
:path: /
|
|
:authority: www.example.com
|
|
|
|
Hex dump of encoded data (without Huffman):
|
|
|
|
8286 8441 0f77 7777 2e65 7861 6d70 6c65 | ...A.www.example
|
|
2e63 6f6d
|
|
|
|
Hex dump of encoded data (with Huffman):
|
|
|
|
8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff
|
|
*/
|
|
request1.clear();
|
|
header1 = {{":method", "GET"},
|
|
{":scheme", "http"},
|
|
{":path", "/"},
|
|
{":authority", "www.example.com"}};
|
|
QVERIFY(encoder.encodeRequest(request1, header1));
|
|
QVERIFY(encoder.dynamicTableSize() == 57);
|
|
|
|
// HPACK, C.3.2 Second Request
|
|
/*
|
|
Header list to encode:
|
|
|
|
:method: GET
|
|
:scheme: http
|
|
:path: /
|
|
:authority: www.example.com
|
|
cache-control: no-cache
|
|
|
|
Hex dump of encoded data (without Huffman):
|
|
|
|
8286 84be 5808 6e6f 2d63 6163 6865
|
|
|
|
Hex dump of encoded data (with Huffman):
|
|
|
|
8286 84be 5886 a8eb 1064 9cbf
|
|
*/
|
|
|
|
request2.clear();
|
|
header2 = {{":method", "GET"},
|
|
{":scheme", "http"},
|
|
{":path", "/"},
|
|
{":authority", "www.example.com"},
|
|
{"cache-control", "no-cache"}};
|
|
encoder.encodeRequest(request2, header2);
|
|
QVERIFY(encoder.dynamicTableSize() == 110);
|
|
|
|
// HPACK, C.3.3 Third Request
|
|
/*
|
|
Header list to encode:
|
|
|
|
:method: GET
|
|
:scheme: https
|
|
:path: /index.html
|
|
:authority: www.example.com
|
|
custom-key: custom-value
|
|
|
|
Hex dump of encoded data (without Huffman):
|
|
|
|
8287 85bf 400a 6375 7374 6f6d 2d6b 6579
|
|
0c63 7573 746f 6d2d 7661 6c75 65
|
|
|
|
Hex dump of encoded data (with Huffman):
|
|
|
|
8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
|
|
a849 e95b b8e8 b4bf
|
|
*/
|
|
request3.clear();
|
|
header3 = {{":method", "GET"},
|
|
{":scheme", "https"},
|
|
{":path", "/index.html"},
|
|
{":authority", "www.example.com"},
|
|
{"custom-key", "custom-value"}};
|
|
encoder.encodeRequest(request3, header3);
|
|
QVERIFY(encoder.dynamicTableSize() == 164);
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeRequest()
|
|
{
|
|
QFETCH(bool, compression);
|
|
|
|
hpackEncodeRequest(compression);
|
|
|
|
// See comments above about these hex dumps ...
|
|
const uchar bytes1NH[] = {0x82, 0x86, 0x84, 0x41,
|
|
0x0f, 0x77, 0x77, 0x77,
|
|
0x2e, 0x65, 0x78, 0x61,
|
|
0x6d, 0x70, 0x6c, 0x65,
|
|
0x2e, 0x63, 0x6f, 0x6d};
|
|
|
|
const uchar bytes1WH[] = {0x82, 0x86, 0x84, 0x41,
|
|
0x8c, 0xf1, 0xe3, 0xc2,
|
|
0xe5, 0xf2, 0x3a, 0x6b,
|
|
0xa0, 0xab, 0x90, 0xf4,
|
|
0xff};
|
|
|
|
const uchar *hexDump1 = compression ? bytes1WH : bytes1NH;
|
|
const quint64 byteLength1 = compression ? sizeof bytes1WH : sizeof bytes1NH;
|
|
|
|
QCOMPARE(request1.byteLength(), byteLength1);
|
|
QCOMPARE(request1.bitLength(), byteLength1 * 8);
|
|
|
|
for (quint32 i = 0, e = request1.byteLength(); i < e; ++i)
|
|
QCOMPARE(hexDump1[i], request1.begin()[i]);
|
|
|
|
const uchar bytes2NH[] = {0x82, 0x86, 0x84, 0xbe,
|
|
0x58, 0x08, 0x6e, 0x6f,
|
|
0x2d, 0x63, 0x61, 0x63,
|
|
0x68, 0x65};
|
|
|
|
const uchar bytes2WH[] = {0x82, 0x86, 0x84, 0xbe,
|
|
0x58, 0x86, 0xa8, 0xeb,
|
|
0x10, 0x64, 0x9c, 0xbf};
|
|
|
|
const uchar *hexDump2 = compression ? bytes2WH : bytes2NH;
|
|
const auto byteLength2 = compression ? sizeof bytes2WH : sizeof bytes2NH;
|
|
QVERIFY(request2.byteLength() == byteLength2);
|
|
QVERIFY(request2.bitLength() == byteLength2 * 8);
|
|
for (quint32 i = 0, e = request2.byteLength(); i < e; ++i)
|
|
QCOMPARE(hexDump2[i], request2.begin()[i]);
|
|
|
|
const uchar bytes3NH[] = {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};
|
|
const uchar bytes3WH[] = {0x82, 0x87, 0x85, 0xbf,
|
|
0x40, 0x88, 0x25, 0xa8,
|
|
0x49, 0xe9, 0x5b, 0xa9,
|
|
0x7d, 0x7f, 0x89, 0x25,
|
|
0xa8, 0x49, 0xe9, 0x5b,
|
|
0xb8, 0xe8, 0xb4, 0xbf};
|
|
|
|
const uchar *hexDump3 = compression ? bytes3WH : bytes3NH;
|
|
const quint64 byteLength3 = compression ? sizeof bytes3WH : sizeof bytes3NH;
|
|
QCOMPARE(request3.byteLength(), byteLength3);
|
|
QCOMPARE(request3.bitLength(), byteLength3 * 8);
|
|
for (quint32 i = 0, e = request3.byteLength(); i < e; ++i)
|
|
QCOMPARE(hexDump3[i], request3.begin()[i]);
|
|
}
|
|
|
|
void tst_Hpack::hpackDecodeRequest_data()
|
|
{
|
|
QTest::addColumn<bool>("compression");
|
|
QTest::newRow("no-string-compression") << false;
|
|
QTest::newRow("with-string-compression") << true;
|
|
}
|
|
|
|
void tst_Hpack::hpackDecodeRequest()
|
|
{
|
|
QFETCH(bool, compression);
|
|
hpackEncodeRequest(compression);
|
|
|
|
QVERIFY(request1.byteLength());
|
|
QVERIFY(request2.byteLength());
|
|
QVERIFY(request3.byteLength());
|
|
|
|
Decoder decoder(4096);
|
|
BitIStream inputStream1(request1.begin(), request1.end());
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream1));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(57));
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header1);
|
|
}
|
|
|
|
BitIStream inputStream2{request2.begin(), request2.end()};
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream2));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(110));
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header2);
|
|
}
|
|
|
|
BitIStream inputStream3(request3.begin(), request3.end());
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream3));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(164));
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header3);
|
|
}
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeResponse_data()
|
|
{
|
|
hpackEncodeRequest_data();
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeResponse()
|
|
{
|
|
QFETCH(bool, compression);
|
|
|
|
hpackEncodeResponse(compression);
|
|
|
|
// TODO: we can also test bytes - using hex dumps from HPACK's specs,
|
|
// for now only test a table behavior/expected sizes.
|
|
}
|
|
|
|
void tst_Hpack::hpackEncodeResponse(bool withCompression)
|
|
{
|
|
Encoder encoder(256, withCompression); // 256 - this will result in entries evicted.
|
|
|
|
// HPACK, C.5.1 First Response
|
|
/*
|
|
Header list to encode:
|
|
|
|
:status: 302
|
|
cache-control: private
|
|
date: Mon, 21 Oct 2013 20:13:21 GMT
|
|
location: https://www.example.com
|
|
*/
|
|
request1.clear();
|
|
header1 = {{":status", "302"},
|
|
{"cache-control", "private"},
|
|
{"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
|
|
{"location", "https://www.example.com"}};
|
|
|
|
QVERIFY(encoder.encodeResponse(request1, header1));
|
|
QCOMPARE(encoder.dynamicTableSize(), quint32(222));
|
|
|
|
// HPACK, C.5.2 Second Response
|
|
/*
|
|
|
|
|
|
The (":status", "302") header field is evicted from the dynamic
|
|
table to free space to allow adding the (":status", "307") header field.
|
|
|
|
Header list to encode:
|
|
|
|
:status: 307
|
|
cache-control: private
|
|
date: Mon, 21 Oct 2013 20:13:21 GMT
|
|
location: https://www.example.com
|
|
*/
|
|
request2.clear();
|
|
header2 = {{":status", "307"},
|
|
{"cache-control", "private"},
|
|
{"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
|
|
{"location", "https://www.example.com"}};
|
|
QVERIFY(encoder.encodeResponse(request2, header2));
|
|
QCOMPARE(encoder.dynamicTableSize(), quint32(222));
|
|
|
|
// HPACK, C.5.3 Third Response
|
|
/*
|
|
Several header fields are evicted from the dynamic table
|
|
during the processing of this header list.
|
|
|
|
Header list to encode:
|
|
|
|
:status: 200
|
|
cache-control: private
|
|
date: Mon, 21 Oct 2013 20:13:22 GMT
|
|
location: https://www.example.com
|
|
content-encoding: gzip
|
|
set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1
|
|
*/
|
|
request3.clear();
|
|
header3 = {{":status", "200"},
|
|
{"cache-control", "private"},
|
|
{"date", "Mon, 21 Oct 2013 20:13:22 GMT"},
|
|
{"location", "https://www.example.com"},
|
|
{"content-encoding", "gzip"},
|
|
{"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}};
|
|
QVERIFY(encoder.encodeResponse(request3, header3));
|
|
QCOMPARE(encoder.dynamicTableSize(), quint32(215));
|
|
}
|
|
|
|
void tst_Hpack::hpackDecodeResponse_data()
|
|
{
|
|
hpackEncodeRequest_data();
|
|
}
|
|
|
|
void tst_Hpack::hpackDecodeResponse()
|
|
{
|
|
QFETCH(bool, compression);
|
|
|
|
hpackEncodeResponse(compression);
|
|
|
|
QVERIFY(request1.byteLength());
|
|
Decoder decoder(256); // This size will result in entries evicted.
|
|
BitIStream inputStream1(request1.begin(), request1.end());
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream1));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(222));
|
|
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header1);
|
|
}
|
|
|
|
QVERIFY(request2.byteLength());
|
|
BitIStream inputStream2(request2.begin(), request2.end());
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream2));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(222));
|
|
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header2);
|
|
}
|
|
|
|
QVERIFY(request3.byteLength());
|
|
BitIStream inputStream3(request3.begin(), request3.end());
|
|
QVERIFY(decoder.decodeHeaderFields(inputStream3));
|
|
QCOMPARE(decoder.dynamicTableSize(), quint32(215));
|
|
|
|
{
|
|
const auto &decoded = decoder.decodedHeader();
|
|
QVERIFY(decoded == header3);
|
|
}
|
|
}
|
|
|
|
QTEST_MAIN(tst_Hpack)
|
|
|
|
#include "tst_hpack.moc"
|