From 3d884e7c069e04160fc7cd489e4520df03898e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 24 Oct 2025 16:25:59 +0200 Subject: [PATCH 1/8] Add initial test for thing based authentication --- libnymea-core/usermanager/usermanager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index 11a5b40f..1be4e0c1 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -348,6 +348,7 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types } } + QString scopesString = Types::scopesToStringList(scopes).join(','); QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(','); From 88d1e4ce520db0eb6700b4b3a2ac86b539811bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 3 Nov 2025 15:17:26 +0100 Subject: [PATCH 2/8] Add thing added and removed logic depending on users thing permission --- libnymea-core/usermanager/usermanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index 1be4e0c1..88882f5a 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -348,7 +348,6 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types } } - QString scopesString = Types::scopesToStringList(scopes).join(','); QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(','); @@ -632,6 +631,7 @@ bool UserManager::verifyToken(const QByteArray &token) return false; } + //qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString(); return true; } 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 3/8] 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 From 5d035677e046db25a91eeceaea4bc328395d10f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 6 Nov 2025 21:15:26 +0100 Subject: [PATCH 4/8] Add WebServerResource support for experience plugins --- libnymea-core/experiences/experiencemanager.cpp | 17 ++++++++++++++--- libnymea-core/experiences/experiencemanager.h | 6 +++++- libnymea-core/nymeacore.cpp | 2 +- libnymea-core/servermanager.cpp | 6 ++++-- libnymea/experiences/experienceplugin.cpp | 7 +++++++ libnymea/experiences/experienceplugin.h | 9 +++++++-- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/libnymea-core/experiences/experiencemanager.cpp b/libnymea-core/experiences/experiencemanager.cpp index 479ada50..8b0fb746 100644 --- a/libnymea-core/experiences/experiencemanager.cpp +++ b/libnymea-core/experiences/experiencemanager.cpp @@ -24,6 +24,7 @@ #include "experiencemanager.h" #include "experiences/experienceplugin.h" +#include "servermanager.h" #include "jsonrpc/jsonrpcserverimplementation.h" #include "loggingcategories.h" @@ -35,9 +36,12 @@ namespace nymeaserver { -ExperienceManager::ExperienceManager(ThingManager *thingManager, JsonRPCServer *jsonRpcServer, QObject *parent) : QObject(parent), - m_thingManager(thingManager), - m_jsonRpcServer(jsonRpcServer) +ExperienceManager::ExperienceManager(ThingManager *thingManager, JsonRPCServer *jsonRpcServer, ServerManager *serverManager, QObject *parent) : + QObject{parent}, + m_thingManager{thingManager}, + m_jsonRpcServer{jsonRpcServer}, + m_serverManager{serverManager} + { staticMetaObject.invokeMethod(this, "loadPlugins", Qt::QueuedConnection); } @@ -122,6 +126,9 @@ void ExperienceManager::loadExperiencePlugin(const QString &file) plugin->setParent(this); plugin->initPlugin(m_thingManager, m_jsonRpcServer); + if (plugin->webServerResource()) { + m_serverManager->registerWebServerResource(plugin->webServerResource()); + } } void ExperienceManager::loadExperiencePlugin(ExperiencePlugin *experiencePlugin) @@ -130,6 +137,10 @@ void ExperienceManager::loadExperiencePlugin(ExperiencePlugin *experiencePlugin) m_plugins.append(experiencePlugin); experiencePlugin->setParent(this); experiencePlugin->initPlugin(m_thingManager, m_jsonRpcServer); + + if (experiencePlugin->webServerResource()) { + m_serverManager->registerWebServerResource(experiencePlugin->webServerResource()); + } } } diff --git a/libnymea-core/experiences/experiencemanager.h b/libnymea-core/experiences/experiencemanager.h index f3a0ad86..47eb3439 100644 --- a/libnymea-core/experiences/experiencemanager.h +++ b/libnymea-core/experiences/experiencemanager.h @@ -33,11 +33,13 @@ class ThingManager; namespace nymeaserver { +class ServerManager; + class ExperienceManager : public QObject { Q_OBJECT public: - explicit ExperienceManager(ThingManager *thingManager, JsonRPCServer *jsonRpcServer, QObject *parent = nullptr); + explicit ExperienceManager(ThingManager *thingManager, JsonRPCServer *jsonRpcServer, ServerManager *serverManager, QObject *parent = nullptr); QList plugins() const; @@ -50,6 +52,8 @@ private slots: private: ThingManager *m_thingManager = nullptr; JsonRPCServer *m_jsonRpcServer = nullptr; + ServerManager *m_serverManager = nullptr; + QList m_plugins; QStringList pluginSearchDirs() const; diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index 85600fca..30d1191d 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -159,7 +159,7 @@ void NymeaCore::init(const QStringList &additionalInterfaces, bool disableLogEng m_serverManager->jsonServer()->registerHandler(new DebugHandler(m_serverManager->jsonServer())); qCDebug(dcCore()) << "Loading experiences"; - m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), this); + m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), m_serverManager, this); connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName); connect(m_thingManager, &ThingManagerImplementation::loaded, this, &NymeaCore::thingManagerLoaded); diff --git a/libnymea-core/servermanager.cpp b/libnymea-core/servermanager.cpp index d316b880..4b21671b 100644 --- a/libnymea-core/servermanager.cpp +++ b/libnymea-core/servermanager.cpp @@ -284,7 +284,6 @@ bool ServerManager::registerWebServerResource(WebServerResource *resource) } m_webServerResources.insert(resource->basePath(), resource); - foreach (WebServer *webserver, m_webServers) webserver->registerResource(resource); @@ -379,7 +378,6 @@ 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(); @@ -399,6 +397,10 @@ void ServerManager::webServerConfigurationRemoved(const QString &id) } WebServer *server = m_webServers.take(id); + + foreach (WebServerResource *resource, m_webServerResources) + server->unregisterResource(resource); + unregisterZeroConfService(id, "http"); server->stopServer(); server->deleteLater(); diff --git a/libnymea/experiences/experienceplugin.cpp b/libnymea/experiences/experienceplugin.cpp index f32df07a..504fb166 100644 --- a/libnymea/experiences/experienceplugin.cpp +++ b/libnymea/experiences/experienceplugin.cpp @@ -36,6 +36,13 @@ void ExperiencePlugin::init() } +/*! This method will can be used to provide a web server resource to the core. + Override this method and provide an object. The resource will be added to the webserver after the init() method has been called. */ +WebServerResource *ExperiencePlugin::webServerResource() const +{ + return nullptr; +} + /*! Returns a pointer to the DeviceManager. The pointer won't be valid unless init() has been called. */ ThingManager *ExperiencePlugin::thingManager() { diff --git a/libnymea/experiences/experienceplugin.h b/libnymea/experiences/experienceplugin.h index c814723d..a4b4ee79 100644 --- a/libnymea/experiences/experienceplugin.h +++ b/libnymea/experiences/experienceplugin.h @@ -29,8 +29,10 @@ class ThingManager; class JsonRPCServer; +class WebServerResource; namespace nymeaserver { + class ExperienceManager; } class ExperiencePlugin : public QObject @@ -38,12 +40,15 @@ class ExperiencePlugin : public QObject Q_OBJECT public: explicit ExperiencePlugin(QObject *parent = nullptr); + virtual ~ExperiencePlugin() = default; virtual void init() = 0; + virtual WebServerResource *webServerResource() const; + protected: - ThingManager* thingManager(); - JsonRPCServer* jsonRpcServer(); + ThingManager *thingManager(); + JsonRPCServer *jsonRpcServer(); private: friend class nymeaserver::ExperienceManager; From 2a309ce59657729c0d282aff5d03a3fe3845b8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 7 Nov 2025 20:59:30 +0100 Subject: [PATCH 5/8] Move static file reply creation to the resource --- libnymea-core/servers/webserver.cpp | 38 +++------------------ libnymea/webserver/webserverresource.cpp | 43 ++++++++++++++++++++++++ libnymea/webserver/webserverresource.h | 2 ++ 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/libnymea-core/servers/webserver.cpp b/libnymea-core/servers/webserver.cpp index 7bb9ff39..e983971f 100644 --- a/libnymea-core/servers/webserver.cpp +++ b/libnymea-core/servers/webserver.cpp @@ -447,42 +447,12 @@ void WebServer::readClient() if (!verifyFile(socket, path)) return; - QFile file(path); - if (file.open(QFile::ReadOnly)) { - qCDebug(dcWebServer()) << "Load file" << file.fileName(); - HttpReply *reply = HttpReply::createSuccessReply(); - // Check content type - if (file.fileName().endsWith(".html")) { - reply->setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";"); - } else if (file.fileName().endsWith(".css")) { - reply->setHeader(HttpReply::ContentTypeHeader, "text/css; charset=\"utf-8\";"); - } else if (file.fileName().endsWith(".pdf")) { - reply->setHeader(HttpReply::ContentTypeHeader, "application/pdf"); - } else if (file.fileName().endsWith(".js")) { - reply->setHeader(HttpReply::ContentTypeHeader, "text/javascript; charset=\"utf-8\";"); - } else if (file.fileName().endsWith(".ttf")) { - reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-ttf"); - } else if (file.fileName().endsWith(".eot")) { - reply->setHeader(HttpReply::ContentTypeHeader, "application/vnd.ms-fontobject"); - } else if (file.fileName().endsWith(".woff")) { - reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-woff"); - } else if (file.fileName().endsWith(".jpg") || file.fileName().endsWith(".jpeg")) { - reply->setHeader(HttpReply::ContentTypeHeader, "image/jpeg"); - } else if (file.fileName().endsWith(".png") || file.fileName().endsWith(".PNG")) { - reply->setHeader(HttpReply::ContentTypeHeader, "image/png"); - } else if (file.fileName().endsWith(".ico")) { - reply->setHeader(HttpReply::ContentTypeHeader, "image/x-icon"); - } else if (file.fileName().endsWith(".svg")) { - reply->setHeader(HttpReply::ContentTypeHeader, "image/svg+xml; charset=\"utf-8\";"); - } + HttpReply *reply = WebServerResource::createFileReply(path); + reply->setClientId(clientId); + sendHttpReply(reply); + reply->deleteLater(); - reply->setPayload(file.readAll()); - reply->setClientId(clientId); - sendHttpReply(reply); - reply->deleteLater(); - return; - } } // Reject everything else... diff --git a/libnymea/webserver/webserverresource.cpp b/libnymea/webserver/webserverresource.cpp index 8d76d944..520621ed 100644 --- a/libnymea/webserver/webserverresource.cpp +++ b/libnymea/webserver/webserverresource.cpp @@ -29,6 +29,9 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "webserverresource.h" +#include "loggingcategories.h" + +#include WebServerResource::WebServerResource(const QString &basePath, QObject *parent) : QObject{parent}, @@ -41,3 +44,43 @@ QString WebServerResource::basePath() const { return m_basePath; } + +HttpReply *WebServerResource::createFileReply(const QString fileName) +{ + qCDebug(dcWebServer()) << "Create file reply for" << fileName; + HttpReply *reply = HttpReply::createSuccessReply(); + + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) { + qCWarning(dcWebServer()) << "Unable to generate file reply. The file" << fileName << "could not be opened. Respond with 403 Forbidden."; + return HttpReply::createErrorReply(HttpReply::Forbidden); + } + + // Check content type + if (file.fileName().endsWith(".html")) { + reply->setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";"); + } else if (file.fileName().endsWith(".css")) { + reply->setHeader(HttpReply::ContentTypeHeader, "text/css; charset=\"utf-8\";"); + } else if (file.fileName().endsWith(".pdf")) { + reply->setHeader(HttpReply::ContentTypeHeader, "application/pdf"); + } else if (file.fileName().endsWith(".js")) { + reply->setHeader(HttpReply::ContentTypeHeader, "text/javascript; charset=\"utf-8\";"); + } else if (file.fileName().endsWith(".ttf")) { + reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-ttf"); + } else if (file.fileName().endsWith(".eot")) { + reply->setHeader(HttpReply::ContentTypeHeader, "application/vnd.ms-fontobject"); + } else if (file.fileName().endsWith(".woff")) { + reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-woff"); + } else if (file.fileName().endsWith(".jpg") || file.fileName().endsWith(".jpeg")) { + reply->setHeader(HttpReply::ContentTypeHeader, "image/jpeg"); + } else if (file.fileName().endsWith(".png") || file.fileName().endsWith(".PNG")) { + reply->setHeader(HttpReply::ContentTypeHeader, "image/png"); + } else if (file.fileName().endsWith(".ico")) { + reply->setHeader(HttpReply::ContentTypeHeader, "image/x-icon"); + } else if (file.fileName().endsWith(".svg")) { + reply->setHeader(HttpReply::ContentTypeHeader, "image/svg+xml; charset=\"utf-8\";"); + } + + reply->setPayload(file.readAll()); + return reply; +} diff --git a/libnymea/webserver/webserverresource.h b/libnymea/webserver/webserverresource.h index 6827fd91..0f29fc83 100644 --- a/libnymea/webserver/webserverresource.h +++ b/libnymea/webserver/webserverresource.h @@ -50,6 +50,8 @@ public: virtual HttpReply *processRequest(const HttpRequest &request) = 0; + static HttpReply *createFileReply(const QString fileName); + protected: QString m_basePath; From 29ba4625e813a260604bbe3410585aa1fee5efdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 12 Nov 2025 10:51:19 +0100 Subject: [PATCH 6/8] Add resource enable/disable handling --- libnymea-core/debugserverhandler.cpp | 9 +++------ libnymea-core/debugserverhandler.h | 2 -- libnymea-core/servers/webserver.cpp | 9 ++++++++- libnymea/integrations/thing.cpp | 9 +++++++++ libnymea/integrations/thing.h | 1 + libnymea/webserver/httpreply.cpp | 17 +++++++++++++++++ libnymea/webserver/httpreply.h | 5 +++++ libnymea/webserver/webserverresource.cpp | 18 +++++++++++++++++- libnymea/webserver/webserverresource.h | 7 ++++++- 9 files changed, 66 insertions(+), 11 deletions(-) diff --git a/libnymea-core/debugserverhandler.cpp b/libnymea-core/debugserverhandler.cpp index baa5bebf..105413a6 100644 --- a/libnymea-core/debugserverhandler.cpp +++ b/libnymea-core/debugserverhandler.cpp @@ -51,14 +51,9 @@ DebugServerHandler::DebugServerHandler(QObject *parent) : onDebugServerEnabledChanged(NymeaCore::instance()->configuration()->debugServerEnabled()); } -bool DebugServerHandler::authenticationRequired() const -{ - return false; -} - HttpReply *DebugServerHandler::processRequest(const HttpRequest &request) { - if (NymeaCore::instance()->configuration()->debugServerEnabled()) { + if (m_enabled) { // Verify methods if (request.method() != HttpRequest::Get && request.method() != HttpRequest::Options) { @@ -658,6 +653,8 @@ void DebugServerHandler::onDebugServerEnabledChanged(bool enabled) m_websocketServer = nullptr; } } + + setEnabled(enabled); } void DebugServerHandler::onWebsocketClientConnected() diff --git a/libnymea-core/debugserverhandler.h b/libnymea-core/debugserverhandler.h index c3928110..dd9777f6 100644 --- a/libnymea-core/debugserverhandler.h +++ b/libnymea-core/debugserverhandler.h @@ -43,8 +43,6 @@ class DebugServerHandler : public WebServerResource public: explicit DebugServerHandler(QObject *parent = nullptr); - bool authenticationRequired() const override; - HttpReply *processRequest(const HttpRequest &request) override; private: diff --git a/libnymea-core/servers/webserver.cpp b/libnymea-core/servers/webserver.cpp index e983971f..e01910fd 100644 --- a/libnymea-core/servers/webserver.cpp +++ b/libnymea-core/servers/webserver.cpp @@ -379,7 +379,14 @@ 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"; + if (!resource->enabled()) { + qCDebug(dcWebServer()) << "The corresponding resource exists but is not enabled. Respond with 404 Not Found."; + HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound); + reply->setClientId(clientId); + sendHttpReply(reply); + reply->deleteLater(); + } + qCDebug(dcDebugServer()) << "Request:" << request.url().toString(); HttpReply *reply = resource->processRequest(request); reply->setClientId(clientId); diff --git a/libnymea/integrations/thing.cpp b/libnymea/integrations/thing.cpp index c70f8a69..7963fddc 100644 --- a/libnymea/integrations/thing.cpp +++ b/libnymea/integrations/thing.cpp @@ -317,6 +317,15 @@ States Thing::states() const return m_states; } + +/*! Returns true, a \l{Param} with the given \a paramTypeId exists for this thing. */ +bool Thing::hasParam(const QString ¶mName) const +{ + ParamTypeId paramTypeId = m_thingClass.paramTypes().findByName(paramName).id(); + return m_params.hasParam(paramTypeId); +} + + /*! Returns true, a \l{Param} with the given \a paramTypeId exists for this thing. */ bool Thing::hasParam(const ParamTypeId ¶mTypeId) const { diff --git a/libnymea/integrations/thing.h b/libnymea/integrations/thing.h index 3c862213..c28e3337 100644 --- a/libnymea/integrations/thing.h +++ b/libnymea/integrations/thing.h @@ -112,6 +112,7 @@ public: ParamList params() const; bool hasParam(const ParamTypeId ¶mTypeId) const; + bool hasParam(const QString ¶mName) const; void setParams(const ParamList ¶ms); QVariant paramValue(const ParamTypeId ¶mTypeId) const; diff --git a/libnymea/webserver/httpreply.cpp b/libnymea/webserver/httpreply.cpp index 4e152175..a76d8a4a 100644 --- a/libnymea/webserver/httpreply.cpp +++ b/libnymea/webserver/httpreply.cpp @@ -197,6 +197,14 @@ HttpReply *HttpReply::createSuccessReply() return reply; } +HttpReply *HttpReply::createJsonReply(const QJsonDocument &jsonDoc, const HttpReply::HttpStatusCode &statusCode) +{ + HttpReply *reply = new HttpReply(statusCode, HttpReply::TypeSync); + reply->setPayload(jsonDoc.toJson(QJsonDocument::Compact)); + reply->setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + return reply; +} + HttpReply *HttpReply::createErrorReply(const HttpReply::HttpStatusCode &statusCode) { HttpReply *reply = new HttpReply(statusCode, HttpReply::TypeSync); @@ -389,6 +397,12 @@ QByteArray HttpReply::getHttpReasonPhrase(const HttpReply::HttpStatusCode &statu case BadRequest: response = QString("Bad Request").toUtf8(); break; + case Unauthorized: + response = QString("Unauthorized").toUtf8(); + break; + case PaymentRequired: + response = QString("Payment required").toUtf8(); + break; case Forbidden: response = QString("Forbidden").toUtf8(); break; @@ -398,6 +412,9 @@ QByteArray HttpReply::getHttpReasonPhrase(const HttpReply::HttpStatusCode &statu case MethodNotAllowed: response = QString("Method Not Allowed").toUtf8(); break; + case NotAcceptable: + response = QString("Not Acceptable").toUtf8(); + break; case RequestTimeout: response = QString("Request Timeout").toUtf8(); break; diff --git a/libnymea/webserver/httpreply.h b/libnymea/webserver/httpreply.h index 8d8d6df6..c584a6a4 100644 --- a/libnymea/webserver/httpreply.h +++ b/libnymea/webserver/httpreply.h @@ -30,6 +30,7 @@ #include #include #include +#include // Note: RFC 7231 HTTP/1.1 Semantics and Content -> http://tools.ietf.org/html/rfc7231 @@ -45,9 +46,12 @@ public: Found = 302, PermanentRedirect = 308, BadRequest = 400, + Unauthorized = 401, + PaymentRequired = 402, Forbidden = 403, NotFound = 404, MethodNotAllowed = 405, + NotAcceptable = 406, RequestTimeout = 408, Conflict = 409, InternalServerError = 500, @@ -80,6 +84,7 @@ public: static HttpReply *createSuccessReply(); static HttpReply *createErrorReply(const HttpReply::HttpStatusCode &statusCode); + static HttpReply *createJsonReply(const QJsonDocument &jsonDoc, const HttpReply::HttpStatusCode &statusCode = HttpStatusCode::Ok); static HttpReply *createAsyncReply(); void setHttpStatusCode(const HttpStatusCode &statusCode); diff --git a/libnymea/webserver/webserverresource.cpp b/libnymea/webserver/webserverresource.cpp index 520621ed..3c4aa720 100644 --- a/libnymea/webserver/webserverresource.cpp +++ b/libnymea/webserver/webserverresource.cpp @@ -45,10 +45,24 @@ QString WebServerResource::basePath() const return m_basePath; } +bool WebServerResource::enabled() const +{ + return m_enabled; +} + +void WebServerResource::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + qCDebug(dcWebServer()) << "The resource" << m_basePath << "is now" << (enabled ? "enabled" : "disabled"); + m_enabled = enabled; + emit enabledChanged(m_enabled); +} + HttpReply *WebServerResource::createFileReply(const QString fileName) { qCDebug(dcWebServer()) << "Create file reply for" << fileName; - HttpReply *reply = HttpReply::createSuccessReply(); QFile file(fileName); if (!file.open(QFile::ReadOnly)) { @@ -56,6 +70,8 @@ HttpReply *WebServerResource::createFileReply(const QString fileName) return HttpReply::createErrorReply(HttpReply::Forbidden); } + HttpReply *reply = HttpReply::createSuccessReply(); + // Check content type if (file.fileName().endsWith(".html")) { reply->setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";"); diff --git a/libnymea/webserver/webserverresource.h b/libnymea/webserver/webserverresource.h index 0f29fc83..879e84fb 100644 --- a/libnymea/webserver/webserverresource.h +++ b/libnymea/webserver/webserverresource.h @@ -46,14 +46,19 @@ public: QString basePath() const; - virtual bool authenticationRequired() const = 0; + bool enabled() const; + void setEnabled(bool enabled); virtual HttpReply *processRequest(const HttpRequest &request) = 0; static HttpReply *createFileReply(const QString fileName); +signals: + void enabledChanged(bool enabled); + protected: QString m_basePath; + bool m_enabled = true; }; From ccc94a0e24a54db56841969bab613412209ff3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 14 Nov 2025 09:53:30 +0100 Subject: [PATCH 7/8] Fix development package install for webserver classes --- debian-qt5/libnymea-dev.install.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian-qt5/libnymea-dev.install.in b/debian-qt5/libnymea-dev.install.in index 32ff475d..7748efc8 100644 --- a/debian-qt5/libnymea-dev.install.in +++ b/debian-qt5/libnymea-dev.install.in @@ -5,9 +5,10 @@ usr/include/nymea/experiences/* usr/include/nymea/hardware/* usr/include/nymea/integrations/* usr/include/nymea/jsonrpc/* +usr/include/nymea/logging/* usr/include/nymea/network/* usr/include/nymea/platform/* usr/include/nymea/time/* usr/include/nymea/types/* -usr/include/nymea/logging/* +usr/include/nymea/webserver/* usr/lib/@DEB_HOST_MULTIARCH@/pkgconfig/nymea.pc From ab9df2711a103d09207766ff28db07a5e667a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 14 Nov 2025 09:54:07 +0100 Subject: [PATCH 8/8] Fix webserver disabled resource response flow --- libnymea-core/libnymea-core.pro | 1 - libnymea-core/servers/webserver.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index 1a5665cf..849968f4 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -69,7 +69,6 @@ HEADERS += nymeacore.h \ logging/logengineinfluxdb.h \ scriptengine/scriptthing.h \ scriptengine/scriptthings.h \ - servers/webserverresource.h \ zwave/zwavedevicedatabase.h \ zwave/zwavemanagerreply.h \ zwave/zwavenodeimplementation.h \ diff --git a/libnymea-core/servers/webserver.cpp b/libnymea-core/servers/webserver.cpp index e01910fd..25eaae5b 100644 --- a/libnymea-core/servers/webserver.cpp +++ b/libnymea-core/servers/webserver.cpp @@ -385,6 +385,7 @@ void WebServer::readClient() reply->setClientId(clientId); sendHttpReply(reply); reply->deleteLater(); + return; } qCDebug(dcDebugServer()) << "Request:" << request.url().toString();