diff options
Diffstat (limited to 'tests/auto/httpserver')
22 files changed, 896 insertions, 0 deletions
diff --git a/tests/auto/httpserver/CMakeLists.txt b/tests/auto/httpserver/CMakeLists.txt new file mode 100644 index 000000000..0a1f881b9 --- /dev/null +++ b/tests/auto/httpserver/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.18) +project(minimal LANGUAGES CXX) + +find_package(Qt6 COMPONENTS Core) +find_package(Qt6 COMPONENTS Network) + +include(httpserver.cmake) diff --git a/tests/auto/httpserver/data/hedgehog.html b/tests/auto/httpserver/data/hedgehog.html new file mode 100644 index 000000000..d8abbcd48 --- /dev/null +++ b/tests/auto/httpserver/data/hedgehog.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <head> + <title>BREAKING NEWS: 15 Hedgehogs With Things That Look Like Hedgehogs</title> + </head> + <body> + <img src="hedgehog.png"/> + </body> +</html> diff --git a/tests/auto/httpserver/data/hedgehog.png b/tests/auto/httpserver/data/hedgehog.png Binary files differnew file mode 100644 index 000000000..4d56d8633 --- /dev/null +++ b/tests/auto/httpserver/data/hedgehog.png diff --git a/tests/auto/httpserver/data/loadprogress/downloadable.tar.gz b/tests/auto/httpserver/data/loadprogress/downloadable.tar.gz Binary files differnew file mode 100644 index 000000000..741cb8ca6 --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/downloadable.tar.gz diff --git a/tests/auto/httpserver/data/loadprogress/main.html b/tests/auto/httpserver/data/loadprogress/main.html new file mode 100644 index 000000000..3b7d2034b --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/main.html @@ -0,0 +1,30 @@ +<html> +<head><title>Load Progress Test Page</title> + <style> + .monospace { font-family: "Lucida Console", Courier, monospace; } + </style> + <title>page1</title> + <script> + function addP(t) { + var p = document.createElement('p') + p.class = 'monospace' + p.innerHTML = t + var d = document.createElement('div') + d.appendChild(p) + document.body.appendChild(d) + } + window.addEventListener('DOMContentLoaded', (event) => { addP('DOMContentLoaded') }) + </script> +</head> +<body> + <h1>Hello.</h1> + <script> + addP('sometext') + </script> + <p class="monospace">body in monospace</p> + <iframe id="page1" src="page1.html"></iframe> + <iframe id="page2" src="page2.html"></iframe> + <iframe id="page3" src="page3.html"></iframe> + <iframe id="page4" src="page4.html"></iframe> +</body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page1.html b/tests/auto/httpserver/data/loadprogress/page1.html new file mode 100644 index 000000000..9b11ce887 --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page1.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>page1</title> + </head> + <body> + <div><a href="page2.html#anchor">page2</a></div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page2.html b/tests/auto/httpserver/data/loadprogress/page2.html new file mode 100644 index 000000000..223817c8c --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page2.html @@ -0,0 +1,15 @@ +<html> + <head> + <title>page2</title> + </head> + <style> + .fardown { + position: absolute; + top: 2500px; + } + </style> + <body> + <div><a href="#anchor">page2</a></div> + <div class="fardown" id="anchor">page2 anchor</div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page3.html b/tests/auto/httpserver/data/loadprogress/page3.html new file mode 100644 index 000000000..d38ca31f0 --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page3.html @@ -0,0 +1,20 @@ +<html> + <head> + <title>page3</title> + </head> + <script> + setTimeout(function(){ + document.getElementById('anchorLink').click(); + },500); + </script> + <style> + .fardown { + position: absolute; + top: 2500px; + } + </style> + <body> + <div><a id="anchorLink" href="#anchor">page3</a></div> + <div class="fardown" id="anchor">page3 anchor</div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page4.html b/tests/auto/httpserver/data/loadprogress/page4.html new file mode 100644 index 000000000..61976b4fb --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page4.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>page4</title> + </head> + <body onload="document.getElementById('downloadLink').focus();"> + <a id="downloadLink" href="downloadable.tar.gz">download</a> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page5.html b/tests/auto/httpserver/data/loadprogress/page5.html new file mode 100644 index 000000000..47709ff08 --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page5.html @@ -0,0 +1,20 @@ +<html> + <head> + <title>page5</title> + </head> + <script> + addEventListener('DOMContentLoaded', (event) => { + document.getElementById('anchorLink').click(); + }); + </script> + <style> + .fardown { + position: absolute; + top: 2500px; + } + </style> + <body> + <div><a id="anchorLink" href="#anchor">go to the anchor</a></div> + <div class="fardown" id="anchor">here is the anchor</div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page6.html b/tests/auto/httpserver/data/loadprogress/page6.html new file mode 100644 index 000000000..98042701a --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page6.html @@ -0,0 +1,13 @@ +<html> + <head> + <title>page6</title> + </head> + <script> + addEventListener('DOMContentLoaded', (event) => { + document.getElementById('anchorLink').click(); + }); + </script> + <body> + <div><a id="anchorLink" href="page2.html#anchor">go to another page</a></div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page7.html b/tests/auto/httpserver/data/loadprogress/page7.html new file mode 100644 index 000000000..42538c5de --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page7.html @@ -0,0 +1,13 @@ +<html> + <head> + <title>page6</title> + </head> + <script> + setTimeout(function(){ + document.getElementById('anchorLink').click(); + },500); + </script> + <body> + <div><a id="anchorLink" href="page2.html#anchor">go to another page</a></div> + </body> +</html> diff --git a/tests/auto/httpserver/data/loadprogress/page8.html b/tests/auto/httpserver/data/loadprogress/page8.html new file mode 100644 index 000000000..8ebdddf97 --- /dev/null +++ b/tests/auto/httpserver/data/loadprogress/page8.html @@ -0,0 +1,20 @@ +<html> + <head> + <title>Page with js navigation in the end of document to anchor within the page</title> + </head> + <style> + .fardown { + position: absolute; + top: 2500px; + } + </style> + <body> + <div><a id="anchorLink" href="#anchor">go to the anchor</a></div> + <div class="fardown" id="anchor">here is the anchor</div> + <script> + addEventListener('load', (event) => { + window.location.replace(document.getElementById('anchorLink').href) + }) + </script> + </body> +</html> diff --git a/tests/auto/httpserver/data/notification.html b/tests/auto/httpserver/data/notification.html new file mode 100644 index 000000000..1d1e9c411 --- /dev/null +++ b/tests/auto/httpserver/data/notification.html @@ -0,0 +1,70 @@ +<!doctype html> +<html> +<head> +<title>Desktop Notifications Demo</title> +<script> + function resetPermission() { document.Notification = 'default' } + + function getPermission() { return document.Notification } + + function sendNotification(title, body) { + let notification = new Notification(title, { body: body, dir: 'rtl', lang: 'de', tag: 'tst' }) + notification.onclick = function() { console.info('onclick') } + notification.onclose = function() { console.info('onclose') } + notification.onerror = function(error) { console.info('onerror: ' + error) } + notification.onshow = function() { console.info('onshow') } + } + + function makeNotification() { + let title = document.getElementById("title").value + let body = document.getElementById("body").value + console.log('making notification:', title) + sendNotification(title, body) + } + + function requestPermission(callback) { + Notification.requestPermission().then(function (permission) { + document.Notification = permission + if (callback) + callback(permission) + }) + } + + function displayNotification() { + console.info('notifications are ' + document.Notification) + + let state = document.getElementById('state') + + if (document.Notification === 'denied') { + state.innerHTML = 'Notifications disabled' + } else if (document.Notification === 'granted') { + makeNotification() + state.innerHTML = 'notification created' + } else { + state.innerHTML = 'requesting permission...' + requestPermission(function (permission) { + console.info('notifications request: ' + permission) + if (permission === 'granted') { + makeNotification() + state.innerHTML = 'permission granted, notification created' + } else if (permission === 'denied') + state.innerHTML = 'Notifications are disabled' + }) + } + } + + document.addEventListener("DOMContentLoaded", function() { + document.Notification = Notification.permission + }) +</script> +</head> +<body> + <form name="NotificationForm" id="notificationForm"> + Title: <input type="text" id="title" placeholder="Notification title" value='sample title'><br> + Body: <input type="text" id="body" placeholder="Notification body" value='default body'><br> + <input type="button" value="Display Notification" onclick="displayNotification()"><br> + <input type="button" value="Reset Permission" onclick="resetPermission()"> + </form> + <div id='state'></div> +</body> +</html> diff --git a/tests/auto/httpserver/httpreqrep.cpp b/tests/auto/httpserver/httpreqrep.cpp new file mode 100644 index 000000000..65a1df365 --- /dev/null +++ b/tests/auto/httpserver/httpreqrep.cpp @@ -0,0 +1,125 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include "httpreqrep.h" + +HttpReqRep::HttpReqRep(QTcpSocket *socket, QObject *parent) + : QObject(parent), m_socket(socket) +{ + m_socket->setParent(this); + connect(m_socket, &QTcpSocket::readyRead, this, &HttpReqRep::handleReadyRead); + connect(m_socket, &QTcpSocket::disconnected, this, &HttpReqRep::handleDisconnected); +} + +void HttpReqRep::sendResponse(int statusCode) +{ + if (m_state != State::REQUEST_RECEIVED) + return; + m_responseStatusCode = statusCode; + m_socket->write("HTTP/1.1 "); + m_socket->write(QByteArray::number(m_responseStatusCode)); + m_socket->write(" OK?\r\n"); + for (const auto & kv : m_responseHeaders) { + m_socket->write(kv.first); + m_socket->write(": "); + m_socket->write(kv.second); + m_socket->write("\r\n"); + } + m_socket->write("Connection: close\r\n"); + m_socket->write("\r\n"); + m_socket->write(m_responseBody); + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT responseSent(); +} + +void HttpReqRep::sendResponse(const QByteArray &response) +{ + m_socket->write(response); + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT responseSent(); +} + +void HttpReqRep::close() +{ + if (m_state != State::REQUEST_RECEIVED) + return; + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("missing response")); +} + +QByteArray HttpReqRep::requestHeader(const QByteArray &key) const +{ + auto it = m_requestHeaders.find(key.toLower()); + if (it != m_requestHeaders.end()) + return it->second; + return {}; +} + +bool HttpReqRep::hasRequestHeader(const QByteArray &key) const +{ + return m_requestHeaders.find(key.toLower()) != m_requestHeaders.end(); +} + +void HttpReqRep::handleReadyRead() +{ + while (m_socket->canReadLine()) { + switch (m_state) { + case State::RECEIVING_REQUEST: { + const auto requestLine = m_socket->readLine(); + const auto requestLineParts = requestLine.split(' '); + if (requestLineParts.size() != 3 || !requestLineParts[2].toUpper().startsWith("HTTP/")) { + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("invalid request line")); + return; + } + m_requestMethod = requestLineParts[0]; + m_requestPath = requestLineParts[1]; + m_state = State::RECEIVING_HEADERS; + break; + } + case State::RECEIVING_HEADERS: { + const auto headerLine = m_socket->readLine(); + if (headerLine == QByteArrayLiteral("\r\n")) { + m_state = State::REQUEST_RECEIVED; + Q_EMIT requestReceived(); + return; + } + int colonIndex = headerLine.indexOf(':'); + if (colonIndex < 0) { + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("invalid header line")); + return; + } + auto headerKey = headerLine.left(colonIndex).trimmed().toLower(); + auto headerValue = headerLine.mid(colonIndex + 1).trimmed().toLower(); + m_requestHeaders.emplace(headerKey, headerValue); + break; + } + default: + return; + } + } +} + +void HttpReqRep::handleDisconnected() +{ + switch (m_state) { + case State::RECEIVING_REQUEST: + case State::RECEIVING_HEADERS: + case State::REQUEST_RECEIVED: + Q_EMIT error(QStringLiteral("unexpected disconnect")); + break; + case State::DISCONNECTING: + break; + case State::DISCONNECTED: + Q_UNREACHABLE(); + } + m_state = State::DISCONNECTED; + Q_EMIT closed(); +} + +#include "moc_httpreqrep.cpp" diff --git a/tests/auto/httpserver/httpreqrep.h b/tests/auto/httpserver/httpreqrep.h new file mode 100644 index 000000000..2b90b774b --- /dev/null +++ b/tests/auto/httpserver/httpreqrep.h @@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#ifndef HTTPREQREP_H +#define HTTPREQREP_H + +#include <QTcpSocket> + +#include <map> +#include <utility> + +// Represents an HTTP request-response exchange. +class HttpReqRep : public QObject +{ + Q_OBJECT +public: + explicit HttpReqRep(QTcpSocket *socket, QObject *parent = nullptr); + + Q_INVOKABLE void sendResponse(int statusCode = 200); + void sendResponse(const QByteArray &response); + void close(); + bool isClosed() const { return m_state == State::DISCONNECTED; } + + // Request parameters (only valid after requestReceived()) + + QByteArray requestMethod() const { return m_requestMethod; } + QByteArray requestPath() const { return m_requestPath; } + QByteArray requestHeader(const QByteArray &key) const; + bool hasRequestHeader(const QByteArray &key) const; + + // Response parameters (can be set until sendResponse()/close()). + + int responseStatus() const { return m_responseStatusCode; } + void setResponseStatus(int statusCode) + { + m_responseStatusCode = statusCode; + } + void setResponseHeader(const QByteArray &key, QByteArray value) + { + m_responseHeaders[key.toLower()] = std::move(value); + } + QByteArray responseBody() const { return m_responseBody; } + Q_INVOKABLE void setResponseBody(QByteArray content) + { + m_responseHeaders["content-length"] = QByteArray::number(content.size()); + m_responseBody = std::move(content); + } + +Q_SIGNALS: + // Emitted when the request has been correctly parsed. + void requestReceived(); + // Emitted on first call to sendResponse(). + void responseSent(); + // Emitted when something goes wrong. + void error(const QString &error); + // Emitted during or some time after sendResponse() or close(). + void closed(); + +private Q_SLOTS: + void handleReadyRead(); + void handleDisconnected(); + +private: + enum class State { + // Waiting for first line of request. + RECEIVING_REQUEST, // Next: RECEIVING_HEADERS or DISCONNECTING. + // Waiting for header lines. + RECEIVING_HEADERS, // Next: REQUEST_RECEIVED or DISCONNECTING. + // Request parsing succeeded, waiting for sendResponse() or close(). + REQUEST_RECEIVED, // Next: DISCONNECTING. + // Waiting for network. + DISCONNECTING, // Next: DISCONNECTED. + // Connection is dead. + DISCONNECTED, // Next: - + }; + QTcpSocket *m_socket = nullptr; + State m_state = State::RECEIVING_REQUEST; + QByteArray m_requestMethod; + QByteArray m_requestPath; + std::map<QByteArray, QByteArray> m_requestHeaders; + int m_responseStatusCode = -1; + std::map<QByteArray, QByteArray> m_responseHeaders; + QByteArray m_responseBody; +}; + +#endif // !HTTPREQREP_H diff --git a/tests/auto/httpserver/httpserver.cmake b/tests/auto/httpserver/httpserver.cmake new file mode 100644 index 000000000..f98434e1a --- /dev/null +++ b/tests/auto/httpserver/httpserver.cmake @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if (NOT TARGET Test::HttpServer) + + add_library(httpserver STATIC + ${CMAKE_CURRENT_LIST_DIR}/httpreqrep.cpp + ${CMAKE_CURRENT_LIST_DIR}/httpreqrep.h + ${CMAKE_CURRENT_LIST_DIR}/httpserver.cpp + ${CMAKE_CURRENT_LIST_DIR}/httpserver.h + ${CMAKE_CURRENT_LIST_DIR}/proxy_server.h + ${CMAKE_CURRENT_LIST_DIR}/proxy_server.cpp + ) + + # moc binary might not exist in case of top level build + qt_autogen_tools(httpserver ENABLE_AUTOGEN_TOOLS "moc") + + if(QT_FEATURE_ssl) + target_sources(httpserver INTERFACE ${CMAKE_CURRENT_LIST_DIR}/httpsserver.h) + endif() + + add_library(Test::HttpServer ALIAS httpserver) + + target_include_directories(httpserver INTERFACE + $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}> + ) + + target_link_libraries(httpserver PUBLIC + Qt::Core + Qt::Network + ) + + get_filename_component(SERVER_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}" REALPATH) + target_compile_definitions(httpserver PRIVATE + SERVER_SOURCE_DIR="${SERVER_SOURCE_DIR}" + ) + + set_target_properties(httpserver PROPERTIES + SHARED_DATA "${CMAKE_CURRENT_LIST_DIR}/data" + ) +endif() diff --git a/tests/auto/httpserver/httpserver.cpp b/tests/auto/httpserver/httpserver.cpp new file mode 100644 index 000000000..8a1d8e6f6 --- /dev/null +++ b/tests/auto/httpserver/httpserver.cpp @@ -0,0 +1,127 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include "httpserver.h" + +#include <QFile> +#include <QLoggingCategory> +#include <QMimeDatabase> + +Q_LOGGING_CATEGORY(gHttpServerLog, "HttpServer") + +HttpServer::HttpServer(QObject *parent) + : HttpServer(new QTcpServer, "http", QHostAddress::LocalHost, 0, parent) +{ +} + +HttpServer::HttpServer(const QHostAddress &hostAddress, quint16 port, QObject *parent) + : HttpServer(new QTcpServer, "http", hostAddress, port, parent) +{ +} + +HttpServer::HttpServer(QTcpServer *tcpServer, const QString &protocol, + const QHostAddress &hostAddress, quint16 port, QObject *parent) + : QObject(parent), m_tcpServer(tcpServer), m_hostAddress(hostAddress), m_port(port) +{ + m_url.setHost(hostAddress.toString()); + m_url.setScheme(protocol); + connect(tcpServer, &QTcpServer::pendingConnectionAvailable, this, + &HttpServer::handleNewConnection); +} + +HttpServer::~HttpServer() +{ + delete m_tcpServer; +} + +bool HttpServer::start() +{ + m_error = false; + m_expectingError = false; + m_ignoreNewConnection = false; + + if (!m_tcpServer->listen(m_hostAddress, m_port)) { + qCWarning(gHttpServerLog).noquote() << m_tcpServer->errorString(); + return false; + } + + m_url.setPort(m_tcpServer->serverPort()); + return true; +} + +bool HttpServer::stop() +{ + m_tcpServer->close(); + return m_error == m_expectingError; +} + +void HttpServer::setExpectError(bool b) +{ + m_expectingError = b; +} + +QUrl HttpServer::url(const QString &path) const +{ + auto copy = m_url; + copy.setPath(path); + return copy; +} + +void HttpServer::handleNewConnection() +{ + if (m_ignoreNewConnection) + return; + + auto rr = new HttpReqRep(m_tcpServer->nextPendingConnection(), this); + connect(rr, &HttpReqRep::requestReceived, [this, rr]() { + Q_EMIT newRequest(rr); + if (rr->isClosed()) // was explicitly answered + return; + + // if request wasn't handled or purposely ignored for default behavior + // then try to serve htmls from resources dirs if set + if (rr->requestMethod() == "GET") { + for (auto &&dir : std::as_const(m_dirs)) { + QFile f(dir + rr->requestPath()); + if (f.exists()) { + if (f.open(QFile::ReadOnly)) { + QMimeType mime = QMimeDatabase().mimeTypeForFileNameAndData(f.fileName(), &f); + rr->setResponseHeader(QByteArrayLiteral("Content-Type"), mime.name().toUtf8()); + rr->setResponseHeader(QByteArrayLiteral("Access-Control-Allow-Origin"), QByteArrayLiteral("*")); + rr->setResponseBody(f.readAll()); + rr->sendResponse(); + } else { + qWarning() << "Can't open resource" << f.fileName() << ": " << f.errorString(); + rr->sendResponse(500); // internal server error + } + break; + } else { + qWarning() << "Can't open resource" << dir + rr->requestPath(); + } + } + } + + if (!rr->isClosed()) + rr->sendResponse(404); + }); + connect(rr, &HttpReqRep::responseSent, [rr]() { + qCInfo(gHttpServerLog).noquote() << rr->requestMethod() << rr->requestPath() + << rr->responseStatus() << rr->responseBody().size(); + }); + connect(rr, &HttpReqRep::error, [this, rr](const QString &error) { + qCWarning(gHttpServerLog).noquote() << rr->requestMethod() << rr->requestPath() + << error; + m_error = true; + }); + + if (!m_tcpServer->isListening()) { + m_ignoreNewConnection = true; + connect(rr, &HttpReqRep::closed, rr, &QObject::deleteLater); + } +} + +QString HttpServer::sharedDataDir() const +{ + return SERVER_SOURCE_DIR + QLatin1String("/data"); +} + +#include "moc_httpserver.cpp" diff --git a/tests/auto/httpserver/httpserver.h b/tests/auto/httpserver/httpserver.h new file mode 100644 index 000000000..a1c2e3791 --- /dev/null +++ b/tests/auto/httpserver/httpserver.h @@ -0,0 +1,82 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#ifndef HTTPSERVER_H +#define HTTPSERVER_H + +#include "httpreqrep.h" + +#include <QTcpServer> +#include <QUrl> + +// Listens on a TCP socket and creates HttpReqReps for each connection. +// +// Usage: +// +// HttpServer server; +// connect(&server, &HttpServer::newRequest, [](HttpReqRep *rr) { +// if (rr->requestPath() == "/myPage.html") { +// rr->setResponseBody("<html><body>Hello, World!</body></html>"); +// rr->sendResponse(); +// } +// }); +// QVERIFY(server.start()); +// /* do stuff */ +// QVERIFY(server.stop()); +// +// HttpServer owns the HttpReqRep objects. The signal handler should not store +// references to HttpReqRep objects. +// +// Only if a handler calls sendResponse() will a response be actually sent. This +// means that multiple handlers can be connected to the signal, with different +// handlers responsible for different paths. +class HttpServer : public QObject +{ + Q_OBJECT +public: + explicit HttpServer(QObject *parent = nullptr); + HttpServer(const QHostAddress &hostAddress, quint16 port, QObject *parent = nullptr); + HttpServer(QTcpServer *server, const QString &protocol, const QHostAddress &address, + quint16 port, QObject *parent = nullptr); + + ~HttpServer() override; + + // Must be called to start listening. + // + // Returns true if a TCP port has been successfully bound. + Q_INVOKABLE Q_REQUIRED_RESULT bool start(); + + // Stops listening and performs final error checks. + Q_INVOKABLE Q_REQUIRED_RESULT bool stop(); + + Q_INVOKABLE void setExpectError(bool b); + + // Full URL for given relative path + Q_INVOKABLE QUrl url(const QString &path = QStringLiteral("/")) const; + + Q_INVOKABLE QString sharedDataDir() const; + + Q_INVOKABLE void setResourceDirs(const QStringList &dirs) { m_dirs = dirs; } + + Q_INVOKABLE void setHostDomain(const QString &host) { m_url.setHost(host); } + + Q_INVOKABLE QTcpServer *getTcpServer() const { return m_tcpServer; } + +Q_SIGNALS: + // Emitted after a HTTP request has been successfully parsed. + void newRequest(HttpReqRep *reqRep); + +private Q_SLOTS: + void handleNewConnection(); + +private: + QTcpServer *m_tcpServer; + QUrl m_url; + QStringList m_dirs; + bool m_error = false; + bool m_ignoreNewConnection = false; + bool m_expectingError = false; + QHostAddress m_hostAddress; + quint16 m_port; +}; + +#endif // !HTTPSERVER_H diff --git a/tests/auto/httpserver/httpsserver.h b/tests/auto/httpserver/httpsserver.h new file mode 100644 index 000000000..237314965 --- /dev/null +++ b/tests/auto/httpserver/httpsserver.h @@ -0,0 +1,73 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#ifndef HTTPSSERVER_H +#define HTTPSSERVER_H + +#include "httpreqrep.h" +#include "httpserver.h" + +#include <QDebug> +#include <QtCore/qfile.h> +#include <QtNetwork/qsslkey.h> +#include <QtNetwork/qsslsocket.h> +#include <QtNetwork/qsslconfiguration.h> +#include <QtNetwork/qsslserver.h> + +static QSslServer *createServer(const QString &certificateFileName, const QString &keyFileName, + const QString &ca) +{ + QSslConfiguration configuration(QSslConfiguration::defaultConfiguration()); + + QFile keyFile(keyFileName); + if (keyFile.open(QIODevice::ReadOnly)) { + QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); + if (!key.isNull()) { + configuration.setPrivateKey(key); + } else { + qCritical() << "Could not parse key: " << keyFileName; + } + } else { + qCritical() << "Could not find key: " << keyFileName; + } + + QList<QSslCertificate> localCerts = QSslCertificate::fromPath(certificateFileName); + if (!localCerts.isEmpty()) { + configuration.setLocalCertificateChain(localCerts); + } else { + qCritical() << "Could not find certificate: " << certificateFileName; + } + + if (!ca.isEmpty()) { + QList<QSslCertificate> caCerts = QSslCertificate::fromPath(ca); + if (!caCerts.isEmpty()) { + configuration.addCaCertificates(caCerts); + configuration.setPeerVerifyMode(QSslSocket::VerifyPeer); + } else { + qCritical() << "Could not find certificate: " << certificateFileName; + } + } + + QSslServer *server = new QSslServer(); + server->setSslConfiguration(configuration); + return server; +} + +struct HttpsServer : HttpServer +{ + HttpsServer(const QString &certPath, const QString &keyPath, const QString &ca, + quint16 port = 0, QObject *parent = nullptr) + : HttpServer(createServer(certPath, keyPath, ca), "https", QHostAddress::LocalHost, port, + parent) + { + } + + void setVerifyMode(const QSslSocket::PeerVerifyMode verifyMode) + { + QSslServer *server = static_cast<QSslServer *>(getTcpServer()); + QSslConfiguration config = server->sslConfiguration(); + config.setPeerVerifyMode(verifyMode); + server->setSslConfiguration(config); + } +}; + +#endif diff --git a/tests/auto/httpserver/proxy_server.cpp b/tests/auto/httpserver/proxy_server.cpp new file mode 100644 index 000000000..e47214ee4 --- /dev/null +++ b/tests/auto/httpserver/proxy_server.cpp @@ -0,0 +1,85 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "proxy_server.h" +#include <QDataStream> +#include <QTcpSocket> +#include <QDebug> + +ProxyServer::ProxyServer(QObject *parent) : QObject(parent) +{ + connect(&m_server, &QTcpServer::newConnection, this, &ProxyServer::handleNewConnection); +} + +void ProxyServer::setPort(int port) +{ + m_port = port; +} + +void ProxyServer::setCredentials(const QByteArray &user, const QByteArray password) +{ + m_auth.append(user); + m_auth.append(':'); + m_auth.append(password); + m_auth = m_auth.toBase64(); + m_authenticate = true; +} + +void ProxyServer::setCookie(const QByteArray &cookie) +{ + m_cookie.append(QByteArrayLiteral("Cookie: ")); + m_cookie.append(cookie); +} + + +bool ProxyServer::isListening() +{ + return m_server.isListening(); +} + +void ProxyServer::run() +{ + if (!m_server.listen(QHostAddress::LocalHost, m_port)) + qFatal("Could not start the test server"); +} + +void ProxyServer::handleNewConnection() +{ + // do one connection at the time + Q_ASSERT(m_data.isEmpty()); + QTcpSocket *socket = m_server.nextPendingConnection(); + Q_ASSERT(socket); + connect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater); + connect(socket, &QAbstractSocket::readyRead, this, &ProxyServer::handleReadReady); +} + +void ProxyServer::handleReadReady() +{ + QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender()); + Q_ASSERT(socket); + + m_data.append(socket->readAll()); + + if (!m_data.endsWith("\r\n\r\n")) + return; + + if (m_authenticate && !m_data.contains(QByteArrayLiteral("Proxy-Authorization: Basic"))) { + socket->write("HTTP/1.1 407 Proxy Authentication Required\nProxy-Authenticate: " + "Basic realm=\"Proxy requires authentication\"\r\n" + "content-length: 0\r\n" + "\r\n"); + return; + } + + if (m_authenticate && m_data.contains(m_auth)) { + emit authenticationSuccess(); + } + + if (m_data.contains(m_cookie)) { + emit cookieMatch(); + } + m_data.clear(); + emit requestReceived(); +} + +#include "moc_proxy_server.cpp" diff --git a/tests/auto/httpserver/proxy_server.h b/tests/auto/httpserver/proxy_server.h new file mode 100644 index 000000000..8f7cd4cc6 --- /dev/null +++ b/tests/auto/httpserver/proxy_server.h @@ -0,0 +1,42 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef PROXY_SERVER_H +#define PROXY_SERVER_H + +#include <QObject> +#include <QTcpServer> + +class ProxyServer : public QObject +{ + Q_OBJECT + +public: + explicit ProxyServer(QObject *parent = nullptr); + void setCredentials(const QByteArray &user, const QByteArray password); + void setCookie(const QByteArray &cookie); + bool isListening(); + void setPort(int port); + +public slots: + void run(); + +private slots: + void handleNewConnection(); + void handleReadReady(); + +signals: + void authenticationSuccess(); + void cookieMatch(); + void requestReceived(); + +private: + int m_port = 5555; + QByteArray m_data; + QTcpServer m_server; + QByteArray m_auth; + QByteArray m_cookie; + bool m_authenticate = false; +}; + +#endif // PROXY_SERVER_H |