/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2017 Simon Stuerz * * * * This file is part of nymea:app. * * * * nymea:app is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, version 3 of the License. * * * * nymea:app 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 nymea:app. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "jsonrpcclient.h" #include "connection/nymeaconnection.h" #include "types/param.h" #include "types/params.h" #include #include #include #include #include #include #include JsonRpcClient::JsonRpcClient(NymeaConnection *connection, QObject *parent) : JsonHandler(parent), m_id(0), m_connection(connection) { connect(m_connection, &NymeaConnection::connectedChanged, this, &JsonRpcClient::onInterfaceConnectedChanged); connect(m_connection, &NymeaConnection::dataAvailable, this, &JsonRpcClient::dataReceived); registerNotificationHandler(this, "notificationReceived"); } QString JsonRpcClient::nameSpace() const { return QStringLiteral("JSONRPC"); } void JsonRpcClient::registerNotificationHandler(JsonHandler *handler, const QString &method) { if (m_notificationHandlers.contains(handler->nameSpace())) { qWarning() << "Already have a notification handler for" << handler->nameSpace(); return; } m_notificationHandlers.insert(handler->nameSpace(), qMakePair(handler, method)); } int JsonRpcClient::sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller, const QString &callbackMethod) { JsonRpcReply *reply = createReply(method, params, caller, callbackMethod); m_replies.insert(reply->commandId(), reply); sendRequest(reply->requestMap()); return reply->commandId(); } int JsonRpcClient::sendCommand(const QString &method, QObject *caller, const QString &callbackMethod) { return sendCommand(method, QVariantMap(), caller, callbackMethod); } void JsonRpcClient::setNotificationsEnabled(bool enabled) { QVariantMap params; params.insert("enabled", enabled); JsonRpcReply *reply = createReply("JSONRPC.SetNotificationStatus", params, this, "setNotificationsEnabledResponse"); m_replies.insert(reply->commandId(), reply); sendRequest(reply->requestMap()); } void JsonRpcClient::getCloudConnectionStatus() { JsonRpcReply *reply = createReply("JSONRPC.IsCloudConnected", QVariantMap(), this, "isCloudConnectedReply"); m_replies.insert(reply->commandId(), reply); sendRequest(reply->requestMap()); } void JsonRpcClient::setNotificationsEnabledResponse(const QVariantMap ¶ms) { qDebug() << "Notifications enabled:" << params; m_connected = true; emit connectedChanged(true); } void JsonRpcClient::notificationReceived(const QVariantMap &data) { qDebug() << "Notification received:" << data; if (data.value("notification").toString() == "JSONRPC.PushButtonAuthFinished") { qDebug() << "Push button auth finished."; if (data.value("params").toMap().value("transactionId").toInt() != m_pendingPushButtonTransaction) { qDebug() << "This push button transaction is not what we're waiting for..."; return; } m_pendingPushButtonTransaction = -1; if (data.value("params").toMap().value("success").toBool()) { qDebug() << "Push button auth succeeded"; m_token = data.value("params").toMap().value("token").toByteArray(); QSettings settings; settings.beginGroup("jsonTokens"); settings.setValue(m_serverUuid, m_token); settings.endGroup(); emit authenticationRequiredChanged(); setNotificationsEnabled(true); } else { emit pushButtonAuthFailed(); } return; } if (data.value("notification").toString() == "JSONRPC.CloudConnectedChanged") { QMetaEnum connectionStateEnum = QMetaEnum::fromType(); m_cloudConnectionState = static_cast(connectionStateEnum.keyToValue(data.value("params").toMap().value("connectionState").toByteArray().data())); emit cloudConnectionStateChanged(); return; } qDebug() << "JsonRpcClient: Unhandled notification received" << data; } void JsonRpcClient::isCloudConnectedReply(const QVariantMap &data) { qDebug() << "Cloud is connected" << data; QMetaEnum connectionStateEnum = QMetaEnum::fromType(); m_cloudConnectionState = static_cast(connectionStateEnum.keyToValue(data.value("params").toMap().value("connectionState").toByteArray().data())); emit cloudConnectionStateChanged(); } void JsonRpcClient::setupRemoteAccessReply(const QVariantMap &data) { qDebug() << "Setup Remote Access reply" << data; } void JsonRpcClient::deployCertificateReply(const QVariantMap &data) { qDebug() << "deploy certificate reply:" << data; } bool JsonRpcClient::connected() const { return m_connected; } bool JsonRpcClient::initialSetupRequired() const { return m_initialSetupRequired; } bool JsonRpcClient::authenticationRequired() const { return m_authenticationRequired && m_token.isEmpty(); } bool JsonRpcClient::pushButtonAuthAvailable() const { return m_pushButtonAuthAvailable; } JsonRpcClient::CloudConnectionState JsonRpcClient::cloudConnectionState() const { return m_cloudConnectionState; } void JsonRpcClient::deployCertificate(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint) { QVariantMap params; params.insert("rootCA", rootCA); params.insert("certificatePEM", certificate); params.insert("publicKey", publicKey); params.insert("privateKey", privateKey); params.insert("endpoint", endpoint); sendCommand("JSONRPC.SetupCloudConnection", params, this, "deployCertificateReply"); } QString JsonRpcClient::serverVersion() const { return m_serverVersion; } QString JsonRpcClient::jsonRpcVersion() const { return m_jsonRpcVersion.toString(); } QString JsonRpcClient::serverUuid() const { return m_serverUuid; } int JsonRpcClient::createUser(const QString &username, const QString &password) { QVariantMap params; params.insert("username", username); params.insert("password", password); JsonRpcReply* reply = createReply("JSONRPC.CreateUser", params, this, "processCreateUser"); m_replies.insert(reply->commandId(), reply); m_connection->sendData(QJsonDocument::fromVariant(reply->requestMap()).toJson()); return reply->commandId(); } int JsonRpcClient::authenticate(const QString &username, const QString &password, const QString &deviceName) { QVariantMap params; params.insert("username", username); params.insert("password", password); params.insert("deviceName", deviceName); JsonRpcReply* reply = createReply("JSONRPC.Authenticate", params, this, "processAuthenticate"); m_replies.insert(reply->commandId(), reply); m_connection->sendData(QJsonDocument::fromVariant(reply->requestMap()).toJson()); return reply->commandId(); } int JsonRpcClient::requestPushButtonAuth(const QString &deviceName) { qDebug() << "Requesting push button auth for device:" << deviceName; QVariantMap params; params.insert("deviceName", deviceName); JsonRpcReply *reply = createReply("JSONRPC.RequestPushButtonAuth", params, this, "processRequestPushButtonAuth"); m_replies.insert(reply->commandId(), reply); m_connection->sendData(QJsonDocument::fromVariant(reply->requestMap()).toJson()); return reply->commandId(); } void JsonRpcClient::setupRemoteAccess(const QString &idToken, const QString &userId) { qDebug() << "Calling SetupRemoteAccess"; QVariantMap params; params.insert("idToken", idToken); params.insert("userId", userId); sendCommand("JSONRPC.SetupRemoteAccess", params, this, "setupRemoteAccessReply"); } bool JsonRpcClient::ensureServerVersion(const QString &jsonRpcVersion) { return QVersionNumber(m_jsonRpcVersion) >= QVersionNumber::fromString(jsonRpcVersion); } void JsonRpcClient::processAuthenticate(const QVariantMap &data) { if (data.value("status").toString() == "success" && data.value("params").toMap().value("success").toBool()) { qDebug() << "authentication successful"; m_token = data.value("params").toMap().value("token").toByteArray(); QSettings settings; settings.beginGroup("jsonTokens"); settings.setValue(m_serverUuid, m_token); settings.endGroup(); emit authenticationRequiredChanged(); setNotificationsEnabled(true); } else { qWarning() << "Authentication failed"; emit authenticationFailed(); } } void JsonRpcClient::processCreateUser(const QVariantMap &data) { qDebug() << "create user response:" << data; if (data.value("status").toString() == "success" && data.value("params").toMap().value("error").toString() == "UserErrorNoError") { m_initialSetupRequired = false; emit initialSetupRequiredChanged(); } else { qDebug() << "Emitting create user failed"; emit createUserFailed(data.value("params").toMap().value("error").toString()); } } void JsonRpcClient::processRequestPushButtonAuth(const QVariantMap &data) { qDebug() << "requestPushButtonAuth response" << data; if (data.value("status").toString() == "success" && data.value("params").toMap().value("success").toBool()) { m_pendingPushButtonTransaction = data.value("params").toMap().value("transactionId").toInt(); } else { emit pushButtonAuthFailed(); } } JsonRpcReply *JsonRpcClient::createReply(const QString &method, const QVariantMap ¶ms, QObject* caller, const QString &callback) { QStringList callParts = method.split('.'); if (callParts.count() != 2) { qWarning() << "Invalid method. Must be Namespace.Method"; return nullptr; } m_id++; return new JsonRpcReply(m_id, callParts.first(), callParts.last(), params, caller, callback); } void JsonRpcClient::sendRequest(const QVariantMap &request) { QVariantMap newRequest = request; newRequest.insert("token", m_token); // qDebug() << "Sending request" << qUtf8Printable(QJsonDocument::fromVariant(newRequest).toJson()); m_connection->sendData(QJsonDocument::fromVariant(newRequest).toJson(QJsonDocument::Compact) + "\n"); } void JsonRpcClient::onInterfaceConnectedChanged(bool connected) { if (!connected) { m_initialSetupRequired = false; m_authenticationRequired = false; if (m_connected) { m_connected = false; emit connectedChanged(false); } } else { QVariantMap request; request.insert("id", 0); qDebug() << "Connected. Starting JSONRPC Handshake"; request.insert("method", "JSONRPC.Hello"); sendRequest(request); } } void JsonRpcClient::dataReceived(const QByteArray &data) { // qDebug() << "JsonRpcClient: received data:" << qUtf8Printable(data); m_receiveBuffer.append(data); int splitIndex = m_receiveBuffer.indexOf("}\n{") + 1; if (splitIndex <= 0) { splitIndex = m_receiveBuffer.length(); } QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(m_receiveBuffer.left(splitIndex), &error); if (error.error != QJsonParseError::NoError) { // qWarning() << "Could not parse json data from nymea" << m_receiveBuffer.left(splitIndex) << error.errorString(); return; } // qDebug() << "received response" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented)); m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 1); if (!m_receiveBuffer.isEmpty()) { staticMetaObject.invokeMethod(this, "dataReceived", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray())); } QVariantMap dataMap = jsonDoc.toVariant().toMap(); // Check if this is the initial handshake if (dataMap.value("id").toInt() == 0 && dataMap.contains("params") && !dataMap.contains("notification")) { dataMap = dataMap.value("params").toMap(); m_initialSetupRequired = dataMap.value("initialSetupRequired").toBool(); m_authenticationRequired = dataMap.value("authenticationRequired").toBool(); m_pushButtonAuthAvailable = dataMap.value("pushButtonAuthAvailable").toBool(); emit pushButtonAuthAvailableChanged(); m_serverUuid = dataMap.value("uuid").toString(); m_serverVersion = dataMap.value("version").toString(); QString protoVersionString = dataMap.value("protocol version").toString(); if (!protoVersionString.contains('.')) { protoVersionString.prepend("0."); } m_jsonRpcVersion = QVersionNumber::fromString(protoVersionString); qDebug() << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;; QVersionNumber minimumRequiredVersion = QVersionNumber(1, 0); if (m_jsonRpcVersion < minimumRequiredVersion) { qWarning() << "Nymea box doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion; m_connection->disconnect(); emit invalidProtocolVersion(m_jsonRpcVersion.toString(), minimumRequiredVersion.toString()); return; } emit handshakeReceived(); if (m_initialSetupRequired) { emit initialSetupRequiredChanged(); return; } if (m_authenticationRequired) { QSettings settings; settings.beginGroup("jsonTokens"); m_token = settings.value(m_serverUuid).toByteArray(); settings.endGroup(); emit authenticationRequiredChanged(); if (m_token.isEmpty()) { return; } } setNotificationsEnabled(true); getCloudConnectionStatus(); } // check if this is a notification if (dataMap.contains("notification")) { QStringList notification = dataMap.value("notification").toString().split("."); QString nameSpace = notification.first(); JsonHandler *handler = m_notificationHandlers.value(nameSpace).first; if (!handler) { // qWarning() << "JsonRpc: handler not implemented:" << nameSpace; return; } // qDebug() << "Incoming notification:" << jsonDoc.toJson(); QMetaObject::invokeMethod(handler, m_notificationHandlers.value(nameSpace).second.toLatin1().data(), Q_ARG(QVariantMap, dataMap)); return; } // check if this is a reply to a request int commandId = dataMap.value("id").toInt(); JsonRpcReply *reply = m_replies.take(commandId); if (reply) { reply->deleteLater(); // qDebug() << QString("JsonRpc: got response for %1.%2: %3").arg(reply->nameSpace(), reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))) << reply->callback() << reply->callback(); if (dataMap.value("status").toString() == "unauthorized") { qWarning() << "Something's off with the token"; m_authenticationRequired = true; m_token.clear(); QSettings settings; settings.beginGroup("jsonTokens"); settings.setValue(m_serverUuid, m_token); settings.endGroup(); emit authenticationRequiredChanged(); } if (!reply->caller().isNull() && !reply->callback().isEmpty()) { QMetaObject::invokeMethod(reply->caller(), reply->callback().toLatin1().data(), Q_ARG(QVariantMap, dataMap)); } emit responseReceived(reply->commandId(), dataMap.value("params").toMap()); return; } } JsonRpcReply::JsonRpcReply(int commandId, QString nameSpace, QString method, QVariantMap params, QPointer caller, const QString &callback): m_commandId(commandId), m_nameSpace(nameSpace), m_method(method), m_params(params), m_caller(caller), m_callback(callback) { } JsonRpcReply::~JsonRpcReply() { } int JsonRpcReply::commandId() const { return m_commandId; } QString JsonRpcReply::nameSpace() const { return m_nameSpace; } QString JsonRpcReply::method() const { return m_method; } QVariantMap JsonRpcReply::params() const { return m_params; } QVariantMap JsonRpcReply::requestMap() { QVariantMap request; request.insert("id", m_commandId); request.insert("method", m_nameSpace + "." + m_method); if (!m_params.isEmpty()) request.insert("params", m_params); return request; } QPointer JsonRpcReply::caller() const { return m_caller; } QString JsonRpcReply::callback() const { return m_callback; }