summaryrefslogtreecommitdiffstats
path: root/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/network/access/qrestaccessmanager/httptestserver.cpp')
-rw-r--r--tests/auto/network/access/qrestaccessmanager/httptestserver.cpp268
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;
+}
+