Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qhttpmessagestreamparser_p.h"
#include <QtCore/QtGlobal>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
/*!
* \class QHttpMessageStreamParser
* \brief Decodes a stream of headers and payloads encoded according to rfc2616 (HTTP/1.1)
*
* It complains about invalid sequences, but is quite permissive in accepting them
*/
QHttpMessageStreamParser::QHttpMessageStreamParser(
std::function<void(const QByteArray &, const QByteArray &)> headerHandler,
std::function<void(const QByteArray &body)> bodyHandler,
std::function<void(QtMsgType error, const QString &msg)> errorHandler, Mode mode)
: m_headerHandler(std::move(headerHandler)),
m_bodyHandler(std::move(bodyHandler)),
m_errorHandler(std::move(errorHandler)),
m_mode(mode)
{
}
bool QHttpMessageStreamParser::receiveEof()
{
if (m_state != State::PreHeader) {
errorMessage(QtWarningMsg, u"Partial message at end of file"_s);
return false;
}
return true;
}
void QHttpMessageStreamParser::receiveData(QByteArrayView data)
{
const char lf = '\n';
const char cr = '\r';
const char colon = ':';
const char space = ' ';
const char tab = '\t';
qsizetype dataPos = 0;
bool didAdvance = false;
auto advance = [&]() {
data = data.mid(dataPos);
dataPos = 0;
didAdvance = true;
};
while (dataPos < data.size()) {
switch (m_state) {
case State::PreHeader:
switch (data.at(dataPos)) {
case lf:
errorMessage(QtWarningMsg,
QStringLiteral("Unexpected newline without preceding carriage "
"return at start of headers"));
m_state = State::AfterCrLf;
++dataPos;
continue;
case cr:
m_state = State::AfterCr;
++dataPos;
continue;
case tab:
case space:
errorMessage(QtWarningMsg, u"Unexpected space at start of headers, skipping"_s);
while (++dataPos < data.size()) {
char c = data.at(dataPos);
if (c != space && c != tab) {
advance();
m_state = State::InHeaderField;
break;
}
}
break;
default:
m_state = State::InHeaderField;
break;
}
Q_ASSERT(m_currentHeaderField.isEmpty() && m_currentHeaderValue.isEmpty());
break;
case State::InHeaderField: {
didAdvance = false;
while (!didAdvance) {
char c = data.at(dataPos);
switch (c) {
case lf:
m_currentHeaderField.append(data.mid(0, dataPos));
errorMessage(
QtWarningMsg,
u"Unexpected carriage return without newline in unterminated header %1"_s
.arg(QString::fromUtf8(m_currentHeaderField)));
m_state = State::AfterCrLf;
advance();
++dataPos;
break;
case cr:
m_state = State::AfterCr;
m_currentHeaderField.append(data.mid(0, dataPos));
errorMessage(QtWarningMsg,
u"Newline before colon in header %1"_s.arg(
QString::fromUtf8(m_currentHeaderField)));
advance();
++dataPos;
break;
case colon:
m_currentHeaderField.append(data.mid(0, dataPos));
m_state = State::HeaderValueSpace;
++dataPos;
advance();
break;
case space:
case tab:
errorMessage(QtWarningMsg, u"Space in header field name"_s);
Q_FALLTHROUGH();
default:
if (++dataPos == data.size()) {
m_currentHeaderField.append(data);
return;
}
break;
}
}
} break;
case State::HeaderValueSpace:
while (dataPos < data.size()) {
char c = data.at(dataPos);
if (c != space && c != tab) {
advance();
m_state = State::InHeaderValue;
m_currentHeaderValue.clear();
break;
}
++dataPos;
}
break;
case State::InHeaderValue: {
didAdvance = false;
while (!didAdvance) {
char c = data.at(dataPos);
switch (c) {
case lf:
m_currentHeaderValue.append(data.mid(0, dataPos));
errorMessage(QtWarningMsg,
QStringLiteral("Unexpected newline without preceding "
"carriage return in header %1")
.arg(QString::fromUtf8(m_currentHeaderField)));
m_state = State::AfterCrLf;
advance();
++dataPos;
break;
case cr:
m_currentHeaderValue.append(data.mid(0, dataPos));
m_state = State::AfterCr;
advance();
++dataPos;
break;
default:
if (++dataPos == data.size()) {
m_currentHeaderValue.append(data);
return;
}
break;
}
}
} break;
case State::AfterCr: {
char c = data.at(dataPos);
switch (c) {
case lf:
m_state = State::AfterCrLf;
++dataPos;
break;
case cr:
errorMessage(QtWarningMsg,
QStringLiteral("Double carriage return encountred, interpreting it as "
"header end after header %1")
.arg(QString::fromUtf8(m_currentHeaderField)));
m_currentPacket.clear();
m_currentPacketSize = 0;
++dataPos;
advance();
m_state = State::InBody;
callHasHeader();
break;
case space:
case tab:
errorMessage(
QtWarningMsg,
u"Unexpected carriage return without following newline in header %1"_s.arg(
QString::fromUtf8(m_currentHeaderField)));
m_state = State::InHeaderValue;
// m_currentHeaderValue.append(data.mid(0,dataPos)) to preserve the (non
// significant) newlines in header value
advance();
break;
default:
errorMessage(
QtWarningMsg,
u"Unexpected carriage return without following newline in header %1"_s.arg(
QString::fromUtf8(m_currentHeaderField)));
m_state = State::InHeaderField;
advance();
callHasHeader();
break;
}
} break;
case State::AfterCrLf: {
char c = data.at(dataPos);
switch (c) {
case lf:
errorMessage(QtWarningMsg,
u"Newline without carriage return in header %1"_s.arg(
QString::fromUtf8(m_currentHeaderField)));
// avoid seeing it as end of headers?
m_state = State::AfterCrLfCr;
break;
case cr:
m_state = State::AfterCrLfCr;
++dataPos;
break;
case space:
case tab:
m_state = State::InHeaderValue;
// m_currentHeaderValue.append(data.mid(0,dataPos)) to preserve the (non
// significant) newlines in header value
advance();
break;
default:
m_state = State::InHeaderField;
advance();
callHasHeader();
break;
}
} break;
case State::AfterCrLfCr: {
char c = data.at(dataPos);
switch (c) {
case lf:
m_currentPacket.clear();
m_currentPacketSize = 0;
++dataPos;
advance();
m_state = State::InBody;
callHasHeader();
break;
default:
errorMessage(
QtWarningMsg,
u"crlfcr without final lf encountred, ignoring it (non clear terminator)"_s);
m_state = State::InHeaderField;
advance();
callHasHeader();
break;
}
} break;
case State::InBody: {
if (m_contentSize == -1) {
errorMessage(QtWarningMsg, u"missing valid Content-Length header"_s);
m_state = State::PreHeader;
continue;
}
qint64 missing = m_contentSize - m_currentPacketSize;
if (missing > 0) {
dataPos = qMin(qsizetype(missing), data.size());
m_currentPacketSize += dataPos;
if (m_mode == BUFFERED)
m_currentPacket.append(data.mid(0, dataPos));
advance();
}
if (m_currentPacketSize >= m_contentSize) {
m_state = State::PreHeader;
callHasBody();
}
} break;
}
}
if (m_state == State::InBody && (m_contentSize == -1 || m_contentSize == 0)) {
// nothing to read, but emit empty body...
m_state = State::PreHeader;
if (m_contentSize == -1)
errorMessage(QtWarningMsg, u"missing valid Content-Length header"_s);
callHasBody();
}
}
void QHttpMessageStreamParser::callHasHeader()
{
static const QByteArray s_contentLengthFieldName = "Content-Length";
if (m_currentHeaderField.isEmpty() && m_currentHeaderValue.isEmpty())
return;
QByteArray field = m_currentHeaderField;
QByteArray value = m_currentHeaderValue;
m_currentHeaderField.clear();
m_currentHeaderValue.clear();
if (s_contentLengthFieldName.compare(field, Qt::CaseInsensitive) == 0) {
bool ok = false;
const int size = value.toInt(&ok);
QString errorMsg;
if (ok) {
if (size > m_maxContentLength) {
// Value exceeds the maximum allowed size
errorMsg = u"Content-Length %1 exceeds maximum allowed size %2"_s.arg(
QString::fromUtf8(value), QString::number(m_maxContentLength));
} else if (size < 0) {
// Negative value is not allowed
errorMsg = u"Negative Content-Length: %1"_s.arg(QString::fromUtf8(value));
} else {
m_contentSize = size;
}
} else {
// Invalid Content-Length value, not in integer range
errorMsg = u"Invalid Content-Length: %1"_s.arg(QString::fromUtf8(value));
}
if (!errorMsg.isEmpty()) {
errorMessage(QtWarningMsg, errorMsg);
m_contentSize = -1;
}
}
if (m_headerHandler)
m_headerHandler(field, value);
}
void QHttpMessageStreamParser::callHasBody()
{
// uses an empty QByteArray in callback for dry run
if (m_mode == UNBUFFERED) {
if (m_bodyHandler)
m_bodyHandler(QByteArray());
return;
}
QByteArray body = m_currentPacket;
m_currentPacket.clear();
m_currentPacketSize = 0;
m_contentSize = -1;
if (m_bodyHandler)
m_bodyHandler(body);
}
void QHttpMessageStreamParser::errorMessage(QtMsgType error, const QString &msg)
{
if (m_errorHandler)
m_errorHandler(error, msg);
}
/*!
* \internal
* \brief Sets the maximum allowed Content-Length.
*
* Messages with Content-Length exceeding this value will be rejected with an error
* \param maxSize Maximum allowed size in bytes.
*/
void QHttpMessageStreamParser::setMaxContentLength(int maxSize)
{
if (maxSize < 0)
errorMessage(QtWarningMsg, u"Negative maximum content length, clamping to zero"_s);
m_maxContentLength = std::clamp(maxSize, 0, std::numeric_limits<int>::max());
}
QT_END_NAMESPACE