diff options
Diffstat (limited to 'tests/auto/network/access/qrestaccessmanager/httptestserver.cpp')
-rw-r--r-- | tests/auto/network/access/qrestaccessmanager/httptestserver.cpp | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp new file mode 100644 index 0000000000..25869eb46b --- /dev/null +++ b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp @@ -0,0 +1,268 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "httptestserver_p.h" + +#include <QtNetwork/qtcpsocket.h> + +#include <QtCore/qcoreapplication.h> + +#include <private/qlocale_p.h> + +using namespace Qt::StringLiterals; + +static constexpr char CRLF[] = "\r\n"; + +HttpTestServer::HttpTestServer(QObject *parent) : QTcpServer(parent) +{ + QObject::connect(this, &QTcpServer::newConnection, this, &HttpTestServer::handleConnected); + const auto ok = listen(QHostAddress::LocalHost); + Q_ASSERT(ok); +}; + +HttpTestServer::~HttpTestServer() +{ + if (isListening()) + close(); +} + +QUrl HttpTestServer::url() +{ + return QUrl(u"http://127.0.0.1:%1"_s.arg(serverPort())); +} + +void HttpTestServer::handleConnected() +{ + Q_ASSERT(!m_socket); // No socket must exist previously, this is a single-connection server + m_socket = nextPendingConnection(); + Q_ASSERT(m_socket); + QObject::connect(m_socket, &QTcpSocket::readyRead, + this, &HttpTestServer::handleDataAvailable); +} + +void HttpTestServer::handleDataAvailable() +{ + Q_ASSERT(m_socket); + bool ok = true; + + // Parse the incoming request data into the HttpData object + while (m_socket->bytesAvailable()) { + if (state == State::ReadingMethod && !(ok = readMethod(m_socket))) + qWarning("Invalid Method"); + if (ok && state == State::ReadingUrl && !(ok = readUrl(m_socket))) + qWarning("Invalid URL"); + if (ok && state == State::ReadingStatus && !(ok = readStatus(m_socket))) + qWarning("Invalid Status"); + if (ok && state == State::ReadingHeader && !(ok = readHeaders(m_socket))) + qWarning("Invalid Header"); + if (ok && state == State::ReadingBody && !(ok = readBody(m_socket))) + qWarning("Invalid Body"); + } // while bytes available + + Q_ASSERT(ok); + Q_ASSERT(m_handler); + Q_ASSERT(state == State::AllDone); + + if (auto values = m_request.headers.values( + QHttpHeaders::WellKnownHeader::Host); !values.empty()) { + const auto parts = values.first().split(':'); + m_request.url.setHost(parts.at(0)); + if (parts.size() == 2) + m_request.url.setPort(parts.at(1).toUInt()); + } + HttpData response; + ResponseControl control; + // Inform the testcase about request and ask for response data + m_handler(m_request, response, control); + + QByteArray responseMessage; + responseMessage += "HTTP/1.1 "; + responseMessage += QByteArray::number(response.status); + responseMessage += CRLF; + // Insert headers if any + for (const auto &[name,value] : response.headers.toListOfPairs()) { + responseMessage += name; + responseMessage += ": "; + responseMessage += value; + responseMessage += CRLF; + } + responseMessage += CRLF; + /* + qDebug() << "HTTPTestServer received request" + << "\nMethod:" << m_request.method + << "\nHeaders:" << m_request.headers + << "\nBody:" << m_request.body; + */ + if (control.respond) { + if (control.responseChunkSize <= 0) { + responseMessage += response.body; + // qDebug() << "HTTPTestServer response:" << responseMessage; + m_socket->write(responseMessage); + } else { + // Respond in chunks, first write the headers + // qDebug() << "HTTPTestServer response:" << responseMessage; + m_socket->write(responseMessage); + // Then write bodydata in chunks, while allowing the testcase to process as well + QByteArray chunk; + while (!response.body.isEmpty()) { + chunk = response.body.left(control.responseChunkSize); + response.body.remove(0, control.responseChunkSize); + // qDebug() << "SERVER writing chunk" << chunk; + m_socket->write(chunk); + m_socket->flush(); + m_socket->waitForBytesWritten(); + // Process events until testcase indicates it's ready for next chunk. + // This way we can control the bytes the testcase gets in each chunk + control.readyForNextChunk = false; + while (!control.readyForNextChunk) + QCoreApplication::processEvents(); + } + } + } + m_socket->disconnectFromHost(); + m_request = {}; + m_socket = nullptr; // deleted by QTcpServer during destruction + state = State::ReadingMethod; + fragment.clear(); +} + +bool HttpTestServer::readMethod(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + if (ascii_isspace(c)) + finished = true; + else if (std::isupper(c) && fragment.size() < 8) + fragment += c; + else + return false; + } + if (finished) { + if (fragment == "HEAD") + method = Method::Head; + else if (fragment == "GET") + method = Method::Get; + else if (fragment == "PUT") + method = Method::Put; + else if (fragment == "PATCH") + method = Method::Patch; + else if (fragment == "POST") + method = Method::Post; + else if (fragment == "DELETE") + method = Method::Delete; + else if (fragment == "FOOBAR") // used by custom verb/method tests + method = Method::Custom; + else + qWarning("Invalid operation %s", fragment.data()); + + state = State::ReadingUrl; + m_request.method = fragment; + fragment.clear(); + + return method != Method::Unknown; + } + return true; +} + +bool HttpTestServer::readUrl(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + if (std::isspace(c)) + finished = true; + else + fragment += c; + } + if (finished) { + if (!fragment.startsWith('/')) { + qWarning("Invalid URL path %s", fragment.constData()); + return false; + } + m_request.url = QStringLiteral("http://127.0.0.1:") + QString::number(m_request.port) + + QString::fromUtf8(fragment); + state = State::ReadingStatus; + if (!m_request.url.isValid()) { + qWarning("Invalid URL %s", fragment.constData()); + return false; + } + fragment.clear(); + } + return true; +} + +bool HttpTestServer::readStatus(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + fragment += socket->read(1); + if (fragment.endsWith(CRLF)) { + finished = true; + fragment.resize(fragment.size() - 2); + } + } + if (finished) { + if (!std::isdigit(fragment.at(fragment.size() - 3)) || + fragment.at(fragment.size() - 2) != '.' || + !std::isdigit(fragment.at(fragment.size() - 1))) { + qWarning("Invalid version"); + return false; + } + m_request.version = std::pair(fragment.at(fragment.size() - 3) - '0', + fragment.at(fragment.size() - 1) - '0'); + state = State::ReadingHeader; + fragment.clear(); + } + return true; +} + +bool HttpTestServer::readHeaders(QTcpSocket *socket) +{ + while (socket->bytesAvailable()) { + fragment += socket->read(1); + if (fragment.endsWith(CRLF)) { + if (fragment == CRLF) { + state = State::ReadingBody; + fragment.clear(); + return true; + } else { + fragment.chop(2); + const int index = fragment.indexOf(':'); + if (index == -1) + return false; + + QByteArray key = fragment.sliced(0, index).trimmed(); + QByteArray value = fragment.sliced(index + 1).trimmed(); + m_request.headers.append(key, value); + fragment.clear(); + } + } + } + return true; +} + +bool HttpTestServer::readBody(QTcpSocket *socket) +{ + qint64 bytesLeft = 0; + if (auto values = m_request.headers.values( + QHttpHeaders::WellKnownHeader::ContentLength); !values.empty()) { + bool conversionResult; + bytesLeft = values.first().toInt(&conversionResult); + if (!conversionResult) + return false; + fragment.resize(bytesLeft); + } + while (bytesLeft) { + qint64 got = socket->read(&fragment.data()[fragment.size() - bytesLeft], bytesLeft); + if (got < 0) + return false; // error + bytesLeft -= got; + if (bytesLeft) + qApp->processEvents(); + } + fragment.swap(m_request.body); + state = State::AllDone; + return true; +} + |