Revision control
Copy as Markdown
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QStringBuilder>
#include "private/qhttpnetworkconnection_p.h"
class tst_QHttpNetworkReply: public QObject
{
Q_OBJECT
private Q_SLOTS:
void parseHeader_data();
void parseHeader();
void parseHeaderVerification_data();
void parseHeaderVerification();
void parseEndOfHeader_data();
void parseEndOfHeader();
void headerSizeLimit_data();
void headerSizeLimit();
void statusLineSizeLimit();
};
void tst_QHttpNetworkReply::parseHeader_data()
{
QTest::addColumn<QByteArray>("headers");
QTest::addColumn<QStringList>("fields");
QTest::addColumn<QStringList>("values");
QTest::newRow("no-fields") << QByteArray("\r\n") << QStringList() << QStringList();
QTest::newRow("empty-field") << QByteArray("Set-Cookie: \r\n")
<< (QStringList() << "Set-Cookie")
<< (QStringList() << "");
QTest::newRow("single-field") << QByteArray("Content-Type: text/html; charset=utf-8\r\n")
<< (QStringList() << "Content-Type")
<< (QStringList() << "text/html; charset=utf-8");
QTest::newRow("single-field-continued") << QByteArray("Content-Type: text/html;\r\n"
" charset=utf-8\r\n")
<< (QStringList() << "Content-Type")
<< (QStringList() << "text/html; charset=utf-8");
QTest::newRow("single-field-on-five-lines")
<< QByteArray("Name:\r\n first\r\n \r\n \r\n last\r\n") << (QStringList() << "Name")
<< (QStringList() << "first last");
QTest::newRow("multi-field") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: 1024\r\n"
"Content-Encoding: gzip\r\n")
<< (QStringList() << "Content-Type" << "Content-Length" << "Content-Encoding")
<< (QStringList() << "text/html; charset=utf-8" << "1024" << "gzip");
QTest::newRow("multi-field-with-emtpy") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: 1024\r\n"
"Set-Cookie: \r\n"
"Content-Encoding: gzip\r\n")
<< (QStringList() << "Content-Type" << "Content-Length" << "Set-Cookie" << "Content-Encoding")
<< (QStringList() << "text/html; charset=utf-8" << "1024" << "" << "gzip");
QTest::newRow("lws-field") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length:\r\n 1024\r\n"
"Content-Encoding: gzip\r\n")
<< (QStringList() << "Content-Type" << "Content-Length" << "Content-Encoding")
<< (QStringList() << "text/html; charset=utf-8" << "1024" << "gzip");
QTest::newRow("duplicated-field") << QByteArray("Vary: Accept-Language\r\n"
"Vary: Cookie\r\n"
"Vary: User-Agent\r\n")
<< (QStringList() << "Vary")
<< (QStringList() << "Accept-Language, Cookie, User-Agent");
}
void tst_QHttpNetworkReply::parseHeader()
{
QFETCH(QByteArray, headers);
QFETCH(QStringList, fields);
QFETCH(QStringList, values);
QHttpNetworkReply reply;
reply.parseHeader(headers);
for (int i = 0; i < fields.size(); ++i) {
//qDebug() << "field" << fields.at(i) << "value" << reply.headerField(fields.at(i)) << "expected" << values.at(i);
QString field = reply.headerField(fields.at(i).toLatin1());
QCOMPARE(field, values.at(i));
}
}
void tst_QHttpNetworkReply::parseHeaderVerification_data()
{
QTest::addColumn<QByteArray>("headers");
QTest::addColumn<bool>("success");
QTest::newRow("no-header-fields") << QByteArray("\r\n") << true;
QTest::newRow("starting-with-space") << QByteArray(" Content-Encoding: gzip\r\n") << false;
QTest::newRow("starting-with-tab") << QByteArray("\tContent-Encoding: gzip\r\n") << false;
QTest::newRow("only-colon") << QByteArray(":\r\n") << false;
QTest::newRow("colon-and-value") << QByteArray(": only-value\r\n") << false;
QTest::newRow("name-with-space") << QByteArray("Content Length: 10\r\n") << false;
QTest::newRow("missing-colon-1") << QByteArray("Content-Encoding\r\n") << false;
QTest::newRow("missing-colon-2")
<< QByteArray("Content-Encoding\r\nContent-Length: 10\r\n") << false;
QTest::newRow("missing-colon-3")
<< QByteArray("Content-Encoding: gzip\r\nContent-Length\r\n") << false;
QTest::newRow("header-field-too-long")
<< (QByteArray("Content-Type: ")
+ QByteArray(HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE, 'a') + QByteArray("\r\n"))
<< false;
QByteArray name = "Content-Type: ";
QTest::newRow("max-header-field-size")
<< (name + QByteArray(HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE - name.size(), 'a')
+ QByteArray("\r\n"))
<< true;
QByteArray tooManyHeaders = QByteArray("Content-Type: text/html; charset=utf-8\r\n")
.repeated(HeaderConstants::DEFAULT_MAX_HEADER_FIELDS + 1);
QTest::newRow("too-many-headers") << tooManyHeaders << false;
QByteArray maxHeaders = QByteArray("Content-Type: text/html; charset=utf-8\r\n")
.repeated(HeaderConstants::DEFAULT_MAX_HEADER_FIELDS);
QTest::newRow("max-headers") << maxHeaders << true;
QByteArray firstValue(HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE / 2, 'a');
constexpr int obsFold = 1;
QTest::newRow("max-continuation-size")
<< (name + firstValue + QByteArray("\r\n ")
+ QByteArray(HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE - name.size()
- firstValue.size() - obsFold,
'b')
+ QByteArray("\r\n"))
<< true;
QTest::newRow("too-long-continuation-size")
<< (name + firstValue + QByteArray("\r\n ")
+ QByteArray(HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE - name.size()
- firstValue.size() - obsFold + 1,
'b')
+ QByteArray("\r\n"))
<< false;
auto appendLongHeaderElement = [](QByteArray &result, QByteArrayView name) {
const qsizetype size = result.size();
result += name;
result += ": ";
result.resize(size + HeaderConstants::DEFAULT_MAX_HEADER_FIELD_SIZE, 'a');
};
QByteArray longHeader;
constexpr qsizetype TrailerLength = sizeof("\r\n\r\n") - 1; // we ignore the trailing newlines
longHeader.reserve(HeaderConstants::DEFAULT_MAX_TOTAL_HEADER_SIZE + TrailerLength + 1);
appendLongHeaderElement(longHeader, "Location");
longHeader += "\r\n";
appendLongHeaderElement(longHeader, "WWW-Authenticate");
longHeader += "\r\nProxy-Authenticate: ";
longHeader.resize(HeaderConstants::DEFAULT_MAX_TOTAL_HEADER_SIZE, 'a');
longHeader += "\r\n\r\n";
// Test with headers which are just large enough to fit our DEFAULT_MAX_TOTAL_HEADER_SIZE limit:
QTest::newRow("total-header-close-to-max-size") << longHeader << true;
// Now add another character to make the total header size exceed the limit:
longHeader.insert(HeaderConstants::DEFAULT_MAX_TOTAL_HEADER_SIZE - TrailerLength, 'a');
QTest::newRow("total-header-too-large") << longHeader << false;
}
void tst_QHttpNetworkReply::parseHeaderVerification()
{
QFETCH(QByteArray, headers);
QFETCH(bool, success);
QHttpNetworkReply reply;
reply.parseHeader(headers);
if (success && QByteArrayView(headers).trimmed().size())
QVERIFY(reply.header().size() > 0);
else
QCOMPARE(reply.header().size(), 0);
}
class TestHeaderSocket : public QAbstractSocket
{
public:
explicit TestHeaderSocket(const QByteArray &input) : QAbstractSocket(QAbstractSocket::TcpSocket, nullptr)
{
inputBuffer.setData(input);
inputBuffer.open(QIODevice::ReadOnly | QIODevice::Unbuffered);
open(QIODevice::ReadOnly | QIODevice::Unbuffered);
}
qint64 readData(char *data, qint64 maxlen) override { return inputBuffer.read(data, maxlen); }
QBuffer inputBuffer;
};
class TestHeaderReply : public QHttpNetworkReply
{
public:
QHttpNetworkReplyPrivate *replyPrivate() { return static_cast<QHttpNetworkReplyPrivate *>(d_ptr.data()); }
};
void tst_QHttpNetworkReply::parseEndOfHeader_data()
{
QTest::addColumn<QByteArray>("headers");
QTest::addColumn<qint64>("lengths");
QTest::newRow("CRLFCRLF") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length:\r\n 1024\r\n"
"Content-Encoding: gzip\r\n\r\nHTTPBODY")
<< qint64(90);
QTest::newRow("CRLFLF") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length:\r\n 1024\r\n"
"Content-Encoding: gzip\r\n\nHTTPBODY")
<< qint64(89);
QTest::newRow("LFCRLF") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length:\r\n 1024\r\n"
"Content-Encoding: gzip\n\r\nHTTPBODY")
<< qint64(89);
QTest::newRow("LFLF") << QByteArray("Content-Type: text/html; charset=utf-8\r\n"
"Content-Length:\r\n 1024\r\n"
"Content-Encoding: gzip\n\nHTTPBODY")
<< qint64(88);
}
void tst_QHttpNetworkReply::parseEndOfHeader()
{
QFETCH(QByteArray, headers);
QFETCH(qint64, lengths);
TestHeaderSocket socket(headers);
TestHeaderReply reply;
QHttpNetworkReplyPrivate *replyPrivate = reply.replyPrivate();
qint64 headerBytes = replyPrivate->readHeader(&socket);
QCOMPARE(headerBytes, lengths);
}
void tst_QHttpNetworkReply::headerSizeLimit_data()
{
QTest::addColumn<QByteArray>("headers");
QTest::addColumn<bool>("accepted");
// Limit used below is 64 bytes of header content.
const QByteArray field = "X-Field: " + QByteArray(40, 'a'); // 49 bytes
QTest::newRow("empty") << QByteArray("\r\n") << true;
QTest::newRow("within-limit") << QByteArray(field + "\r\n\r\n") << true;
QTest::newRow("within-limit-LFLF") << QByteArray(field + "\n\n") << true;
QTest::newRow("at-limit") << QByteArray("X-Field: " + QByteArray(55, 'a') + "\r\n\r\n")
<< true;
QTest::newRow("over-limit-terminated")
<< QByteArray(field + "\r\n" + field + "\r\n\r\n") << false;
QTest::newRow("over-limit-unterminated") << QByteArray("X-Field: " + QByteArray(4096, 'a'))
<< false;
QTest::newRow("over-limit-unterminated-lines")
<< QByteArray(field + "\r\n").repeated(100) << false;
}
void tst_QHttpNetworkReply::headerSizeLimit()
{
QFETCH(QByteArray, headers);
QFETCH(bool, accepted);
TestHeaderSocket socket(headers);
TestHeaderReply reply;
QHttpNetworkReplyPrivate *replyPrivate = reply.replyPrivate();
replyPrivate->parser.setMaxTotalHeaderSize(64);
const qint64 headerBytes = replyPrivate->readHeader(&socket);
if (accepted) {
QCOMPARE(headerBytes, qint64(headers.size()));
QCOMPARE(replyPrivate->state, QHttpNetworkReplyPrivate::ReadingDataState);
} else {
QCOMPARE(headerBytes, qint64(-1));
QCOMPARE_NE(replyPrivate->state, QHttpNetworkReplyPrivate::ReadingDataState);
// Nothing may be retained from the rejected block.
QVERIFY(replyPrivate->fragment.isEmpty());
// Must stop reading as soon as the limit is exceeded, not consume everything.
QCOMPARE_LE(socket.inputBuffer.pos(), replyPrivate->parser.maxRawHeaderSize() + 1);
}
}
void tst_QHttpNetworkReply::statusLineSizeLimit()
{
// A status line that is never terminated must be rejected once it can
// no longer be valid, instead of being buffered indefinitely.
const QByteArray status = "HTTP/1.1 200 " + QByteArray(1024 * 1024, 'a');
TestHeaderSocket socket(status);
TestHeaderReply reply;
QHttpNetworkReplyPrivate *replyPrivate = reply.replyPrivate();
QCOMPARE(replyPrivate->readStatus(&socket), qint64(-1));
QVERIFY(replyPrivate->fragment.isEmpty());
QCOMPARE_LT(socket.inputBuffer.pos(), 4096);
// The longest acceptable reason phrase still parses.
const QByteArray okStatus = "HTTP/1.1 200 " + QByteArray(1024, 'a') + "\r\n";
TestHeaderSocket okSocket(okStatus);
TestHeaderReply okReply;
QCOMPARE(okReply.replyPrivate()->readStatus(&okSocket), qint64(okStatus.size()));
QCOMPARE(okReply.statusCode(), 200);
}
QTEST_MAIN(tst_QHttpNetworkReply)
#include "tst_qhttpnetworkreply.moc"