From cf7864369f0ce471ff3c1cbb1fdf979708365df6 Mon Sep 17 00:00:00 2001 From: Hanabishi <13597663+HanabishiRecca@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:12:09 +0000 Subject: [PATCH] Improve parsing of HTTP headers Parse HTTP headers using raw byte arrays instead of strings. This allows us to apply different encodings for different parts. This change is backward compatible and should not affect any existing operation, so WebAPI version bump is not required. --- src/base/http/requestparser.cpp | 56 +++++++++++++++++++-------------- src/base/http/requestparser.h | 2 +- src/base/utils/bytearray.cpp | 1 - src/base/utils/bytearray.h | 17 +++++++++- test/testutilsbytearray.cpp | 28 +++++++++++++++++ 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/base/http/requestparser.cpp b/src/base/http/requestparser.cpp index e8a02dd09..be6aba319 100644 --- a/src/base/http/requestparser.cpp +++ b/src/base/http/requestparser.cpp @@ -31,12 +31,13 @@ #include "requestparser.h" #include +#include #include +#include #include #include #include -#include #include #include @@ -59,21 +60,19 @@ namespace return in; } - bool parseHeaderLine(const QStringView line, HeaderMap &out) + std::optional parseHeaderLine(const QByteArrayView line) { // [rfc7230] 3.2. Header Fields const int i = line.indexOf(u':'); if (i <= 0) { qWarning() << Q_FUNC_INFO << "invalid http header:" << line; - return false; + return std::nullopt; } - const QString name = line.first(i).trimmed().toString().toLower(); - const QString value = line.sliced(i + 1).trimmed().toString(); - out[name] = value; - - return true; + const QString name = QString::fromLatin1(line.first(i).trimmed()).toLower(); + const QString value = QString::fromLatin1(line.sliced(i + 1).trimmed()); + return {{name, value}}; } } @@ -93,7 +92,7 @@ RequestParser::ParseResult RequestParser::doParse(const QByteArrayView data) return {ParseStatus::Incomplete, Request(), 0}; } - const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd); + const QByteArrayView httpHeaders = data.first(headerEnd); if (!parseStartLines(httpHeaders)) { qWarning() << Q_FUNC_INFO << "header parsing error"; @@ -152,36 +151,40 @@ RequestParser::ParseResult RequestParser::doParse(const QByteArrayView data) return {ParseStatus::BadMethod, m_request, 0}; } -bool RequestParser::parseStartLines(const QStringView data) +bool RequestParser::parseStartLines(const QByteArrayView data) { // we don't handle malformed request which uses `LF` for newline - const QList lines = data.split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts); + const QList lines = splitToViews(data, CRLF, Qt::SkipEmptyParts); // [rfc7230] 3.2.2. Field Order - QStringList requestLines; + QByteArrayList requestLines; for (const auto &line : lines) { - if (line.at(0).isSpace() && !requestLines.isEmpty()) + if (QChar::fromLatin1(line.at(0)).isSpace() && !requestLines.isEmpty()) { // continuation of previous line requestLines.last() += line; } else { - requestLines += line.toString(); + requestLines += line.toByteArray(); } } if (requestLines.isEmpty()) return false; - if (!parseRequestLine(requestLines[0])) + if (!parseRequestLine(QString::fromLatin1(requestLines[0]))) return false; for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i) { - if (!parseHeaderLine(*i, m_request.headers)) + const std::optional header = parseHeaderLine(*i); + if (!header.has_value()) return false; + + const auto [name, value] = header.value(); + m_request.headers[name] = value; } return true; @@ -310,17 +313,23 @@ bool RequestParser::parseFormData(const QByteArrayView data) return false; } - const QString headers = QString::fromLatin1(data.first(eohPos)); + const QByteArrayView headers = data.first(eohPos); const QByteArrayView payload = viewWithoutEndingWith(data.sliced((eohPos + EOH.size())), CRLF); HeaderMap headersMap; - const QList headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts); + const QList headerLines = splitToViews(headers, CRLF, Qt::SkipEmptyParts); for (const auto &line : headerLines) { - if (line.trimmed().startsWith(HEADER_CONTENT_DISPOSITION, Qt::CaseInsensitive)) + const std::optional header = parseHeaderLine(line); + if (!header.has_value()) + return false; + + const auto [name, value] = header.value(); + + if (name == HEADER_CONTENT_DISPOSITION) { // extract out filename & name - const QList directives = line.split(u';', Qt::SkipEmptyParts); + const QList directives = splitToViews(line, ";", Qt::SkipEmptyParts); for (const auto &directive : directives) { @@ -328,15 +337,14 @@ bool RequestParser::parseFormData(const QByteArrayView data) if (idx < 0) continue; - const QString name = directive.first(idx).trimmed().toString().toLower(); - const QString value = Utils::String::unquote(directive.sliced(idx + 1).trimmed()).toString(); + const QString name = QString::fromLatin1(directive.first(idx).trimmed()).toLower(); + const QString value = QString::fromLatin1(unquote(directive.sliced(idx + 1).trimmed())); headersMap[name] = value; } } else { - if (!parseHeaderLine(line, headersMap)) - return false; + headersMap[name] = value; } } diff --git a/src/base/http/requestparser.h b/src/base/http/requestparser.h index 13b19f3ba..337583701 100644 --- a/src/base/http/requestparser.h +++ b/src/base/http/requestparser.h @@ -61,7 +61,7 @@ namespace Http RequestParser() = default; ParseResult doParse(QByteArrayView data); - bool parseStartLines(QStringView data); + bool parseStartLines(QByteArrayView data); bool parseRequestLine(const QString &line); bool parsePostMessage(QByteArrayView data); diff --git a/src/base/utils/bytearray.cpp b/src/base/utils/bytearray.cpp index d372f507f..61c581008 100644 --- a/src/base/utils/bytearray.cpp +++ b/src/base/utils/bytearray.cpp @@ -31,7 +31,6 @@ #include #include -#include #include QList Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep, const Qt::SplitBehavior behavior) diff --git a/src/base/utils/bytearray.h b/src/base/utils/bytearray.h index 03f0f06d0..602953ae2 100644 --- a/src/base/utils/bytearray.h +++ b/src/base/utils/bytearray.h @@ -31,9 +31,9 @@ #include #include +#include class QByteArray; -class QByteArrayView; namespace Utils::ByteArray { @@ -42,4 +42,19 @@ namespace Utils::ByteArray QByteArray asQByteArray(QByteArrayView view); QByteArray toBase32(const QByteArray &in); + + template + T unquote(const T &arr, const QByteArrayView quotes = "\"") + { + if (arr.length() < 2) + return arr; + + for (const char quote : quotes) + { + if (arr.startsWith(quote) && arr.endsWith(quote)) + return arr.sliced(1, (arr.length() - 2)); + } + + return arr; + } } diff --git a/test/testutilsbytearray.cpp b/test/testutilsbytearray.cpp index f00d43c09..99b26ad84 100644 --- a/test/testutilsbytearray.cpp +++ b/test/testutilsbytearray.cpp @@ -28,6 +28,7 @@ */ #include +#include #include #include #include @@ -123,6 +124,33 @@ private slots: QCOMPARE(Utils::ByteArray::toBase32("0000000000"), "GAYDAMBQGAYDAMBQ"); QCOMPARE(Utils::ByteArray::toBase32("1"), "GE======"); } + + void testUnquote() const + { + const auto test = []() + { + QCOMPARE(Utils::ByteArray::unquote({}), {}); + QCOMPARE(Utils::ByteArray::unquote("abc"), "abc"); + QCOMPARE(Utils::ByteArray::unquote("\"abc\""), "abc"); + QCOMPARE(Utils::ByteArray::unquote("\"a b c\""), "a b c"); + QCOMPARE(Utils::ByteArray::unquote("\"abc"), "\"abc"); + QCOMPARE(Utils::ByteArray::unquote("abc\""), "abc\""); + QCOMPARE(Utils::ByteArray::unquote(" \"abc\" "), " \"abc\" "); + QCOMPARE(Utils::ByteArray::unquote("\"a\"bc\""), "a\"bc"); + QCOMPARE(Utils::ByteArray::unquote("'abc'", "'"), "abc"); + QCOMPARE(Utils::ByteArray::unquote("'abc'", "\"'"), "abc"); + QCOMPARE(Utils::ByteArray::unquote("\"'abc'\"", "\"'"), "'abc'"); + QCOMPARE(Utils::ByteArray::unquote("\"'abc'\"", "'\""), "'abc'"); + QCOMPARE(Utils::ByteArray::unquote("\"'abc'\"", "'"), "\"'abc'\""); + QCOMPARE(Utils::ByteArray::unquote("\"abc'", "'"), "\"abc'"); + QCOMPARE(Utils::ByteArray::unquote("'abc\"", "'"), "'abc\""); + QCOMPARE(Utils::ByteArray::unquote("\"\""), ""); + QCOMPARE(Utils::ByteArray::unquote("\""), "\""); + }; + + test.template operator()(); + test.template operator()(); + } }; QTEST_APPLESS_MAIN(TestUtilsByteArray)