Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:network-protocol
#include "qhttpserverrequest_p.h"
#include "qhttpserverparser_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtNetwork/qhttpheaders.h>
#if QT_CONFIG(http)
#include <QtNetwork/private/qhttp2connection_p.h>
#endif
#include <limits>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcHttpServerParser, "qt.httpserver.parser")
using namespace Qt::StringLiterals;
namespace {
QHttpServerRequest::Method parseRequestMethod(QByteArrayView str)
{
if (str == "GET")
return QHttpServerRequest::Method::Get;
else if (str == "PUT")
return QHttpServerRequest::Method::Put;
else if (str == "DELETE")
return QHttpServerRequest::Method::Delete;
else if (str == "POST")
return QHttpServerRequest::Method::Post;
else if (str == "HEAD")
return QHttpServerRequest::Method::Head;
else if (str == "OPTIONS")
return QHttpServerRequest::Method::Options;
else if (str == "PATCH")
return QHttpServerRequest::Method::Patch;
else if (str == "CONNECT")
return QHttpServerRequest::Method::Connect;
else if (str == "TRACE")
return QHttpServerRequest::Method::Trace;
else
return QHttpServerRequest::Method::Unknown;
}
const char* getErrorString(QHttpServerResponder::StatusCode code)
{
switch (code) {
case QHttpServerResponder::StatusCode::PayloadTooLarge:
return "Request Entity Too Large";
case QHttpServerResponder::StatusCode::UriTooLong:
return "Request-URI Too Long";
case QHttpServerResponder::StatusCode::RequestHeaderFieldsTooLarge:
return "Request Header Fields Too Large";
default:
return "Bad Request";
};
}
} // anonymous namespace
/*!
\internal
*/
bool QHttpServerParser::parseRequestLine(QByteArrayView line)
{
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
auto i = line.indexOf(' ');
if (i == -1)
return false;
const auto requestMethod = line.first(i);
i++;
while (i < line.size() && line[i] == ' ')
i++;
auto j = line.indexOf(' ', i);
if (j == -1)
return false;
const auto requestUrl = line.sliced(i, j - i);
i = j + 1;
while (i < line.size() && line[i] == ' ')
i++;
if (i >= line.size())
return false;
j = line.indexOf(' ', i);
const auto protocol = j == -1 ? line.sliced(i) : line.sliced(i, j - i);
if (protocol.size() != 8 || !protocol.startsWith("HTTP"))
return false;
headerParser.setMajorVersion(protocol[5] - '0');
headerParser.setMinorVersion(protocol[7] - '0');
majorVersion = protocol[5] - '0';
minorVersion = protocol[7] - '0';
method = parseRequestMethod(requestMethod);
url = QUrl::fromEncoded(requestUrl.toByteArray());
return true;
}
/*!
\internal
*/
qsizetype QHttpServerParser::readRequestLine(QIODevice *socket)
{
if (fragment.isEmpty()) {
// reserve bytes for the request line. This is better than always append() which reallocs
// the byte array
fragment.reserve(32);
}
qsizetype bytes = 0;
char c;
qsizetype haveRead = 0;
do {
haveRead = socket->read(&c, 1);
if (haveRead == -1)
return -1; // unexpected EOF
else if (haveRead == 0)
break; // read more later
else if (haveRead == 1 && fragment.size() == 0
&& (c == '\v' || c == '\n' || c == '\r' || c == ' ' || c == '\t'))
continue; // Ignore all whitespace that was trailing from a previous request on that
// socket
bytes++;
// allow both CRLF & LF (only) line endings
if (c == '\n') {
// remove the CR at the end
if (fragment.endsWith('\r')) {
fragment.truncate(fragment.size() - 1);
}
bool ok = parseRequestLine(fragment);
state = State::ReadingHeader;
fragment.clear();
if (!ok) {
return -1;
}
break;
} else {
fragment.append(c);
if (!isUrlSizeAllowed(fragment.first(bytes))) {
fragment.clear();
sendError(socket, QHttpServerResponder::StatusCode::UriTooLong);
qCDebug(lcHttpServerParser) << "URI too long at" << bytes << "bytes from client"
<< getClientIpAddressAndPort();
return -1;
}
}
} while (haveRead == 1);
return bytes;
}
bool QHttpServerParser::isUrlSizeAllowed(QByteArrayView requestLine)
{
if (filter->isUrlSizeAllowed(requestLine.size()))
return true; // Succeed if entire request line is not too long
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
constexpr qsizetype shortestCaseExtraCharacters = 3 + 1 + 1 + 8 + 2;
if (!filter->isUrlSizeAllowed(requestLine.size() - shortestCaseExtraCharacters))
return false;
qsizetype firstSpace = requestLine.indexOf(' ');
if (firstSpace < 0)
return false; // First space not read yet
qsizetype uriStart = firstSpace;
while (uriStart < requestLine.size() && requestLine[uriStart] == ' ')
uriStart++;
qsizetype secondSpace = requestLine.indexOf(' ', uriStart);
if (secondSpace < 0)
secondSpace = requestLine.size(); // Second space not read yet
return filter->isUrlSizeAllowed(secondSpace - uriStart);
}
void QHttpServerParser::sendError(QIODevice *socket, QHttpServerResponder::StatusCode error)
{
QByteArray buffer;
buffer.append("HTTP/1.1 ");
buffer.append(QByteArray::number(int(error)));
buffer.append(" ");
buffer.append(getErrorString(error));
buffer.append("\r\nConnection: close\r\n\r\n");
socket->write(buffer);
}
/*!
\internal
*/
qint64 QHttpServerParser::contentLength() const
{
const QByteArray value = headerParser.firstHeaderField("content-length");
if (value.isEmpty())
return 0; // absent -> same as Content-Length: 0 (no body)
bool ok = false;
const qulonglong length = value.toULongLong(&ok);
if (!ok || length > qulonglong(QByteArray::maxSize()))
return -1; // invalid
return qint64(length);
}
/*!
\internal
*/
qsizetype QHttpServerParser::readHeader(QIODevice *socket)
{
if (fragment.isEmpty()) {
// according to
// the average size of the header block is 381 bytes. reserve bytes. This is better than
// always append() which reallocs the byte array.
fragment.reserve(512);
}
qint64 bytes = 0;
char c = 0;
bool allHeaders = false;
qint64 haveRead = 0;
do {
haveRead = socket->read(&c, 1);
if (haveRead == 0) {
// read more later
break;
} else if (haveRead == -1) {
// connection broke down
return -1;
} else {
fragment.append(c);
bytes++;
if (!filter->isTotalHeaderSizeAllowed(bytes)) {
sendError(socket, QHttpServerResponder::StatusCode::RequestHeaderFieldsTooLarge);
qCDebug(lcHttpServerParser) << "Total header size too large, currently at" << bytes
<< "bytes from client" << getClientIpAddressAndPort();
return -1;
}
if (c == '\n') {
// check for possible header endings. As per HTTP rfc,
// the header endings will be marked by CRLFCRLF. But
// we will allow CRLFCRLF, CRLFLF, LFCRLF, LFLF
if (fragment.endsWith("\n\r\n") || fragment.endsWith("\n\n"))
allHeaders = true;
// there is another case: We have no headers. Then the fragment equals just the line
// ending
if ((fragment.size() == 2 && fragment.endsWith("\r\n"))
|| (fragment.size() == 1 && fragment.endsWith("\n")))
allHeaders = true;
}
}
} while (!allHeaders && haveRead > 0);
// we received all headers now parse them
if (allHeaders) {
bool parseSuccess = headerParser.parseHeaders(fragment);
if (!parseSuccess) {
sendError(socket, QHttpServerResponder::StatusCode::RequestHeaderFieldsTooLarge);
qCDebug(lcHttpServerParser)
<< "Error parsing headers from client" << getClientIpAddressAndPort();
return -1;
}
headers = headerParser.headers();
fragment.clear(); // next fragment
// A Content-Length with more than one value (whether from repeated header
// fields or a single field carrying a comma-separated list) has ambiguous
// framing. RFC 9110 8.6 / RFC 9112 6.3.5 require rejecting differing values
// and permit rejecting identical duplicates. combinedValue() joins repeated
// fields with commas, so a comma in the result means more than one value.
if (headers.combinedValue("content-length").contains(',')) {
sendError(socket, QHttpServerResponder::StatusCode::BadRequest);
qCDebug(lcHttpServerParser) << "Multiple Content-Length values from client"
<< getClientIpAddressAndPort();
return -1;
}
if (!filter->isNumberOfHeaderFieldsAllowed(headers.size())) {
sendError(socket, QHttpServerResponder::StatusCode::RequestHeaderFieldsTooLarge);
qCDebug(lcHttpServerParser) << "Number of header fields" << headers.size()
<< "too large from client" << getClientIpAddressAndPort();
return -1;
}
for (qsizetype i = 0; i < headers.size(); ++i) {
qsizetype headerFieldSize = headers.nameAt(i).size() + 1 + headers.valueAt(i).size();
if (!filter->isHeaderFieldSizeAllowed(headerFieldSize)) {
sendError(socket, QHttpServerResponder::StatusCode::RequestHeaderFieldsTooLarge);
QByteArrayView name(headers.nameAt(i).data(), qMin(50, headers.nameAt(i).size()));
qCDebug(lcHttpServerParser)
<< "Header field" << name << "too large at" << headerFieldSize
<< "bytes from client" << getClientIpAddressAndPort();
return -1;
}
}
auto hostUrl = QString::fromUtf8(headerField("host"));
if (!hostUrl.isEmpty())
url.setAuthority(hostUrl);
if (url.scheme().isEmpty()) {
#if QT_CONFIG(ssl)
auto sslSocket = qobject_cast<QSslSocket *>(socket);
url.setScheme(sslSocket && sslSocket->isEncrypted() ? u"https"_s : u"http"_s);
#else
url.setScheme(u"http"_s);
#endif
}
if (url.host().isEmpty())
url.setHost(u"127.0.0.1"_s);
if (url.port() == -1)
url.setPort(port);
bodyLength = contentLength();
if (bodyLength < 0) {
sendError(socket, QHttpServerResponder::StatusCode::BadRequest);
qCDebug(lcHttpServerParser) << "Invalid Content-Length from client"
<< getClientIpAddressAndPort();
return -1;
}
if (bodyLength > 0 && !filter->isBodySizeAllowed(bodyLength)) {
sendError(socket, QHttpServerResponder::StatusCode::PayloadTooLarge);
qCDebug(lcHttpServerParser) << "Body size too large at" << bodyLength
<< "bytes from client" << getClientIpAddressAndPort();
return -1;
}
// Parse Transfer-Encoding as a list of transfer-codings (RFC 9112 6.1)
// rather than a substring match. Qt only implements the "chunked" coding,
// and it must be present exactly once as the sole coding. Every other case
// is rejected so the parser cannot disagree with an intermediary on how the
// message body is framed.
chunkedTransferEncoding = false;
const QByteArray transferEncoding = headerField("transfer-encoding");
if (!transferEncoding.isEmpty()) {
QList<QByteArray> codings;
const QList<QByteArray> rawCodings = transferEncoding.split(',');
for (QByteArray coding : rawCodings) {
if (const qsizetype paramStart = coding.indexOf(';'); paramStart != -1)
coding.truncate(paramStart);
coding = coding.trimmed().toLower();
if (!coding.isEmpty())
codings.append(coding);
}
if (codings.size() == 1 && codings.constFirst() == "chunked") {
chunkedTransferEncoding = true;
} else if (!codings.isEmpty()) {
// Either an unsupported coding, or "chunked" combined with another
// coding / not applied exactly once (it must be the sole coding for
// Qt to decode it). Reject rather than guess the framing.
const bool hasChunked = codings.contains("chunked");
sendError(socket, hasChunked
? QHttpServerResponder::StatusCode::BadRequest
: QHttpServerResponder::StatusCode::NotImplemented);
qCDebug(lcHttpServerParser)
<< "Unsupported or malformed Transfer-Encoding from client"
<< getClientIpAddressAndPort();
return -1;
}
}
QByteArray connectionHeaderField = headerField("connection");
upgrade = connectionHeaderField.toLower().contains("upgrade");
// RFC 9112 6.1: a message with both Transfer-Encoding and Content-Length has
// ambiguous framing that a front-end may resolve differently (request
// smuggling). An origin server may reject it rather than strip the
// Content-Length, so reject.
if (!headerField("transfer-encoding").isEmpty()
&& !headers.values("content-length").isEmpty()) {
sendError(socket, QHttpServerResponder::StatusCode::BadRequest);
qCDebug(lcHttpServerParser)
<< "Both Transfer-Encoding and Content-Length from client"
<< getClientIpAddressAndPort();
return -1;
}
if (chunkedTransferEncoding || bodyLength > 0) {
if (headerField("expect").compare("100-continue", Qt::CaseInsensitive) == 0)
state = State::ExpectContinue;
else
state = State::ReadingData;
} else {
state = State::AllDone;
}
}
return bytes;
}
/*!
\internal
*/
qsizetype QHttpServerParser::sendContinue(QIODevice *socket)
{
qsizetype ret = socket->write("HTTP/1.1 100 Continue\r\n\r\n");
state = State::ReadingData;
return ret;
}
/*!
\internal
*/
QHttpServerParser::QHttpServerParser(const QHostAddress &remoteAddress, quint16 remotePort,
const QHostAddress &localAddress, quint16 localPort,
QHttpServerRequestFilter *filter)
: remoteAddress(remoteAddress),
remotePort(remotePort),
localAddress(localAddress),
localPort(localPort),
filter(filter)
{
clear();
headerParser.setMaxHeaderFieldSize(std::numeric_limits<qsizetype>::max());
headerParser.setMaxTotalHeaderSize(std::numeric_limits<qsizetype>::max());
headerParser.setMaxHeaderFields(std::numeric_limits<qsizetype>::max());
}
/*!
\internal
*/
bool QHttpServerParser::parse(QIODevice *socket)
{
qsizetype read;
do {
switch (state) {
case State::AllDone:
clear();
[[fallthrough]];
case State::NothingDone:
state = State::ReadingRequestLine;
[[fallthrough]];
case State::ReadingRequestLine:
read = readRequestLine(socket);
continue;
case State::ReadingHeader:
read = readHeader(socket);
continue;
case State::ExpectContinue:
read = sendContinue(socket);
continue;
case State::ReadingData:
if (chunkedTransferEncoding)
read = readRequestBodyChunked(socket);
else
read = readBodyFast(socket);
if (state == State::AllDone) {
body = bodyBuffer.readAll();
bodyBuffer.clear();
}
continue;
}
Q_UNREACHABLE(); // fixes GCC -Wmaybe-uninitialized warning on `read`
} while (state != State::AllDone && read > 0);
return read != -1;
}
#if QT_CONFIG(http)
bool QHttpServerParser::parse(QHttp2Stream *socket)
{
clear();
majorVersion = 2;
minorVersion = 0;
for (const auto &pair : socket->receivedHeaders()) {
if (pair.name == ":method") {
method = parseRequestMethod(pair.value);
} else if (pair.name == ":scheme") {
url.setScheme(QLatin1StringView(pair.value));
} else if (pair.name == ":authority") {
url.setAuthority(QLatin1StringView(pair.value));
} else if (pair.name == ":path") {
auto path = QUrl::fromEncoded(pair.value);
url.setPath(path.path());
url.setQuery(path.query());
} else {
headerParser.appendHeaderField(pair.name, pair.value);
}
}
headers = headerParser.headers();
if (url.scheme().isEmpty())
url.setScheme(u"https"_s);
if (url.host().isEmpty())
url.setHost(u"127.0.0.1"_s);
if (url.port() == -1)
url.setPort(port);
bodyLength = contentLength(); // cache the length
body = socket->downloadBuffer().readAll();
return true;
}
#endif
/*!
\internal
*/
void QHttpServerParser::clear()
{
headerParser.clear();
bodyLength = 0;
contentRead = 0;
chunkedTransferEncoding = false;
lastChunkRead = false;
currentChunkRead = 0;
currentChunkSize = 0;
upgrade = false;
majorVersion = 0;
minorVersion = 0;
fragment.clear();
bodyBuffer.clear();
url.clear();
method = QHttpServerRequest::Method::Unknown;
headers.clear();
body.clear();
}
// The body reading functions were mostly copied from QHttpNetworkReplyPrivate
/*!
\internal
*/
// note this function can only be used for non-chunked, non-compressed with
// known content length
qsizetype QHttpServerParser::readBodyFast(QIODevice *socket)
{
qsizetype toBeRead = qMin(socket->bytesAvailable(), bodyLength - contentRead);
if (!toBeRead)
return 0;
QByteArray bd;
bd.resize(toBeRead);
qsizetype haveRead = socket->read(bd.data(), toBeRead);
if (haveRead == -1) {
bd.clear();
return 0; // ### error checking here;
}
bd.resize(haveRead);
bodyBuffer.append(bd);
contentRead += haveRead;
if (contentRead == bodyLength)
state = State::AllDone;
return haveRead;
}
/*!
\internal
*/
qsizetype QHttpServerParser::readRequestBodyRaw(QIODevice *socket, qsizetype size)
{
// FIXME get rid of this function and just use readBodyFast and give it socket->bytesAvailable()
qsizetype bytes = 0;
Q_ASSERT(socket);
int toBeRead = qMin<qsizetype>(128 * 1024, qMin<qint64>(size, socket->bytesAvailable()));
while (toBeRead > 0) {
QByteArray byteData;
byteData.resize(toBeRead);
qsizetype haveRead = socket->read(byteData.data(), byteData.size());
if (haveRead <= 0) {
// ### error checking here
byteData.clear();
return bytes;
}
byteData.resize(haveRead);
bodyBuffer.append(byteData);
bytes += haveRead;
size -= haveRead;
toBeRead = qMin<qsizetype>(128 * 1024, qMin<qsizetype>(size, socket->bytesAvailable()));
}
return bytes;
}
/*!
\internal
*/
qsizetype QHttpServerParser::readRequestBodyChunked(QIODevice *socket)
{
qsizetype bytes = 0;
while (socket->bytesAvailable()) {
if (!lastChunkRead && currentChunkRead >= currentChunkSize) {
// For the first chunk and when we're done with a chunk
currentChunkSize = 0;
currentChunkRead = 0;
if (bytes) {
// After a chunk
char crlf[2];
// read the "\r\n" after the chunk
qsizetype haveRead = socket->read(crlf, 2);
// FIXME: This code is slightly broken and not optimal. What if the 2 bytes are not
// available yet?! For nice reasons (the toLong in getChunkSize accepting \n at the
// beginning it right now still works, but we should definitely fix this.
if (haveRead != 2)
return bytes;
bytes += haveRead;
}
// Note that chunk size gets stored in currentChunkSize, what is returned is the bytes
// read
const qsizetype chunkSizeBytes = getChunkSize(socket, ¤tChunkSize);
if (chunkSizeBytes < 0)
return -1;
bytes += chunkSizeBytes;
if (currentChunkSize == -1)
break;
qsizetype projectedBodySize = 0;
if (qAddOverflow(qsizetype(bodyBuffer.byteAmount()), currentChunkSize,
&projectedBodySize)
|| !filter->isBodySizeAllowed(projectedBodySize)) {
sendError(socket, QHttpServerResponder::StatusCode::PayloadTooLarge);
socket->skip(socket->bytesAvailable());
qCDebug(lcHttpServerParser) << "Body size too large; running total"
<< bodyBuffer.byteAmount() << "+ chunk"
<< currentChunkSize << "from client"
<< getClientIpAddressAndPort();
return -1;
}
}
// if the chunk size is 0, end of the stream
if (currentChunkSize == 0 || lastChunkRead) {
lastChunkRead = true;
// try to read the "\r\n" after the chunk
char crlf[2];
qsizetype haveRead = socket->read(crlf, 2);
if (haveRead > 0)
bytes += haveRead;
if ((haveRead == 2 && crlf[0] == '\r' && crlf[1] == '\n')
|| (haveRead == 1 && crlf[0] == '\n')) {
state = State::AllDone;
} else if (haveRead == 1 && crlf[0] == '\r') {
break; // Still waiting for the last \n
} else if (haveRead > 0) {
// If we read something else then CRLF, we need to close the channel.
// FIXME forceConnectionCloseEnabled = true;
state = State::AllDone;
}
break;
}
// otherwise, try to begin reading this chunk / to read what is missing for this chunk
qsizetype haveRead = readRequestBodyRaw(socket, currentChunkSize - currentChunkRead);
currentChunkRead += haveRead;
bytes += haveRead;
// ### error checking here
}
return bytes;
}
/*!
\internal
*/
qsizetype QHttpServerParser::getChunkSize(QIODevice *socket, qsizetype *chunkSize)
{
qsizetype bytes = 0;
char crlf[2];
*chunkSize = -1;
int bytesAvailable = socket->bytesAvailable();
// FIXME rewrite to permanent loop without bytesAvailable
while (bytesAvailable > bytes) {
qsizetype sniffedBytes = socket->peek(crlf, 2);
int fragmentSize = fragment.size();
// check the next two bytes for a "\r\n", skip blank lines
if ((fragmentSize && sniffedBytes == 2 && crlf[0] == '\r' && crlf[1] == '\n')
|| (fragmentSize > 1 && fragment.endsWith('\r') && crlf[0] == '\n')) {
bytes += socket->read(crlf, 1); // read the \r or \n
if (crlf[0] == '\r')
bytes += socket->read(crlf, 1); // read the \n
// ignore the chunk-extension
QByteArrayView sizeField = fragment;
if (const qsizetype pos = sizeField.indexOf(';'); pos != -1)
sizeField = sizeField.first(pos);
bool ok = false;
const qulonglong parsed = sizeField.trimmed().toULongLong(&ok, 16);
fragment.clear();
if (!ok || parsed > qulonglong(QByteArray::maxSize())) {
sendError(socket, QHttpServerResponder::StatusCode::BadRequest);
socket->skip(socket->bytesAvailable());
qCDebug(lcHttpServerParser) << "Invalid chunk size from client"
<< getClientIpAddressAndPort();
return -1; // hard failure -> caller closes the connection
}
*chunkSize = qsizetype(parsed);
break; // size done
} else {
// read the fragment to the buffer
char c = 0;
qsizetype haveRead = socket->read(&c, 1);
if (haveRead < 0)
return -1;
bytes += haveRead;
fragment.append(c);
if (!filter->isHeaderFieldSizeAllowed(fragment.size())) {
sendError(socket, QHttpServerResponder::StatusCode::BadRequest);
socket->skip(socket->bytesAvailable());
qCDebug(lcHttpServerParser) << "Chunk size line too long at" << fragment.size()
<< "bytes from client" << getClientIpAddressAndPort();
fragment.clear();
return -1;
}
}
}
return bytes;
}
/*!
\internal
*/
QString QHttpServerParser::getClientIpAddressAndPort() const
{
using namespace Qt::StringLiterals;
QString result;
if (remoteAddress.protocol() != QAbstractSocket::IPv4Protocol)
result.append(u"["_s);
result.append(remoteAddress.toString());
if (remoteAddress.protocol() != QAbstractSocket::IPv4Protocol)
result.append(u"]"_s);
result.append(u":"_s);
result.append(QString::number(remotePort));
return result;
}
QT_END_NAMESPACE