From 40182978e0f8fe305357c867c0cdb1b04c32c06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 5 Nov 2025 21:05:34 +0100 Subject: [PATCH] Implement webserver resource mechanism --- libnymea-core/debugserverhandler.cpp | 32 +++- libnymea-core/debugserverhandler.h | 10 +- libnymea-core/libnymea-core.pro | 5 +- libnymea-core/nymeacore.cpp | 2 + libnymea-core/nymeacore.h | 2 +- libnymea-core/servermanager.cpp | 40 ++++ libnymea-core/servermanager.h | 18 +- libnymea-core/servers/webserver.cpp | 179 ++++++++++-------- libnymea-core/servers/webserver.h | 22 ++- libnymea-core/zwave/zwavemanager.cpp | 2 +- libnymea/experiences/experienceplugin.h | 1 - libnymea/libnymea.pro | 6 + .../webserver}/httpreply.cpp | 11 +- .../webserver}/httpreply.h | 19 +- .../webserver}/httprequest.cpp | 3 - .../webserver}/httprequest.h | 5 +- libnymea/webserver/webserverresource.cpp | 43 +++++ libnymea/webserver/webserverresource.h | 58 ++++++ plugins/mock/httpdaemon.h | 2 +- tests/auto/webserver/testwebserver.cpp | 2 + 20 files changed, 324 insertions(+), 138 deletions(-) rename {libnymea-core/servers => libnymea/webserver}/httpreply.cpp (97%) rename {libnymea-core/servers => libnymea/webserver}/httpreply.h (92%) rename {libnymea-core/servers => libnymea/webserver}/httprequest.cpp (99%) rename {libnymea-core/servers => libnymea/webserver}/httprequest.h (95%) create mode 100644 libnymea/webserver/webserverresource.cpp create mode 100644 libnymea/webserver/webserverresource.h diff --git a/libnymea-core/debugserverhandler.cpp b/libnymea-core/debugserverhandler.cpp index 9a642862..baa5bebf 100644 --- a/libnymea-core/debugserverhandler.cpp +++ b/libnymea-core/debugserverhandler.cpp @@ -23,14 +23,12 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "nymeacore.h" -#include "servers/httpreply.h" #include "nymeasettings.h" #include "loggingcategories.h" #include "debugserverhandler.h" #include "nymeaconfiguration.h" #include "version.h" -#include #include #include #include @@ -41,19 +39,43 @@ #include #include - namespace nymeaserver { -QList DebugServerHandler::s_websocketClients; +QList DebugServerHandler::s_websocketClients; QMutex DebugServerHandler::s_loggingMutex; DebugServerHandler::DebugServerHandler(QObject *parent) : - QObject(parent) + WebServerResource("/debug", parent) { connect(NymeaCore::instance()->configuration(), &NymeaConfiguration::debugServerEnabledChanged, this, &DebugServerHandler::onDebugServerEnabledChanged); onDebugServerEnabledChanged(NymeaCore::instance()->configuration()->debugServerEnabled()); } +bool DebugServerHandler::authenticationRequired() const +{ + return false; +} + +HttpReply *DebugServerHandler::processRequest(const HttpRequest &request) +{ + if (NymeaCore::instance()->configuration()->debugServerEnabled()) { + + // Verify methods + if (request.method() != HttpRequest::Get && request.method() != HttpRequest::Options) { + HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); + reply->setHeader(HttpReply::AllowHeader, "GET, OPTIONS"); + return reply; + } + + qCDebug(dcDebugServer()) << "Request:" << request.url().toString(); + return processDebugRequest(request.url().path(), request.urlQuery()); + + } else { + qCWarning(dcWebServer()) << "The debug server handler is disabled. You can enable it by adding \'debugServerEnabled=true\' in the \'nymead\' section of the nymead.conf file."; + return HttpReply::createErrorReply(HttpReply::NotFound); + } +} + HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, const QUrlQuery &requestQuery) { qCDebug(dcDebugServer()) << "Debug request for" << requestPath; diff --git a/libnymea-core/debugserverhandler.h b/libnymea-core/debugserverhandler.h index b4083fba..c3928110 100644 --- a/libnymea-core/debugserverhandler.h +++ b/libnymea-core/debugserverhandler.h @@ -33,17 +33,19 @@ #include #include "debugreportgenerator.h" -#include "servers/httpreply.h" +#include "webserver/webserverresource.h" namespace nymeaserver { -class DebugServerHandler : public QObject +class DebugServerHandler : public WebServerResource { Q_OBJECT public: explicit DebugServerHandler(QObject *parent = nullptr); - HttpReply *processDebugRequest(const QString &requestPath, const QUrlQuery &requestQuery); + bool authenticationRequired() const override; + + HttpReply *processRequest(const HttpRequest &request) override; private: static QList s_websocketClients; @@ -63,6 +65,8 @@ private: DebugReportGenerator *m_debugReportGenerator = nullptr; + HttpReply *processDebugRequest(const QString &requestPath, const QUrlQuery &requestQuery); + QByteArray loadResourceData(const QString &resourceFileName); QString getResourceFileName(const QString &requestPath); bool resourceFileExits(const QString &requestPath); diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index 09259ab5..1a5665cf 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -69,6 +69,7 @@ HEADERS += nymeacore.h \ logging/logengineinfluxdb.h \ scriptengine/scriptthing.h \ scriptengine/scriptthings.h \ + servers/webserverresource.h \ zwave/zwavedevicedatabase.h \ zwave/zwavemanagerreply.h \ zwave/zwavenodeimplementation.h \ @@ -107,8 +108,6 @@ HEADERS += nymeacore.h \ servers/tcpserver.h \ servers/mocktcpserver.h \ servers/webserver.h \ - servers/httprequest.h \ - servers/httpreply.h \ servers/bluetoothserver.h \ servers/websocketserver.h \ servers/mqttbroker.h \ @@ -210,8 +209,6 @@ SOURCES += nymeacore.cpp \ servers/tcpserver.cpp \ servers/mocktcpserver.cpp \ servers/webserver.cpp \ - servers/httprequest.cpp \ - servers/httpreply.cpp \ servers/websocketserver.cpp \ servers/bluetoothserver.cpp \ servers/mqttbroker.cpp \ diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index 0872fd4a..85600fca 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -37,6 +37,7 @@ #include "jsonrpc/scriptshandler.h" #include "jsonrpc/debughandler.h" #include "usermanager/usermanager.h" +#include "debugserverhandler.h" #include "version.h" #include "integrations/thingmanagerimplementation.h" @@ -152,6 +153,7 @@ void NymeaCore::init(const QStringList &additionalInterfaces, bool disableLogEng qCDebug(dcCore) << "Creating Debug Server Handler"; m_debugServerHandler = new DebugServerHandler(this); + m_serverManager->registerWebServerResource(m_debugServerHandler); qCDebug(dcCore) << "Register Debug Handler"; m_serverManager->jsonServer()->registerHandler(new DebugHandler(m_serverManager->jsonServer())); diff --git a/libnymea-core/nymeacore.h b/libnymea-core/nymeacore.h index ea6c9785..3d9e5e9a 100644 --- a/libnymea-core/nymeacore.h +++ b/libnymea-core/nymeacore.h @@ -39,7 +39,6 @@ #include "time/timemanager.h" #include "hardwaremanagerimplementation.h" -#include "debugserverhandler.h" #include @@ -63,6 +62,7 @@ class ZigbeeManager; class ZWaveManager; class ModbusRtuManager; class SerialPortMonitor; +class DebugServerHandler; namespace scriptengine { class ScriptEngine; diff --git a/libnymea-core/servermanager.cpp b/libnymea-core/servermanager.cpp index 37ef8dce..d316b880 100644 --- a/libnymea-core/servermanager.cpp +++ b/libnymea-core/servermanager.cpp @@ -56,6 +56,8 @@ #include "network/zeroconf/zeroconfservicepublisher.h" +#include + #include #include #include @@ -213,6 +215,13 @@ ServerManager::ServerManager(Platform *platform, NymeaConfiguration *configurati foreach (const WebServerConfiguration &config, configuration->webServerConfigurations()) { WebServer *webServer = new WebServer(config, m_sslConfiguration, this); m_webServers.insert(config.id, webServer); + + foreach (WebServerResource *resource, m_webServerResources) { + if (!webServer->registerResource(resource)) { + qCWarning(dcServerManager()) << "Unable to register resource" << resource->basePath() << "on webserver" << webServer->serverUrl().toString(); + } + } + if (webServer->startServer()) { registerZeroConfService(config, "http", "_http._tcp"); } @@ -267,6 +276,30 @@ MqttBroker *ServerManager::mqttBroker() const return m_mqttBroker; } +bool ServerManager::registerWebServerResource(WebServerResource *resource) +{ + if (m_webServerResources.contains(resource->basePath())) { + qCDebug(dcServerManager()) << "Could not register web server resource" << resource->basePath() << "because a resource with this path has already been registered"; + return false; + } + + m_webServerResources.insert(resource->basePath(), resource); + + foreach (WebServer *webserver, m_webServers) + webserver->registerResource(resource); + + return true; +} + +void ServerManager::unregisterWebServerResource(WebServerResource *resource) +{ + m_webServerResources.remove(resource->basePath()); + + foreach (WebServer *webserver, m_webServers) + webserver->unregisterResource(resource); + +} + void ServerManager::tcpServerConfigurationChanged(const QString &id) { ServerConfiguration config = NymeaCore::instance()->configuration()->tcpServerConfigurations().value(id); @@ -346,6 +379,12 @@ void ServerManager::webServerConfigurationChanged(const QString &id) qCDebug(dcServerManager()) << "Received a Web Server config change event but don't have a Web Server instance for it. Creating new WebServer instance on" << config.address << config.port << "(SSL:" << config.sslEnabled << ")"; server = new WebServer(config, m_sslConfiguration, this); m_webServers.insert(config.id, server); + + foreach (WebServerResource *resource, m_webServerResources) { + if (!server->registerResource(resource)) { + qCWarning(dcServerManager()) << "Unable to register resource" << resource->basePath() << "on webserver" << server->serverUrl().toString(); + } + } } if (server->startServer()) { registerZeroConfService(config, "http", "_http._tcp"); @@ -358,6 +397,7 @@ void ServerManager::webServerConfigurationRemoved(const QString &id) qCWarning(dcServerManager()) << "Received a Web Server config removed event but don't have a Web Server instance for it."; return; } + WebServer *server = m_webServers.take(id); unregisterZeroConfService(id, "http"); server->stopServer(); diff --git a/libnymea-core/servermanager.h b/libnymea-core/servermanager.h index fb127bdb..5fe6355a 100644 --- a/libnymea-core/servermanager.h +++ b/libnymea-core/servermanager.h @@ -32,6 +32,7 @@ #include #include +class WebServerResource; namespace nymeaserver { @@ -55,13 +56,14 @@ public: // Interfaces JsonRPCServerImplementation *jsonServer() const; - - BluetoothServer* bluetoothServer() const; - + BluetoothServer *bluetoothServer() const; MockTcpServer *mockTcpServer() const; - MqttBroker *mqttBroker() const; + // Resources for the webservers + bool registerWebServerResource(WebServerResource *resource); + void unregisterWebServerResource(WebServerResource *resource); + public slots: void setServerName(const QString &serverName); @@ -94,14 +96,16 @@ private: JsonRPCServerImplementation *m_jsonServer; BluetoothServer *m_bluetoothServer; - QHash m_tcpServers; - QHash m_webSocketServers; - QHash m_webServers; + QHash m_tcpServers; + QHash m_webSocketServers; + QHash m_webServers; QHash m_tunnelProxyServers; MockTcpServer *m_mockTcpServer; MqttBroker *m_mqttBroker; + QHash m_webServerResources; + // Encrytption and stuff QSslConfiguration m_sslConfiguration; QSslKey m_certificateKey; diff --git a/libnymea-core/servers/webserver.cpp b/libnymea-core/servers/webserver.cpp index b60fd573..7bb9ff39 100644 --- a/libnymea-core/servers/webserver.cpp +++ b/libnymea-core/servers/webserver.cpp @@ -76,11 +76,10 @@ #include "webserver.h" #include "loggingcategories.h" -#include "nymeasettings.h" #include "nymeacore.h" -#include "httpreply.h" -#include "httprequest.h" -#include "debugserverhandler.h" +#include "webserver/httpreply.h" +#include "webserver/httprequest.h" +#include "webserver/webserverresource.h" #include "version.h" #include @@ -107,16 +106,20 @@ WebServer::WebServer(const WebServerConfiguration &configuration, const QSslConf m_configuration(configuration), m_sslConfiguration(sslConfiguration) { - if (QCoreApplication::instance()->organizationName() == "nymea-test") { + if (QCoreApplication::instance()->organizationName() == "nymea-test") m_configuration.publicFolder = QCoreApplication::applicationDirPath(); - } - qCDebug(dcWebServer()) << "Starting WebServer. Interface:" << m_configuration.address << "Port:" << m_configuration.port << "SSL:" << m_configuration.sslEnabled << "AUTH:" << m_configuration.authenticationEnabled << "Public folder:" << QDir(m_configuration.publicFolder).canonicalPath(); + + qCInfo(dcWebServer()) << "Starting WebServer. Interface:" << m_configuration.address + << "Port:" << m_configuration.port + << "SSL:" << (m_configuration.sslEnabled ? "enabled" : "disabled") + << "AUTH:" << (m_configuration.authenticationEnabled ? "enabled" : "disabled") + << "Public folder:" << QDir(m_configuration.publicFolder).canonicalPath(); } /*! Destructor of this \l{WebServer}. */ WebServer::~WebServer() { - qCDebug(dcWebServer()) << "Shutting down \"Webserver\"" << serverUrl().toString(); + qCInfo(dcWebServer()) << "Shutting down \"Webserver\"" << serverUrl().toString(); this->close(); } @@ -124,7 +127,17 @@ WebServer::~WebServer() /*! Returns the server URL of this WebServer. */ QUrl WebServer::serverUrl() const { - return QUrl(QString("%1://%2:%3").arg((m_configuration.sslEnabled ? "https" : "http")).arg(m_configuration.address).arg(m_configuration.port)); + QUrl url; + url.setScheme(m_configuration.sslEnabled ? "https" : "http"); + url.setHost(m_configuration.address); + url.setPort(m_configuration.port); + return url; +} + +/*! Returns the configuration of this WebServer. */ +WebServerConfiguration WebServer::configuration() const +{ + return m_configuration; } /*! Send the given \a reply map to the corresponding client. @@ -133,7 +146,6 @@ QUrl WebServer::serverUrl() const */ void WebServer::sendHttpReply(HttpReply *reply) { - // get the right socket QSslSocket *socket = nullptr; socket = m_clientList.value(reply->clientId()); if (!socket) { @@ -148,13 +160,40 @@ void WebServer::sendHttpReply(HttpReply *reply) socket->write(reply->data()); } +QList WebServer::resources() const +{ + return m_resources.values(); +} + +bool WebServer::registerResource(WebServerResource *resource) +{ + qCDebug(dcWebServer()) << "Register resource" << resource->basePath() << "on server" << serverUrl().toString(); + + if (m_resources.contains(resource->basePath())) { + qCWarning(dcWebServer()) << "Could not register resource" << resource->basePath() << "because there is already a resource resistered for this base path."; + return false; + } + + m_resources.insert(resource->basePath(), resource); + return true; +} + +void WebServer::unregisterResource(WebServerResource *resource) +{ + qCDebug(dcWebServer()) << "Unregister resource" << resource->basePath() << "from server" << serverUrl().toString(); + if (!m_resources.contains(resource->basePath())) { + qCWarning(dcWebServer()) << "Could not unregister resource" << resource->basePath() << "because there is no resource resistered with this base path."; + return; + } + + m_resources.remove(resource->basePath()); +} + bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName) { QFileInfo file(fileName); - - // make sure the file exists if (!file.exists()) { - qCDebug(dcWebServer()) << "requested file" << file.filePath() << "does not exist."; + qCDebug(dcWebServer()) << "Requested file" << file.filePath() << "does not exist."; HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound); reply->setClientId(m_clientList.key(socket)); sendHttpReply(reply); @@ -182,6 +221,7 @@ bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName) reply->deleteLater(); return false; } + return true; } @@ -269,16 +309,7 @@ void WebServer::incomingConnection(qintptr socketDescriptor) return; } - connect(socket, &QSslSocket::readyRead, this, &WebServer::readClient); - connect(socket, &QSslSocket::disconnected, this, &WebServer::onDisconnected); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - connect(socket, &QSslSocket::errorOccurred, this, &WebServer::onError); -#else - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); -#endif - - emit clientConnected(clientId); + setupClient(clientId, socket); } void WebServer::readClient() @@ -345,6 +376,29 @@ void WebServer::readClient() } } + // Verify if we habe a resource for this request + foreach (WebServerResource *resource, m_resources) { + if (request.url().path().startsWith(resource->basePath())) { + qCDebug(dcWebServer()) << "Let the resource handle this request"; + qCDebug(dcDebugServer()) << "Request:" << request.url().toString(); + HttpReply *reply = resource->processRequest(request); + reply->setClientId(clientId); + + // Handle async replies + if (reply->type() == HttpReply::TypeAsync) { + connect(reply, &HttpReply::finished, this, &WebServer::onAsyncReplyFinished); + reply->startWait(); + } else { + sendHttpReply(reply); + reply->deleteLater(); + } + + return; + } + } + + // No resource handled this request, let the webserver itself handle it + // Verify method if (request.method() == HttpRequest::Unhandled) { HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); @@ -364,43 +418,6 @@ void WebServer::readClient() return; } - // Check if this is a debug call - if (request.url().path().startsWith("/debug")) { - // Check if debug server is enabled - if (NymeaCore::instance()->configuration()->debugServerEnabled()) { - // Verify methods - if (request.method() != HttpRequest::Get && request.method() != HttpRequest::Options) { - HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); - reply->setClientId(clientId); - reply->setHeader(HttpReply::AllowHeader, "GET, OPTIONS"); - sendHttpReply(reply); - reply->deleteLater(); - return; - } - - qCDebug(dcDebugServer()) << "Request:" << request.url().toString(); - HttpReply *reply = NymeaCore::instance()->debugServerHandler()->processDebugRequest(request.url().path(), request.urlQuery()); - reply->setClientId(clientId); - - // Handle async replies - if (reply->type() == HttpReply::TypeAsync) { - connect(reply, &HttpReply::finished, this, &WebServer::onAsyncReplyFinished); - reply->startWait(); - } else { - sendHttpReply(reply); - reply->deleteLater(); - } - return; - } else { - qCWarning(dcWebServer()) << "The debug server handler is disabled. You can enable it by adding \'debugServerEnabled=true\' in the \'nymead\' section of the nymead.conf file."; - HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound); - reply->setClientId(clientId); - sendHttpReply(reply); - reply->deleteLater(); - return; - } - } - // Check server.xml call if (request.url().path() == "/server.xml" && request.method() == HttpRequest::Get) { qCDebug(dcWebServer()) << "Server XML request call"; @@ -413,7 +430,6 @@ void WebServer::readClient() return; } - // Request for a file... if (request.method() == HttpRequest::Get) { // Check if the webinterface dir does exist, otherwise a filerequest is not relevant @@ -508,15 +524,7 @@ void WebServer::onEncrypted() { QSslSocket* socket = static_cast(sender()); qCDebug(dcWebServer()).noquote() << QString("Encrypted connection %1:%2 successfully established.").arg(socket->peerAddress().toString()).arg(socket->peerPort()); - connect(socket, &QSslSocket::readyRead, this, &WebServer::readClient); - connect(socket, &QSslSocket::disconnected, this, &WebServer::onDisconnected); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - connect(socket, &QSslSocket::errorOccurred, this, &WebServer::onError); -#else - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); -#endif - emit clientConnected(m_clientList.key(socket)); + setupClient(m_clientList.key(socket), socket); } void WebServer::onError(QAbstractSocket::SocketError error) @@ -547,6 +555,20 @@ void WebServer::onAsyncReplyFinished() reply->deleteLater(); } +void WebServer::setupClient(const QUuid &clientId, QSslSocket *socket) +{ + connect(socket, &QSslSocket::readyRead, this, &WebServer::readClient); + connect(socket, &QSslSocket::disconnected, this, &WebServer::onDisconnected); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + connect(socket, &QSslSocket::errorOccurred, this, &WebServer::onError); +#else + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); +#endif + + emit clientConnected(clientId); +} + /*! Set the configuration of this \l{WebServer} to the given \a config. * * \sa WebServerConfiguration @@ -581,7 +603,7 @@ bool WebServer::startServer() bool WebServer::stopServer() { foreach (QSslSocket *client, m_clientList.values()) - client->close(); + client->close(); close(); m_enabled = false; @@ -589,12 +611,6 @@ bool WebServer::stopServer() return true; } -WebServerConfiguration WebServer::configuration() const -{ - return m_configuration; -} - - QByteArray WebServer::createServerXmlDocument(QHostAddress address) { QByteArray uuid = NymeaCore::instance()->configuration()->serverUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); @@ -612,9 +628,9 @@ QByteArray WebServer::createServerXmlDocument(QHostAddress address) writer.writeEndElement(); // specVersion QString presentationUrl = QString("%1://%2:%3") - .arg(m_configuration.sslEnabled ? "https" : "http") - .arg(address.toString()) - .arg(m_configuration.port); + .arg(m_configuration.sslEnabled ? "https" : "http") + .arg(address.toString()) + .arg(m_configuration.port); writer.writeStartElement("device"); writer.writeTextElement("presentationURL", presentationUrl); writer.writeTextElement("deviceType", "urn:schemas-upnp-org:device:Basic:1"); @@ -815,8 +831,7 @@ void WebServerClient::removeConnection(QSslSocket *socket) */ void WebServerClient::resetTimout(QSslSocket *socket) { - QTimer *timer = nullptr; - timer = m_runningConnections.key(socket); + QTimer *timer = m_runningConnections.key(socket); if (timer) timer->start(); } diff --git a/libnymea-core/servers/webserver.h b/libnymea-core/servers/webserver.h index 9b609f31..8938b671 100644 --- a/libnymea-core/servers/webserver.h +++ b/libnymea-core/servers/webserver.h @@ -43,10 +43,11 @@ // Note: Hypertext Transfer Protocol (HTTP/1.1) from the Internet Engineering Task Force (IETF): // https://tools.ietf.org/html/rfc7231 -namespace nymeaserver { - class HttpReply; class HttpRequest; +class WebServerResource; + +namespace nymeaserver { class WebServerClient : public QObject { @@ -80,9 +81,14 @@ public: ~WebServer() override; QUrl serverUrl() const; + WebServerConfiguration configuration() const; void sendHttpReply(HttpReply *reply); + QList resources() const; + bool registerResource(WebServerResource *resource); + void unregisterResource(WebServerResource *resource); + private: QHash m_clientList; QList m_webServerClients; @@ -92,6 +98,8 @@ private: WebServerConfiguration m_configuration; QSslConfiguration m_sslConfiguration; + QHash m_resources; + bool m_enabled = false; bool verifyFile(QSslSocket *socket, const QString &fileName); @@ -99,16 +107,16 @@ private: QByteArray createServerXmlDocument(QHostAddress address); HttpReply *processIconRequest(const QString &fileName); - HttpReply *processDebugRequest(const QString &requestPath); protected: void incomingConnection(qintptr socketDescriptor) override; signals: - void httpRequestReady(const QUuid &clientId, const HttpRequest &httpRequest); void clientConnected(const QUuid &clientId); void clientDisconnected(const QUuid &clientId); + void httpRequestReady(const QUuid &clientId, const HttpRequest &httpRequest); + private slots: void readClient(); void onDisconnected(); @@ -116,12 +124,14 @@ private slots: void onError(QAbstractSocket::SocketError error); void onAsyncReplyFinished(); + void setupClient(const QUuid &clientId, QSslSocket *socket); + public slots: - void setConfiguration(const WebServerConfiguration &config); + void setConfiguration(const nymeaserver::WebServerConfiguration &config); void setServerName(const QString &serverName); bool startServer(); bool stopServer(); - WebServerConfiguration configuration() const; + }; diff --git a/libnymea-core/zwave/zwavemanager.cpp b/libnymea-core/zwave/zwavemanager.cpp index 239f8d43..a0f0af17 100644 --- a/libnymea-core/zwave/zwavemanager.cpp +++ b/libnymea-core/zwave/zwavemanager.cpp @@ -122,7 +122,7 @@ void ZWaveManager::loadZWaveNetworks() NymeaSettings settings(NymeaSettings::SettingsRoleZWave); qCDebug(dcZWave()) << "Loading ZWave networks from" << settings.fileName(); settings.beginGroup("Networks"); - foreach (const QString &uuidString, settings.childGroups()) { + foreach (const QString &uuidString, settings.childGroups()) { settings.beginGroup(uuidString); QString serialPort = settings.value("serialPort").toString(); quint32 homeId = settings.value("homeId").toULongLong(); diff --git a/libnymea/experiences/experienceplugin.h b/libnymea/experiences/experienceplugin.h index 88f25d2e..c814723d 100644 --- a/libnymea/experiences/experienceplugin.h +++ b/libnymea/experiences/experienceplugin.h @@ -56,5 +56,4 @@ private: Q_DECLARE_INTERFACE(ExperiencePlugin, "io.nymea.ExperiencePlugin") - #endif // EXPERIENCEPLUGIN_H diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index e7995063..8eb1196f 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -139,6 +139,9 @@ HEADERS += \ platform/platformupdatecontroller.h \ platform/platformzeroconfcontroller.h \ experiences/experienceplugin.h \ + webserver/httprequest.h \ + webserver/httpreply.h \ + webserver/webserverresource.h \ SOURCES += \ hardware/modbus/modbusrtuhardwareresource.cpp \ @@ -257,6 +260,9 @@ SOURCES += \ platform/platformupdatecontroller.cpp \ platform/platformzeroconfcontroller.cpp \ experiences/experienceplugin.cpp \ + webserver/httprequest.cpp \ + webserver/httpreply.cpp \ + webserver/webserverresource.cpp \ RESOURCES += \ diff --git a/libnymea-core/servers/httpreply.cpp b/libnymea/webserver/httpreply.cpp similarity index 97% rename from libnymea-core/servers/httpreply.cpp rename to libnymea/webserver/httpreply.cpp index 4d277a99..4e152175 100644 --- a/libnymea-core/servers/httpreply.cpp +++ b/libnymea/webserver/httpreply.cpp @@ -137,15 +137,12 @@ #include "httpreply.h" #include "loggingcategories.h" -#include "nymeacore.h" -#include "version.h" +#include "../version.h" #include #include #include -namespace nymeaserver { - HttpReply::HttpReply(QObject *parent) : QObject(parent), m_statusCode(HttpReply::Ok), @@ -162,7 +159,7 @@ HttpReply::HttpReply(QObject *parent) : // set known headers setHeader(HttpReply::ContentTypeHeader, "text/plain; charset=\"utf-8\";"); setHeader(HttpHeaderType::ServerHeader, "nymea/" + QByteArray(NYMEA_VERSION_STRING)); - setHeader(HttpHeaderType::DateHeader, NymeaCore::instance()->timeManager()->currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8()); + setHeader(HttpHeaderType::DateHeader, QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8()); setHeader(HttpHeaderType::CacheControlHeader, "no-cache"); setHeader(HttpHeaderType::ConnectionHeader, "Keep-Alive"); setRawHeader("Access-Control-Allow-Origin","*"); @@ -185,7 +182,7 @@ HttpReply::HttpReply(const HttpReply::HttpStatusCode &statusCode, const HttpRepl // set known / default headers setHeader(HttpReply::ContentTypeHeader, "text/plain; charset=\"utf-8\";"); setHeader(HttpHeaderType::ServerHeader, "nymea/" + QByteArray(NYMEA_VERSION_STRING)); - setHeader(HttpHeaderType::DateHeader, NymeaCore::instance()->timeManager()->currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8()); + setHeader(HttpHeaderType::DateHeader, QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8()); setHeader(HttpHeaderType::CacheControlHeader, "no-cache"); setHeader(HttpHeaderType::ConnectionHeader, "Keep-Alive"); setRawHeader("Access-Control-Allow-Origin","*"); @@ -490,5 +487,3 @@ QDebug operator<<(QDebug debug, HttpReply *httpReply) debug << qUtf8Printable(httpReply->payload()); return debug; } - -} diff --git a/libnymea-core/servers/httpreply.h b/libnymea/webserver/httpreply.h similarity index 92% rename from libnymea-core/servers/httpreply.h rename to libnymea/webserver/httpreply.h index 681652ad..8d8d6df6 100644 --- a/libnymea-core/servers/httpreply.h +++ b/libnymea/webserver/httpreply.h @@ -33,13 +33,10 @@ // Note: RFC 7231 HTTP/1.1 Semantics and Content -> http://tools.ietf.org/html/rfc7231 -namespace nymeaserver { - class HttpReply: public QObject { Q_OBJECT public: - enum HttpStatusCode { Ok = 200, Created = 201, @@ -81,9 +78,9 @@ public: HttpReply(QObject *parent = nullptr); HttpReply(const HttpStatusCode &statusCode = HttpStatusCode::Ok, const Type &type = TypeSync, QObject *parent = nullptr); - static HttpReply* createSuccessReply(); - static HttpReply* createErrorReply(const HttpReply::HttpStatusCode &statusCode); - static HttpReply* createAsyncReply(); + static HttpReply *createSuccessReply(); + static HttpReply *createErrorReply(const HttpReply::HttpStatusCode &statusCode); + static HttpReply *createAsyncReply(); void setHttpStatusCode(const HttpStatusCode &statusCode); HttpStatusCode httpStatusCode() const; @@ -116,9 +113,9 @@ public: bool timedOut() const; private: - HttpStatusCode m_statusCode; + HttpStatusCode m_statusCode = HttpReply::Ok; QByteArray m_reasonPhrase; - Type m_type; + Type m_type = HttpReply::TypeSync; QUuid m_clientId; QByteArray m_rawHeader; @@ -127,11 +124,11 @@ private: QHash m_rawHeaderList; - bool m_closeConnection; + bool m_closeConnection = false; QTimer *m_timer = nullptr; int m_timeout = 60000; - bool m_timedOut; + bool m_timedOut = false; QByteArray getHttpReasonPhrase(const HttpStatusCode &statusCode); QByteArray getHeaderType(const HttpHeaderType &headerType); @@ -149,6 +146,4 @@ signals: QDebug operator<<(QDebug debug, HttpReply *httpReply); -} - #endif // HTTPREPLY_H diff --git a/libnymea-core/servers/httprequest.cpp b/libnymea/webserver/httprequest.cpp similarity index 99% rename from libnymea-core/servers/httprequest.cpp rename to libnymea/webserver/httprequest.cpp index ac7158b0..3c53a2e6 100644 --- a/libnymea-core/servers/httprequest.cpp +++ b/libnymea/webserver/httprequest.cpp @@ -63,8 +63,6 @@ #include #include -namespace nymeaserver { - /*! Construct an empty \l{HttpRequest}. */ HttpRequest::HttpRequest() : m_rawData(QByteArray()), @@ -279,4 +277,3 @@ QDebug operator<<(QDebug debug, const HttpRequest &httpRequest) return debug; } -} diff --git a/libnymea-core/servers/httprequest.h b/libnymea/webserver/httprequest.h similarity index 95% rename from libnymea-core/servers/httprequest.h rename to libnymea/webserver/httprequest.h index 8c3f800b..6b33ea59 100644 --- a/libnymea-core/servers/httprequest.h +++ b/libnymea/webserver/httprequest.h @@ -30,8 +30,6 @@ #include #include -namespace nymeaserver { - class HttpRequest { public: @@ -86,7 +84,6 @@ private: RequestMethod getRequestMethodType(const QString &methodString); }; -QDebug operator<< (QDebug debug, const HttpRequest &httpRequest); +QDebug operator<<(QDebug debug, const HttpRequest &httpRequest); -} #endif // HTTPREQUEST_H diff --git a/libnymea/webserver/webserverresource.cpp b/libnymea/webserver/webserverresource.cpp new file mode 100644 index 00000000..8d76d944 --- /dev/null +++ b/libnymea/webserver/webserverresource.cpp @@ -0,0 +1,43 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "webserverresource.h" + +WebServerResource::WebServerResource(const QString &basePath, QObject *parent) + : QObject{parent}, + m_basePath{basePath} +{ + +} + +QString WebServerResource::basePath() const +{ + return m_basePath; +} diff --git a/libnymea/webserver/webserverresource.h b/libnymea/webserver/webserverresource.h new file mode 100644 index 00000000..6827fd91 --- /dev/null +++ b/libnymea/webserver/webserverresource.h @@ -0,0 +1,58 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef WebServerResource_H +#define WebServerResource_H + +#include +#include + +#include "httpreply.h" +#include "httprequest.h" + +class WebServerResource : public QObject +{ + Q_OBJECT +public: + explicit WebServerResource(const QString &basePath, QObject *parent = nullptr); + virtual ~WebServerResource() = default; + + QString basePath() const; + + virtual bool authenticationRequired() const = 0; + + virtual HttpReply *processRequest(const HttpRequest &request) = 0; + +protected: + QString m_basePath; + +}; + +#endif // WebServerResource_H diff --git a/plugins/mock/httpdaemon.h b/plugins/mock/httpdaemon.h index fedafb43..0b5e2377 100644 --- a/plugins/mock/httpdaemon.h +++ b/plugins/mock/httpdaemon.h @@ -41,8 +41,8 @@ class HttpDaemon : public QTcpServer public: HttpDaemon(Thing *thing, IntegrationPlugin* parent = nullptr); ~HttpDaemon(); - void incomingConnection(qintptr socket) override; + void incomingConnection(qintptr socket) override; void actionExecuted(const ActionTypeId &actionTypeId); signals: diff --git a/tests/auto/webserver/testwebserver.cpp b/tests/auto/webserver/testwebserver.cpp index c81a0a27..0ee59264 100644 --- a/tests/auto/webserver/testwebserver.cpp +++ b/tests/auto/webserver/testwebserver.cpp @@ -25,6 +25,8 @@ #include "nymeatestbase.h" #include "nymeacore.h" +#include + #include #include