// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea. * * nymea is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * nymea 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with nymea. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "jsonvalidator.h" #include "jsonrpc/jsonhandler.h" #include "loggingcategories.h" #include #include #include #include namespace nymeaserver { bool JsonValidator::checkRefs(const QVariantMap &map, const QVariantMap &api) { QVariantMap enums = api.value("enums").toMap(); QVariantMap flags = api.value("flags").toMap(); QVariantMap types = api.value("types").toMap(); foreach (const QString &key, map.keys()) { if (map.value(key).toString().startsWith("$ref:")) { QString refName = map.value(key).toString().remove("$ref:"); if (!enums.contains(refName) && !flags.contains(refName) && !types.contains(refName)) { qCWarning(dcJsonRpc()) << "Invalid reference to" << refName << "in" << key << map; return false; } } if (map.value(key).userType() == QMetaType::QVariantMap) { bool ret = checkRefs(map.value(key).toMap(), api); if (!ret) { return false; } } if (map.value(key).userType() == QMetaType::QVariantList) { foreach (const QVariant &entry, map.value(key).toList()) { if (entry.toString().startsWith("$ref:")) { QString refName = entry.toString().remove("$ref:"); if (!enums.contains(refName) && !flags.contains(refName) && !types.contains(refName)) { qCWarning(dcJsonRpc()) << "Invalid reference to" << refName << "in" << key << map; return false; } } if (entry.userType() == QMetaType::QVariantMap) { bool ret = checkRefs(map.value(key).toMap(), api); if (!ret) { return false; } } } } } return true; } JsonValidator::Result JsonValidator::validateParams(const QVariantMap ¶ms, const QString &method, const QVariantMap &api) { QVariantMap paramDefinition = api.value("methods").toMap().value(method).toMap().value("params").toMap(); m_result = validateMap(params, paramDefinition, api, QIODevice::WriteOnly); m_result.setWhere(method + ", param " + m_result.where()); return m_result; } JsonValidator::Result JsonValidator::validateReturns(const QVariantMap &returns, const QString &method, const QVariantMap &api) { QVariantMap returnsDefinition = api.value("methods").toMap().value(method).toMap().value("returns").toMap(); m_result = validateMap(returns, returnsDefinition, api, QIODevice::ReadOnly); m_result.setWhere(method + ", returns " + m_result.where()); return m_result; } JsonValidator::Result JsonValidator::validateNotificationParams(const QVariantMap ¶ms, const QString ¬ification, const QVariantMap &api) { QVariantMap paramDefinition = api.value("notifications").toMap().value(notification).toMap().value("params").toMap(); m_result = validateMap(params, paramDefinition, api, QIODevice::ReadOnly); m_result.setWhere(notification + ", param " + m_result.where()); return m_result; } JsonValidator::Result JsonValidator::result() const { return m_result; } JsonValidator::Result JsonValidator::validateMap(const QVariantMap &map, const QVariantMap &definition, const QVariantMap &api, QIODevice::OpenMode openMode) { // Make sure all required values are available foreach (const QString &key, definition.keys()) { QRegularExpression isOptional = QRegularExpression("^([a-z]:)*o:.*"); if (isOptional.match(key).hasMatch()) { continue; } QRegularExpression isReadOnly = QRegularExpression("^([a-z]:)*r:.*"); if (isReadOnly.match(key).hasMatch() && openMode.testFlag(QIODevice::WriteOnly)) { continue; } QString trimmedKey = key; trimmedKey.remove(QRegularExpression("^(o:|r:|d:)*")); if (!map.contains(trimmedKey)) { return Result(false, "Missing required key: " + key, key); } } // Make sure given values are valid foreach (const QString &key, map.keys()) { // Is the key allowed in here? QVariant expectedValue = definition.value(key); foreach (const QString &definitionKey, definition.keys()) { QRegularExpression regExp = QRegularExpression("(o:|r:|d:)" + key); if (regExp.match(definitionKey).hasMatch()) { expectedValue = definition.value(definitionKey); break; } } if (!expectedValue.isValid()) { expectedValue = definition.value("o:" + key); } if (!expectedValue.isValid()) { expectedValue = definition.value("d:" + key); } if (!expectedValue.isValid()) { return Result(false, "Invalid key: " + key); } // Validate content QVariant value = map.value(key); Result result = validateEntry(value, expectedValue, api, openMode); if (!result.success()) { result.setWhere(key + '.' + result.where()); result.setErrorString(result.errorString()); return result; } } return Result(true); } JsonValidator::Result JsonValidator::validateEntry(const QVariant &value, const QVariant &definition, const QVariantMap &api, QIODevice::OpenMode openMode) { if (definition.userType() == QMetaType::QString) { QString expectedTypeName = definition.toString(); if (expectedTypeName.startsWith("$ref:")) { QString refName = expectedTypeName; refName.remove("$ref:"); // Refs might be enums QVariantMap enums = api.value("enums").toMap(); if (enums.contains(refName)) { QVariant refDefinition = enums.value(refName); QVariantList enumList = refDefinition.toList(); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (value.metaType().id() == QMetaType::QStringList) { foreach (const QString &valueString, value.toStringList()) { if (!enumList.contains(valueString)) { return Result(false, "Expected enum value for " + refName + " but got " + value.toString()); } } } else { if (!enumList.contains(value.toString())) { return Result(false, "Expected enum value for " + refName + " but got " + value.toString()); } } #else if (!enumList.contains(value.toString())) { return Result(false, "Expected enum value for " + refName + " but got " + value.toString()); } #endif return Result(true); } // Or flags QVariantMap flags = api.value("flags").toMap(); if (flags.contains(refName)) { QVariant refDefinition = flags.value(refName); if (value.userType() != QMetaType::QVariantList && value.userType() != QMetaType::QStringList) { return Result(false, "Expected flags " + refName + " but got " + value.toString()); } QString flagEnum = refDefinition.toList().first().toString(); foreach (const QVariant &flagsEntry, value.toList()) { Result result = validateEntry(flagsEntry, flagEnum, api, openMode); if (!result.success()) { return result; } } return Result(true); } QVariantMap types = api.value("types").toMap(); QVariant refDefinition = types.value(refName); return validateEntry(value, refDefinition, api, openMode); } JsonHandler::BasicType expectedBasicType = JsonHandler::enumNameToValue(expectedTypeName); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // Any string converts fine to Uuid, but the resulting uuid might be null if (expectedBasicType == JsonHandler::Uuid && value.toUuid().isNull()) { QString typeName(value.typeName()); //qCCritical(dcJsonRpc()) << value << value.userType() << value.typeId() << value.typeName() << value.toString() << value.toUuid() << value.canConvert(QMetaType::QUuid); // Verify if this is one of our own uuid types if (typeName == "ThingId" || typeName == "EventTypeId" || typeName == "StateTypeId" || typeName == "ActionTypeId") { return Result(true); }/* else { return Result(false, "Invalid Uuid: " + value.toString()); }*/ } #else QMetaType::Type expectedVariantType = JsonHandler::basicTypeToMetaType(expectedBasicType); // Verify basic compatiblity if (expectedBasicType != JsonHandler::Variant && !value.canConvert(expectedVariantType)) { return Result(false, "Invalid value. Expected: " + definition.toString() + ", Got: " + value.toString()); } // Any string converts fine to Uuid, but the resulting uuid might be null if (expectedBasicType == JsonHandler::Uuid && value.toUuid().isNull()) { return Result(false, "Invalid Uuid: " + value.toString()); } #endif // Make sure ints are valid if (expectedBasicType == JsonHandler::Int) { bool ok; value.toLongLong(&ok); if (!ok) { return Result(false, "Invalid Int: " + value.toString()); } } // UInts if (expectedBasicType == JsonHandler::Uint) { bool ok; value.toULongLong(&ok); if (!ok) { return Result(false, "Invalid UInt: " + value.toString()); } } // Double if (expectedBasicType == JsonHandler::Double) { bool ok; value.toDouble(&ok); if (!ok) { return Result(false, "Invalid Double: " + value.toString()); } } // Color if (expectedBasicType == JsonHandler::Color) { QColor color = value.value(); if (!color.isValid()) { return Result(false, "Invalid Color: " + value.toString()); } } // Time if (expectedBasicType == JsonHandler::Time) { QTime time = QTime::fromString(value.toString(), "hh:mm"); if (!time.isValid()) { return Result(false, "Invalid Time: " + value.toString()); } } return Result(true); } if (definition.userType() == QMetaType::QVariantMap) { if (value.userType() != QMetaType::QVariantMap) { return Result(false, "Invalid value. Expected a map but received: " + value.toString()); } return validateMap(value.toMap(), definition.toMap(), api, openMode); } if (definition.userType() == QMetaType::QVariantList) { QVariantList list = definition.toList(); QVariant entryDefinition = list.first(); if (value.userType() != QMetaType::QVariantList && value.userType() != QMetaType::QStringList) { QString elementType = QString(value.typeName()).remove("QList<").remove(">"); if (elementType == "ThingId" || elementType == "EventTypeId" || elementType == "StateTypeId" || elementType == "ActionTypeId") { elementType = "Uuid"; } /*else if (elementType == "ParamList") { elementType = "Param"; } else { return Result(false, "Expected list of " + entryDefinition.toString() + " but got value of type " + value.typeName() + "\n" + QJsonDocument::fromVariant(value).toJson()); }*/ } foreach (const QVariant &entry, value.toList()) { Result result = validateEntry(entry, entryDefinition, api, openMode); if (!result.success()) { return result; } } return Result(true); } Q_ASSERT_X(false, "JsonValildator", QString("Incomplete validation. Unexpected type %1 in template").arg(definition.type()).toUtf8()); return Result(false); } }