/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 "evdashwebserverresource.h" #include "evdashsettings.h" #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience) EvDashWebServerResource::EvDashWebServerResource(QObject *parent) : WebServerResource{"/evdash", parent} { // Load users EvDashSettings settings; settings.beginGroup("Users"); foreach (const QString &username, settings.childGroups()) { UserInfo info; settings.beginGroup(username); info.username = username; info.passwordHash = settings.value("hash").toString().toUtf8(); info.passwordSalt = settings.value("salt").toString().toUtf8(); settings.endGroup(); // username m_users.insert(username, info); } settings.endGroup(); // Users qCInfo(dcEvDashExperience()) << "Loaded" << m_users.count() << "users for the dashboard."; } HttpReply *EvDashWebServerResource::processRequest(const HttpRequest &request) { qCDebug(dcEvDashExperience()) << "Process request" << request.url().toString(); const QString path = request.url().path(); if (path == basePath() + QStringLiteral("/api/login")) return handleLoginRequest(request); if (path == basePath() + QStringLiteral("/api/refresh")) return handleRefreshRequest(request); // Verify methods for static content if (request.method() != HttpRequest::Get) { HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); reply->setHeader(HttpReply::AllowHeader, "GET"); return reply; } // Redirect base url to index if (path == basePath() || path == basePath() + QStringLiteral("/")) { qCDebug(dcEvDashExperience()) << "Base URL called, redirect to main page..."; return redirectToIndex(); } // Check if this is a static file we can provide QString fileName = path; fileName.remove(basePath()); qCDebug(dcEvDashExperience()) << "Check filename" << fileName; if (verifyStaticFile(fileName)) return WebServerResource::createFileReply(":/dashboard" + fileName); // If nothing matches, redirect to main page qCWarning(dcEvDashExperience()) << "Resource for debug interface not found. Redirecting to main page..."; return redirectToIndex(); } QStringList EvDashWebServerResource::usernames() const { return m_users.keys(); } EvDashEngine::EvDashError EvDashWebServerResource::addUser(const QString &username, const QString &password) { if (m_users.keys().contains(username)) { qCWarning(dcEvDashExperience()) << "Cannot add new user. There is already a user with the username" << username; return EvDashEngine::EvDashErrorDuplicateUser; } if (password.size() < s_minimalPasswordLength) { qCWarning(dcEvDashExperience()) << "Cannot add new user. The given password is to short. The minimum size is" << s_minimalPasswordLength; return EvDashEngine::EvDashErrorBadPassword; } UserInfo info; info.username = username; info.passwordSalt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); info.passwordHash = QCryptographicHash::hash(QString(password + info.passwordSalt).toUtf8(), QCryptographicHash::Sha3_512).toBase64(); EvDashSettings settings; settings.beginGroup("Users"); settings.beginGroup(username); settings.setValue("hash", QString::fromUtf8(info.passwordHash)); settings.setValue("salt", QString::fromUtf8(info.passwordSalt)); settings.endGroup(); // username settings.endGroup(); // Users qCDebug(dcEvDashExperience()) << "Added successfully new user with username" << username; m_users.insert(username, info); emit userAdded(username); return EvDashEngine::EvDashErrorNoError; } EvDashEngine::EvDashError EvDashWebServerResource::removeUser(const QString &username) { if (!m_users.contains(username)) { qCWarning(dcEvDashExperience()) << "Cannot remove user with username" << username << "because there is no such user."; return EvDashEngine::EvDashErrorUserNotFound; } m_users.remove(username); foreach (const QString &token, m_activeTokens.keys()) { if (m_activeTokens.value(token).username == username) { qCDebug(dcEvDashExperience()) << "Revoke active token" << token << "for user" << username; m_activeTokens.remove(token); } } EvDashSettings settings; settings.beginGroup("Users"); settings.remove(username); settings.endGroup(); // Users qCDebug(dcEvDashExperience()) << "User with username" << username << "removed successfully"; emit userRemoved(username); return EvDashEngine::EvDashErrorNoError; } HttpReply *EvDashWebServerResource::redirectToIndex() { HttpReply *reply = HttpReply::createErrorReply(HttpReply::PermanentRedirect); reply->setHeader(HttpReply::LocationHeader, QString(basePath() + "/index.html").toLocal8Bit()); return reply; } bool EvDashWebServerResource::verifyStaticFile(const QString &fileName) { if (QFileInfo::exists(":/dashboard" + fileName)) return true; qCWarning(dcEvDashExperience()) << "Could not find" << fileName << "in resource files"; return false; } HttpReply *EvDashWebServerResource::handleLoginRequest(const HttpRequest &request) { if (request.method() != HttpRequest::Post) { HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); reply->setHeader(HttpReply::AllowHeader, "POST"); return reply; } QJsonParseError parseError; const QJsonDocument requestDoc = QJsonDocument::fromJson(request.payload(), &parseError); if (parseError.error != QJsonParseError::NoError || !requestDoc.isObject()) { qCWarning(dcEvDashExperience()) << "Invalid login payload" << parseError.errorString(); QJsonObject errorPayload { {QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("invalidRequest")} }; return HttpReply::createJsonReply(QJsonDocument(errorPayload), HttpReply::BadRequest); } const QJsonObject requestObject = requestDoc.object(); const QString username = requestObject.value(QStringLiteral("username")).toString(); const QString password = requestObject.value(QStringLiteral("password")).toString(); if (!verifyCredentials(username, password)) { QJsonObject response { {QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("unauthorized")} }; return HttpReply::createJsonReply(QJsonDocument(response), HttpReply::Unauthorized); } const QString token = issueToken(username); const TokenInfo tokenInfo = m_activeTokens.value(token); QJsonObject payload { {QStringLiteral("success"), true}, {QStringLiteral("token"), token}, {QStringLiteral("expiresAt"), tokenInfo.expiresAt.toString(Qt::ISODateWithMs)} }; return HttpReply::createJsonReply(QJsonDocument(payload)); } HttpReply *EvDashWebServerResource::handleRefreshRequest(const HttpRequest &request) { if (request.method() != HttpRequest::Post) { HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed); reply->setHeader(HttpReply::AllowHeader, "POST"); return reply; } QJsonParseError parseError; const QJsonDocument requestDoc = QJsonDocument::fromJson(request.payload(), &parseError); if (parseError.error != QJsonParseError::NoError || !requestDoc.isObject()) { QJsonObject errorPayload { {QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("invalidRequest")} }; return HttpReply::createJsonReply(QJsonDocument(errorPayload), HttpReply::BadRequest); } purgeExpiredTokens(); const QJsonObject requestObject = requestDoc.object(); const QString token = requestObject.value(QStringLiteral("token")).toString(); if (token.isEmpty() || !m_activeTokens.contains(token)) { QJsonObject response { {QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("unauthorized")} }; return HttpReply::createJsonReply(QJsonDocument(response), HttpReply::Unauthorized); } TokenInfo info = m_activeTokens.value(token); info.expiresAt = QDateTime::currentDateTimeUtc().addSecs(s_tokenLifetimeSeconds); m_activeTokens.insert(token, info); QJsonObject payload { {QStringLiteral("success"), true}, {QStringLiteral("token"), token}, {QStringLiteral("expiresAt"), info.expiresAt.toString(Qt::ISODateWithMs)} }; return HttpReply::createJsonReply(QJsonDocument(payload)); } bool EvDashWebServerResource::verifyCredentials(const QString &username, const QString &password) const { const UserInfo info = m_users.value(username); if (info.passwordHash != QCryptographicHash::hash(QString(password + info.passwordSalt).toUtf8(), QCryptographicHash::Sha3_512).toBase64()) { qCWarning(dcEvDashExperience()) << "Authentication error for user:" << username; return false; } return true; } QString EvDashWebServerResource::issueToken(const QString &username) { purgeExpiredTokens(); const QString token = QUuid::createUuid().toString(QUuid::WithoutBraces); TokenInfo info; info.username = username; info.expiresAt = QDateTime::currentDateTimeUtc().addSecs(s_tokenLifetimeSeconds); m_activeTokens.insert(token, info); return token; } bool EvDashWebServerResource::validateToken(const QString &token) { purgeExpiredTokens(); auto it = m_activeTokens.find(token); if (it == m_activeTokens.end()) return false; if (it->expiresAt < QDateTime::currentDateTimeUtc()) { m_activeTokens.erase(it); return false; } return true; } void EvDashWebServerResource::purgeExpiredTokens() { const QDateTime now = QDateTime::currentDateTimeUtc(); auto it = m_activeTokens.begin(); while (it != m_activeTokens.end()) { if (it->expiresAt < now) it = m_activeTokens.erase(it); else ++it; } }