848 lines
32 KiB
C++
848 lines
32 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright 2013 - 2020, 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
* 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 "jsonrpcclient.h"
|
|
#include "connection/nymeaconnection.h"
|
|
#include "types/param.h"
|
|
#include "types/params.h"
|
|
|
|
#include "connection/tcpsockettransport.h"
|
|
#include "connection/websockettransport.h"
|
|
#include "connection/bluetoothtransport.h"
|
|
#include "connection/tunnelproxytransport.h"
|
|
|
|
#include <QJsonDocument>
|
|
#include <QVariantMap>
|
|
#include <QDebug>
|
|
#include <QUuid>
|
|
#include <QSettings>
|
|
#include <QVersionNumber>
|
|
#include <QMetaEnum>
|
|
#include <QLocale>
|
|
#include <QDir>
|
|
#include <QStandardPaths>
|
|
|
|
#include "logging.h"
|
|
NYMEA_LOGGING_CATEGORY(dcJsonRpc, "JsonRpc")
|
|
|
|
JsonRpcClient::JsonRpcClient(QObject *parent) :
|
|
QObject(parent),
|
|
m_id(0)
|
|
{
|
|
m_connection = new NymeaConnection(this);
|
|
m_connection->registerTransport(new TcpSocketTransportFactory());
|
|
m_connection->registerTransport(new WebsocketTransportFactory());
|
|
m_connection->registerTransport(new BluetoothTransportFactoy());
|
|
m_connection->registerTransport(new TunnelProxyTransportFactory());
|
|
|
|
connect(m_connection, &NymeaConnection::availableBearerTypesChanged, this, &JsonRpcClient::availableBearerTypesChanged);
|
|
connect(m_connection, &NymeaConnection::connectionStatusChanged, this, &JsonRpcClient::connectionStatusChanged);
|
|
connect(m_connection, &NymeaConnection::connectedChanged, this, &JsonRpcClient::onInterfaceConnectedChanged);
|
|
connect(m_connection, &NymeaConnection::currentHostChanged, this, &JsonRpcClient:: currentHostChanged);
|
|
connect(m_connection, &NymeaConnection::currentConnectionChanged, this, &JsonRpcClient:: currentConnectionChanged);
|
|
// We'll connect this Queued, because in case of a disconnect we'll want to react on that ASAP instead of processing a queue that may be left in buffers
|
|
// Especially on mobile platforms (hello Android) we get a huge queue of buffers upon resume from suspend just to get a disconnect after that.
|
|
connect(m_connection, &NymeaConnection::dataAvailable, this, &JsonRpcClient::dataReceived, Qt::QueuedConnection);
|
|
|
|
registerNotificationHandler(this, QStringLiteral("JSONRPC"), "notificationReceived");
|
|
}
|
|
|
|
void JsonRpcClient::registerNotificationHandler(QObject *handler, const QString &nameSpace, const QString &method)
|
|
{
|
|
if (m_notificationHandlers.key(handler) == nameSpace) {
|
|
qWarning() << "Notification handler" << handler << " already registered for namespace" << nameSpace;
|
|
return;
|
|
}
|
|
m_notificationHandlers.insert(nameSpace, handler);
|
|
m_notificationHandlerMethods.insert(handler, method);
|
|
setNotificationsEnabled();
|
|
}
|
|
|
|
void JsonRpcClient::unregisterNotificationHandler(QObject *handler)
|
|
{
|
|
foreach (const QString nameSpace, m_notificationHandlers.keys(handler)) {
|
|
m_notificationHandlers.remove(nameSpace, handler);
|
|
}
|
|
m_notificationHandlerMethods.remove(handler);
|
|
setNotificationsEnabled();
|
|
}
|
|
|
|
int JsonRpcClient::sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller, const QString &callbackMethod)
|
|
{
|
|
|
|
JsonRpcReply *reply = createReply(method, params, caller, callbackMethod);
|
|
|
|
if (m_cacheHashes.contains(method)) {
|
|
QString hash = m_cacheHashes.value(method);
|
|
QString callSignature = method + '-' + QJsonDocument::fromVariant(params).toJson() + '-' + QLocale().name();
|
|
QString callSignatureHash = QCryptographicHash::hash(callSignature.toUtf8(), QCryptographicHash::Md5).toHex();
|
|
QFile f(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + '/' + method + '-' + callSignatureHash + '-' + hash + ".cache");
|
|
if (f.exists() && f.open(QFile::ReadOnly)) {
|
|
QJsonParseError error;
|
|
QVariantMap cachedParams = QJsonDocument::fromJson(f.readAll(), &error).toVariant().toMap();
|
|
f.close();
|
|
if (error.error == QJsonParseError::NoError) {
|
|
qDebug() << "Loaded results for" << reply->nameSpace() + '.' + reply->method() << "from cache";
|
|
// We want to make sure this is an async operation even if we have stuff in cache, so only call callbacks using Qt::QueuedConnection
|
|
if (!reply->caller().isNull() && !reply->callback().isEmpty()) {
|
|
QMetaObject::invokeMethod(reply->caller(), reply->callback().toLatin1().data(), Qt::QueuedConnection, Q_ARG(int, reply->commandId()), Q_ARG(QVariantMap, cachedParams));
|
|
}
|
|
QMetaObject::invokeMethod(this, "responseReceived", Qt::QueuedConnection, Q_ARG(int, reply->commandId()), Q_ARG(QVariantMap, cachedParams));
|
|
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
|
|
return reply->commandId();
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
NymeaConnection::BearerTypes JsonRpcClient::availableBearerTypes() const
|
|
{
|
|
return m_connection->availableBearerTypes();
|
|
}
|
|
|
|
NymeaConnection::ConnectionStatus JsonRpcClient::connectionStatus() const
|
|
{
|
|
return m_connection->connectionStatus();
|
|
}
|
|
|
|
void JsonRpcClient::connectToHost(NymeaHost *host, Connection *connection)
|
|
{
|
|
if (m_connection->currentHost()) {
|
|
disconnect(m_connection->currentHost(), &NymeaHost::nameChanged, this, &JsonRpcClient::serverNameChanged);
|
|
}
|
|
|
|
m_connection->connectToHost(host, connection);
|
|
|
|
connect(host, &NymeaHost::nameChanged, this, &JsonRpcClient::serverNameChanged);
|
|
emit serverNameChanged();
|
|
}
|
|
|
|
void JsonRpcClient::disconnectFromHost()
|
|
{
|
|
m_connection->disconnectFromHost();
|
|
}
|
|
|
|
void JsonRpcClient::acceptCertificate(const QString &serverUuid, const QByteArray &pem)
|
|
{
|
|
qDebug() << "Pinning new certificate for" << serverUuid << pem;
|
|
storePem(serverUuid, pem);
|
|
}
|
|
|
|
bool JsonRpcClient::tokenExists(const QString &serverUuid) const
|
|
{
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
return settings.contains(QUuid(serverUuid).toString());
|
|
}
|
|
|
|
void JsonRpcClient::addToken(const QString &serverUuid, const QByteArray &token)
|
|
{
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
settings.setValue(QUuid(serverUuid).toString(), token);
|
|
settings.endGroup();
|
|
}
|
|
|
|
void JsonRpcClient::setNotificationsEnabledResponse(int commandId, const QVariantMap ¶ms)
|
|
{
|
|
qCDebug(dcJsonRpc()) << "Notification configuration response:" << commandId << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
|
|
|
if (!m_connected) {
|
|
m_connected = true;
|
|
emit connectedChanged(true);
|
|
}
|
|
}
|
|
|
|
void JsonRpcClient::notificationReceived(const QVariantMap &data)
|
|
{
|
|
qCDebug(dcJsonRpc()) << "Notification received:" << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
|
|
if (data.value("notification").toString() == "JSONRPC.PushButtonAuthFinished") {
|
|
qCInfo(dcJsonRpc()) << "Push button auth finished.";
|
|
if (data.value("params").toMap().value("transactionId").toInt() != m_pendingPushButtonTransaction) {
|
|
qCWarning(dcJsonRpc()) << "This push button transaction is not what we're waiting for...";
|
|
return;
|
|
}
|
|
m_pendingPushButtonTransaction = -1;
|
|
if (data.value("params").toMap().value("success").toBool()) {
|
|
qCInfo(dcJsonRpc()) << "Push button auth succeeded";
|
|
m_token = data.value("params").toMap().value("token").toByteArray();
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
settings.setValue(m_connection->currentHost()->uuid().toString(), m_token);
|
|
settings.endGroup();
|
|
|
|
m_initialSetupRequired = false;
|
|
|
|
emit authenticationRequiredChanged();
|
|
|
|
// Push button auth will always hand out admin tokens
|
|
m_permissionScopes = UserInfo::PermissionScopeAdmin;
|
|
emit permissionsChanged();
|
|
|
|
setNotificationsEnabled();
|
|
} else {
|
|
emit pushButtonAuthFailed();
|
|
}
|
|
return;
|
|
}
|
|
|
|
qCWarning(dcJsonRpc()) << "JsonRpcClient: Unhandled notification received" << data;
|
|
}
|
|
|
|
void JsonRpcClient::getVersionsReply(int /*commandId*/, const QVariantMap &data)
|
|
{
|
|
m_serverQtVersion = data.value("qtVersion").toString();
|
|
m_serverQtBuildVersion = data.value("qtBuildVersion").toString();
|
|
if (!m_serverQtVersion.isEmpty()) {
|
|
emit serverQtVersionChanged();
|
|
}
|
|
}
|
|
|
|
bool JsonRpcClient::connected() const
|
|
{
|
|
return m_connected;
|
|
}
|
|
|
|
NymeaHost *JsonRpcClient::currentHost() const
|
|
{
|
|
return m_connection->currentHost();
|
|
}
|
|
|
|
Connection *JsonRpcClient::currentConnection() const
|
|
{
|
|
return m_connection->currentConnection();
|
|
}
|
|
|
|
QVariantMap JsonRpcClient::certificateIssuerInfo() const
|
|
{
|
|
QVariantMap issuerInfo;
|
|
foreach (const QByteArray &attr, m_connection->sslCertificate().issuerInfoAttributes()) {
|
|
issuerInfo.insert(attr, m_connection->sslCertificate().issuerInfo(attr));
|
|
}
|
|
|
|
QByteArray certificateFingerprint;
|
|
QByteArray digest = m_connection->sslCertificate().digest(QCryptographicHash::Sha256);
|
|
for (int i = 0; i < digest.length(); i++) {
|
|
if (certificateFingerprint.length() > 0) {
|
|
certificateFingerprint.append(":");
|
|
}
|
|
certificateFingerprint.append(digest.mid(i,1).toHex().toUpper());
|
|
}
|
|
|
|
issuerInfo.insert("fingerprint", certificateFingerprint);
|
|
|
|
return issuerInfo;
|
|
}
|
|
|
|
bool JsonRpcClient::initialSetupRequired() const
|
|
{
|
|
return m_initialSetupRequired;
|
|
}
|
|
|
|
bool JsonRpcClient::authenticationRequired() const
|
|
{
|
|
return m_authenticationRequired && m_token.isEmpty();
|
|
}
|
|
|
|
bool JsonRpcClient::pushButtonAuthAvailable() const
|
|
{
|
|
return m_pushButtonAuthAvailable;
|
|
}
|
|
|
|
bool JsonRpcClient::authenticated() const
|
|
{
|
|
return m_authenticated;
|
|
}
|
|
|
|
QHash<QString, QString> JsonRpcClient::cacheHashes() const
|
|
{
|
|
return m_cacheHashes;
|
|
}
|
|
|
|
UserInfo::PermissionScopes JsonRpcClient::permissions() const
|
|
{
|
|
return m_permissionScopes;
|
|
}
|
|
|
|
QString JsonRpcClient::serverVersion() const
|
|
{
|
|
return m_serverVersion;
|
|
}
|
|
|
|
QString JsonRpcClient::jsonRpcVersion() const
|
|
{
|
|
return m_jsonRpcVersion.toString();
|
|
}
|
|
|
|
QString JsonRpcClient::serverUuid() const
|
|
{
|
|
return m_connection && m_connection->currentHost() ? m_connection->currentHost()->uuid().toString() : "";
|
|
}
|
|
|
|
QString JsonRpcClient::serverName() const
|
|
{
|
|
return m_connection->currentHost() ? m_connection->currentHost()->name() : "";
|
|
}
|
|
|
|
QString JsonRpcClient::serverQtVersion()
|
|
{
|
|
if (!m_serverQtVersion.isEmpty()) {
|
|
return m_serverQtVersion;
|
|
}
|
|
if (ensureServerVersion("4.0")) {
|
|
sendCommand("JSONRPC.Version", QVariantMap(), this, "getVersionsReply");
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QString JsonRpcClient::serverQtBuildVersion()
|
|
{
|
|
return m_serverQtBuildVersion;
|
|
}
|
|
|
|
QVariantMap JsonRpcClient::experiences() const
|
|
{
|
|
return m_experiences;
|
|
}
|
|
|
|
int JsonRpcClient::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email)
|
|
{
|
|
QVariantMap params;
|
|
params.insert("username", username);
|
|
params.insert("password", password);
|
|
if (ensureServerVersion("6.0")) {
|
|
params.insert("displayName", displayName);
|
|
params.insert("email", email);
|
|
}
|
|
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);
|
|
qDebug() << "Authenticating:" << username << password << 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();
|
|
}
|
|
|
|
bool JsonRpcClient::ensureServerVersion(const QString &jsonRpcVersion)
|
|
{
|
|
return QVersionNumber(m_jsonRpcVersion) >= QVersionNumber::fromString(jsonRpcVersion);
|
|
}
|
|
|
|
void JsonRpcClient::processAuthenticate(int /*commandId*/, const QVariantMap &data)
|
|
{
|
|
if (data.value("success").toBool()) {
|
|
qCInfo(dcJsonRpc()) << "authentication successful";
|
|
m_token = data.value("token").toByteArray();
|
|
m_username = data.value("username").toString();
|
|
if (m_jsonRpcVersion.majorVersion() >= 6) {
|
|
m_permissionScopes = UserInfo::listToScopes(data.value("scopes").toStringList());
|
|
} else {
|
|
m_permissionScopes = UserInfo::PermissionScopeAdmin;
|
|
}
|
|
emit permissionsChanged();
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
settings.setValue(m_connection->currentHost()->uuid().toString(), m_token);
|
|
settings.endGroup();
|
|
emit authenticationRequiredChanged();
|
|
|
|
m_authenticated = true;
|
|
emit authenticatedChanged();
|
|
|
|
setNotificationsEnabled();
|
|
} else {
|
|
qCWarning(dcJsonRpc()) << "Authentication failed" << data;
|
|
emit authenticationFailed();
|
|
}
|
|
}
|
|
|
|
void JsonRpcClient::processCreateUser(int /*commandId*/, const QVariantMap &data)
|
|
{
|
|
qDebug() << "create user response:" << data;
|
|
if (data.value("error").toString() == "UserErrorNoError") {
|
|
emit createUserSucceeded();
|
|
m_initialSetupRequired = false;
|
|
emit initialSetupRequiredChanged();
|
|
} else {
|
|
qDebug() << "Emitting create user failed";
|
|
emit createUserFailed(data.value("error").toString());
|
|
}
|
|
}
|
|
|
|
void JsonRpcClient::processRequestPushButtonAuth(int /*commandId*/, const QVariantMap &data)
|
|
{
|
|
qDebug() << "requestPushButtonAuth response" << data;
|
|
if (data.value("success").toBool()) {
|
|
m_pendingPushButtonTransaction = data.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) {
|
|
qCWarning(dcJsonRpc()) << "Invalid method. Must be Namespace.Method";
|
|
return nullptr;
|
|
}
|
|
m_id++;
|
|
return new JsonRpcReply(m_id, callParts.first(), callParts.last(), params, caller, callback);
|
|
}
|
|
|
|
void JsonRpcClient::setNotificationsEnabled()
|
|
{
|
|
QStringList namespaces;
|
|
foreach (const QString &nameSpace, m_notificationHandlers.keys()) {
|
|
namespaces.append(nameSpace);
|
|
}
|
|
|
|
if (!m_connection->connected()) {
|
|
return;
|
|
}
|
|
|
|
// We always want the Users notification to check for changed permissions
|
|
if (!namespaces.contains("Users")) {
|
|
namespaces.append("Users");
|
|
}
|
|
|
|
QVariantMap params;
|
|
|
|
if (ensureServerVersion("3.1")) {
|
|
params.insert("namespaces", namespaces);
|
|
} else {
|
|
params.insert("enabled", namespaces.count() > 0);
|
|
}
|
|
JsonRpcReply *reply = createReply("JSONRPC.SetNotificationStatus", params, this, "setNotificationsEnabledResponse");
|
|
m_replies.insert(reply->commandId(), reply);
|
|
qCDebug(dcJsonRpc) << "Setting notification status";
|
|
sendRequest(reply->requestMap());
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
bool JsonRpcClient::loadPem(const QUuid &serverUud, QByteArray &pem)
|
|
{
|
|
QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/");
|
|
QFile certFile(dir.absoluteFilePath(serverUud.toString().remove(QRegExp("[{}]")) + ".pem"));
|
|
if (!certFile.open(QFile::ReadOnly)) {
|
|
return false;
|
|
}
|
|
pem.clear();
|
|
pem.append(certFile.readAll());
|
|
return true;
|
|
}
|
|
|
|
bool JsonRpcClient::storePem(const QUuid &serverUuid, const QByteArray &pem)
|
|
{
|
|
QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/");
|
|
if (!dir.exists()) {
|
|
dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/");
|
|
}
|
|
QFile certFile(dir.absoluteFilePath(serverUuid.toString().remove(QRegExp("[{}]")) + ".pem"));
|
|
if (!certFile.open(QFile::WriteOnly | QFile::Truncate)) {
|
|
return false;
|
|
}
|
|
certFile.write(pem);
|
|
certFile.close();
|
|
return true;
|
|
}
|
|
|
|
void JsonRpcClient::onInterfaceConnectedChanged(bool connected)
|
|
{
|
|
|
|
if (!connected) {
|
|
qCInfo(dcJsonRpc()) << "JsonRpcClient: Transport disconnected.";
|
|
m_initialSetupRequired = false;
|
|
m_authenticationRequired = false;
|
|
m_authenticated = false;
|
|
m_receiveBuffer.clear();
|
|
m_serverQtVersion.clear();
|
|
m_serverQtBuildVersion.clear();
|
|
if (m_connected) {
|
|
m_connected = false;
|
|
emit connectedChanged(false);
|
|
}
|
|
} else {
|
|
qCInfo(dcJsonRpc()) << "JsonRpcClient: Transport connected. Starting handshake.";
|
|
// Clear anything that might be left in the buffer from a previous connection.
|
|
m_receiveBuffer.clear();
|
|
|
|
// Load token for this host
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
m_token = settings.value(currentHost()->uuid().toString()).toByteArray();
|
|
settings.endGroup();
|
|
|
|
|
|
QVariantMap params;
|
|
params.insert("locale", QLocale().name());
|
|
sendCommand("JSONRPC.Hello", params, this, "helloReply");
|
|
}
|
|
}
|
|
|
|
void JsonRpcClient::dataReceived(const QByteArray &data)
|
|
{
|
|
if (!m_connection->connected()) {
|
|
// Given this slot is invoked with QueuedConnection, we might still get pending data packages after a disconnected event
|
|
// In that case we can discard all pending packages as we'll have to reconnect anyways.
|
|
return;
|
|
}
|
|
// 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 a notification
|
|
if (dataMap.contains("notification")) {
|
|
qCDebug(dcJsonRpc()) << "Incoming notification:" << qUtf8Printable(jsonDoc.toJson());
|
|
// Check if our permissions changed
|
|
if (dataMap.value("notification").toString() == "Users.UserChanged") {
|
|
QVariantMap userMap = dataMap.value("params").toMap().value("userInfo").toMap();
|
|
if (userMap.value("username").toString() == m_username) {
|
|
m_permissionScopes = UserInfo::listToScopes(userMap.value("scopes").toStringList());
|
|
qCInfo(dcJsonRpc()) << "Permissions changed for" << userMap.value("username") << userMap.value("scopes").toStringList().join(",") << m_permissionScopes;
|
|
emit permissionsChanged();
|
|
}
|
|
}
|
|
QStringList notification = dataMap.value("notification").toString().split(".");
|
|
QString nameSpace = notification.first();
|
|
foreach (QObject *handler, m_notificationHandlers.values(nameSpace)) {
|
|
QMetaObject::invokeMethod(handler, m_notificationHandlerMethods.value(handler).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();
|
|
// qWarning() << 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") {
|
|
qCWarning(dcJsonRpc()) << "Something's off with the token";
|
|
m_authenticationRequired = true;
|
|
m_token.clear();
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
settings.setValue(serverUuid(), m_token);
|
|
settings.endGroup();
|
|
emit authenticationRequiredChanged();
|
|
m_authenticated = false;
|
|
emit authenticatedChanged();
|
|
}
|
|
|
|
if (dataMap.value("status").toString() == "error") {
|
|
qCWarning(dcJsonRpc()) << "An error happened in the JSONRPC layer:" << dataMap.value("error").toString();
|
|
qCWarning(dcJsonRpc()) << "Request was:" << qUtf8Printable(QJsonDocument::fromVariant(reply->requestMap()).toJson());
|
|
if (reply->nameSpace() == "JSONRPC" && reply->method() == "Hello") {
|
|
qCInfo(dcJsonRpc()) << "Hello call failed. Trying again without locale";
|
|
m_id = 0;
|
|
sendCommand("JSONRPC.Hello", QVariantMap(), this, "helloReply");
|
|
}
|
|
}
|
|
// Note: We're still forwarding a failed call, params will be empty tho...
|
|
// This should never really happen as errors on this layer indicate a but in the caller code in the first place
|
|
// Some methods however, like authenticate might fail on an invalid token tho and stil need to act on it
|
|
|
|
if (!reply->caller().isNull() && !reply->callback().isEmpty()) {
|
|
QMetaObject::invokeMethod(reply->caller(), reply->callback().toLatin1().data(), Q_ARG(int, commandId), Q_ARG(QVariantMap, dataMap.value("params").toMap()));
|
|
}
|
|
|
|
emit responseReceived(reply->commandId(), dataMap.value("params").toMap());
|
|
|
|
|
|
// If the server supports cache hashes, cache stuff locally
|
|
QString fullMethod = reply->nameSpace() + '.' + reply->method();
|
|
if (m_cacheHashes.contains(fullMethod)) {
|
|
QString hash = m_cacheHashes.value(fullMethod);
|
|
QString callSignature = fullMethod + '-' + QJsonDocument::fromVariant(reply->params()).toJson() + '-' + QLocale().name();
|
|
QString callSignatureHash = QCryptographicHash::hash(callSignature.toUtf8(), QCryptographicHash::Md5).toHex();
|
|
QFile f(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + '/' + fullMethod + '-' + callSignatureHash + '-' + hash + ".cache");
|
|
if (!f.exists() && f.open(QFile::WriteOnly | QFile::Truncate)) {
|
|
f.write(QJsonDocument::fromVariant(dataMap.value("params")).toJson());
|
|
f.close();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms)
|
|
{
|
|
m_initialSetupRequired = params.value("initialSetupRequired").toBool();
|
|
m_authenticationRequired = params.value("authenticationRequired").toBool();
|
|
m_pushButtonAuthAvailable = params.value("pushButtonAuthAvailable").toBool();
|
|
emit pushButtonAuthAvailableChanged();
|
|
|
|
m_serverVersion = params.value("version").toString();
|
|
QUuid serverUuid = params.value("uuid").toUuid();
|
|
QString name = params.value("name").toString();
|
|
m_experiences.clear();
|
|
foreach (const QVariant &experience, params.value("experiences").toList()) {
|
|
m_experiences.insert(experience.toMap().value("name").toString(), experience.toMap().value("version").toString());
|
|
}
|
|
|
|
QString protoVersionString = params.value("protocol version").toString();
|
|
if (!protoVersionString.contains('.')) {
|
|
protoVersionString.prepend("0.");
|
|
}
|
|
|
|
m_jsonRpcVersion = QVersionNumber::fromString(protoVersionString);
|
|
|
|
qCInfo(dcJsonRpc()) << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;
|
|
|
|
if (m_connection->currentHost()->uuid().isNull()) {
|
|
qCDebug(dcJsonRpc()) << "Updating Server UUID in connection:" << m_connection->currentHost()->uuid().toString() << "->" << serverUuid;
|
|
m_connection->currentHost()->setUuid(serverUuid);
|
|
// Now that we know the server uuid, if we have a token for this host, let's try again.
|
|
if (tokenExists(serverUuid.toString())){
|
|
onInterfaceConnectedChanged(true);
|
|
return;
|
|
}
|
|
} else if (m_connection->currentHost()->uuid() != serverUuid) {
|
|
qCWarning(dcJsonRpc()) << "Unexpected server UUID" << serverUuid.toString() << "expected:" << m_connection->currentHost()->uuid();
|
|
emit invalidServerUuid(serverUuid);
|
|
return;
|
|
}
|
|
|
|
m_connection->currentHost()->setName(name);
|
|
|
|
QVersionNumber minimumRequiredVersion = QVersionNumber(5, 0);
|
|
QVersionNumber maximumMajorVersion = QVersionNumber(8);
|
|
if (m_jsonRpcVersion < minimumRequiredVersion) {
|
|
qCWarning(dcJsonRpc()) << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion;
|
|
emit invalidMinimumVersion(m_jsonRpcVersion.toString(), minimumRequiredVersion.toString());
|
|
return;
|
|
}
|
|
if (m_jsonRpcVersion.majorVersion() > maximumMajorVersion.majorVersion()) {
|
|
qCWarning(dcJsonRpc()) << "Nymea core has breaking API changes not supported by this app version. Core major version:" << m_jsonRpcVersion.majorVersion() << "Maximum supported major version:" << maximumMajorVersion.majorVersion();
|
|
emit invalidMaximumVersion(m_jsonRpcVersion.toString(), QString("%1.x").arg(maximumMajorVersion.majorVersion()));
|
|
return;
|
|
}
|
|
|
|
|
|
// Verify SSL certificate
|
|
if (m_connection->isEncrypted()) {
|
|
QByteArray oldPem;
|
|
QSslCertificate certificate = m_connection->sslCertificate();
|
|
if (!loadPem(serverUuid, oldPem)) {
|
|
qCInfo(dcJsonRpc()) << "No SSL certificate for this host stored. Accepting and pinning new certificate.";
|
|
// No certificate yet! Inform ui about it.
|
|
emit newSslCertificate();
|
|
storePem(serverUuid, m_connection->sslCertificate().toPem());
|
|
} else {
|
|
// We have a certificate pinned already. Check if it's the same
|
|
if (certificate.toPem() != oldPem) {
|
|
// Uh oh, the certificate has changed
|
|
qCWarning(dcJsonRpc()) << "This connections certificate has changed!";
|
|
qCWarning(dcJsonRpc()) << "Old PEM:" << oldPem;
|
|
qCWarning(dcJsonRpc()) << "New PEM:" << certificate.toPem();
|
|
|
|
// Extract certificate info before disconnecting.
|
|
QVariantMap issuerInfo = certificateIssuerInfo();
|
|
|
|
// Reject the connection until the UI explicitly accepts this...
|
|
m_connection->disconnectFromHost();
|
|
|
|
emit verifyConnectionCertificate(serverUuid.toString(), issuerInfo, certificate.toPem());
|
|
return;
|
|
}
|
|
qCInfo(dcJsonRpc()) << "This connections certificate is trusted.";
|
|
}
|
|
}
|
|
|
|
m_cacheHashes.clear();
|
|
qCDebug(dcJsonRpc()) << "Hello reply:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
|
QVariantList cacheHashes = params.value("cacheHashes").toList();
|
|
foreach (const QVariant &cacheHash, cacheHashes) {
|
|
m_cacheHashes.insert(cacheHash.toMap().value("method").toString(), cacheHash.toMap().value("hash").toString());
|
|
}
|
|
// qDebug() << "Caches:" << m_cacheHashes;
|
|
|
|
if (m_jsonRpcVersion.majorVersion() >= 6 && m_authenticationRequired) {
|
|
if (!params.value("authenticated").toBool()) {
|
|
qCWarning(dcJsonRpc) << "Seems our token is not valid!";
|
|
m_token.clear();
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
settings.setValue(serverUuid.toString(), m_token);
|
|
settings.endGroup();
|
|
emit authenticationRequiredChanged();
|
|
m_authenticated = false;
|
|
emit authenticatedChanged();
|
|
return;
|
|
}
|
|
m_permissionScopes = UserInfo::listToScopes(params.value("permissionScopes").toStringList());
|
|
} else {
|
|
m_permissionScopes = UserInfo::PermissionScopeAdmin;
|
|
}
|
|
m_username = params.value("username").toString();
|
|
qCInfo(dcJsonRpc()) << "User:" << m_username << "Permissions:" << UserInfo::scopesToList(m_permissionScopes);
|
|
emit permissionsChanged();
|
|
|
|
emit handshakeReceived();
|
|
|
|
if (m_initialSetupRequired) {
|
|
qCInfo(dcJsonRpc()) << "Initial setup is required for this nymea instance!";
|
|
emit initialSetupRequiredChanged();
|
|
return;
|
|
}
|
|
|
|
if (m_authenticationRequired) {
|
|
// Reload the token, now that we're certain about the server uuid.
|
|
QSettings settings;
|
|
settings.beginGroup("jsonTokens");
|
|
m_token = settings.value(serverUuid.toString()).toByteArray();
|
|
settings.endGroup();
|
|
emit authenticationRequiredChanged();
|
|
|
|
if (m_token.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_authenticated = true;
|
|
qCInfo(dcJsonRpc()) << "Authenticated to nymea instance.";
|
|
emit authenticatedChanged();
|
|
}
|
|
|
|
setNotificationsEnabled();
|
|
}
|
|
|
|
JsonRpcReply::JsonRpcReply(int commandId, QString nameSpace, QString method, QVariantMap params, QPointer<QObject> 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<QObject> JsonRpcReply::caller() const
|
|
{
|
|
return m_caller;
|
|
}
|
|
|
|
QString JsonRpcReply::callback() const
|
|
{
|
|
return m_callback;
|
|
}
|