diff --git a/libnymea-core/cloud/cloudmanager.cpp b/libnymea-core/cloud/cloudmanager.cpp index dcef091a..a4263e82 100644 --- a/libnymea-core/cloud/cloudmanager.cpp +++ b/libnymea-core/cloud/cloudmanager.cpp @@ -25,12 +25,17 @@ #include "cloudnotifications.h" #include "nymeaconfiguration.h" #include "cloudtransport.h" +#include "nymeaconfiguration.h" +#include "nymeasettings.h" #include "nymea-remoteproxyclient/remoteproxyconnection.h" +#include using namespace remoteproxyclient; -CloudManager::CloudManager(NetworkManager *networkManager, QObject *parent) : QObject(parent), +CloudManager::CloudManager(NymeaConfiguration *configuration, NetworkManager *networkManager, QObject *parent): + QObject(parent), + m_configuration(configuration), m_networkManager(networkManager) { m_awsConnector = new AWSConnector(this); @@ -49,35 +54,30 @@ CloudManager::CloudManager(NetworkManager *networkManager, QObject *parent) : QO m_transport = new CloudTransport(ServerConfiguration()); connect(m_awsConnector, &AWSConnector::proxyConnectionRequestReceived, m_transport, &CloudTransport::connectToCloud); + + m_deviceId = m_configuration->serverUuid(); + m_deviceName = m_configuration->serverName(); + m_serverUrl = m_configuration->cloudServerUrl(); + m_caCertificate = m_configuration->cloudCertificateCA(); + m_clientCertificate = m_configuration->cloudCertificate(); + m_clientCertificateKey = m_configuration->cloudCertificateKey(); + + setEnabled(m_configuration->cloudEnabled()); + connect(m_configuration, &NymeaConfiguration::cloudEnabledChanged, this, &CloudManager::setEnabled); + connect(m_configuration, &NymeaConfiguration::serverNameChanged, this, &CloudManager::setDeviceName); + } CloudManager::~CloudManager() { } -void CloudManager::setServerUrl(const QString &serverUrl) -{ - m_serverUrl = serverUrl; -} - -void CloudManager::setDeviceId(const QUuid &deviceId) -{ - m_deviceId = deviceId; -} - void CloudManager::setDeviceName(const QString &name) { m_deviceName = name; m_awsConnector->setDeviceName(name); } -void CloudManager::setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey) -{ - m_caCertificate = caCertificate; - m_clientCertificate = clientCertificate; - m_clientCertificateKey = clientCertificateKey; -} - bool CloudManager::enabled() const { return m_enabled; @@ -86,6 +86,9 @@ bool CloudManager::enabled() const void CloudManager::setEnabled(bool enabled) { if (enabled) { + m_enabled = true; + emit connectionStateChanged(); + bool missingConfig = false; if (m_deviceId.isNull()) { qCWarning(dcCloud()) << "Don't have a unique device ID."; @@ -117,7 +120,6 @@ void CloudManager::setEnabled(bool enabled) } qCDebug(dcCloud()) << "Enabling cloud connection."; - m_enabled = true; if (!m_awsConnector->isConnected() && m_networkManager->state() == NetworkManager::NetworkManagerStateConnectedGlobal) { connect2aws(); } @@ -125,12 +127,77 @@ void CloudManager::setEnabled(bool enabled) qCDebug(dcCloud()) << "Disabling cloud connection."; m_enabled = false; m_awsConnector->disconnectAWS(); + emit connectionStateChanged(); } } -bool CloudManager::connected() const +bool CloudManager::installClientCertificates(const QByteArray &rootCA, const QByteArray &certificatePEM, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint) { - return m_awsConnector->isConnected(); + QString baseDir = NymeaSettings::storagePath() + "/certs/cloud/"; + QDir dir; + // We never delete old certs, cycle until we find an unused path + int i = 0; + do { + dir = QDir(baseDir + QString::number(i++) + '/'); + } while (dir.exists()); + + if (!dir.mkpath(dir.absolutePath())) { + qCWarning(dcCloud) << "Cannot install cloud certificates. Unable to create path."; + return false; + } + QFile ca(dir.absoluteFilePath("aws-certification-authority.crt")); + if (!ca.open(QFile::WriteOnly) || ca.write(rootCA) != rootCA.length()) { + qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write CA file" << dir.absoluteFilePath(ca.fileName()); + ca.close(); + return false; + } + ca.close(); + QFile pem(dir.absoluteFilePath("guh-cloud.pem")); + if (!pem.open(QFile::WriteOnly) || pem.write(certificatePEM) != certificatePEM.length()) { + qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write certificate file" << dir.absoluteFilePath(pem.fileName()); + pem.close(); + return false; + } + pem.close(); + QFile pub(dir.absoluteFilePath("guh-cloud.pub")); + if (!pub.open(QFile::WriteOnly) || pub.write(publicKey) != publicKey.length()) { + qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write public key file" << dir.absoluteFilePath(pub.fileName()); + pub.close(); + return false; + } + pub.close(); + QFile key(dir.absoluteFilePath("guh-cloud.key")); + if (!key.open(QFile::WriteOnly) || key.write(privateKey) != privateKey.length()) { + qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write private key file" << dir.absoluteFilePath(key.fileName()); + key.close(); + return false; + } + key.close(); + qCDebug(dcCloud) << "Installed cloud certificates to" << dir.absolutePath(); + m_caCertificate = dir.absoluteFilePath("aws-certification-authority.crt"); + m_clientCertificate = dir.absoluteFilePath("guh-cloud.pem"); + m_clientCertificateKey = dir.absoluteFilePath("guh-cloud.key"); + m_serverUrl = endpoint; + + if (m_enabled) { + m_awsConnector->disconnectAWS(); + connect2aws(); + } + return true; +} + +CloudManager::CloudConnectionState CloudManager::connectionState() const +{ + if (m_awsConnector->isConnected()) { + return CloudConnectionStateConnected; + } + if (!m_enabled) { + return CloudConnectionStateDisabled; + } + if (m_deviceId.isNull() || m_deviceName.isEmpty() || m_serverUrl.isEmpty() || m_clientCertificate.isEmpty() || m_clientCertificateKey.isEmpty() || m_caCertificate.isEmpty()) { + return CloudConnectionStateUnconfigured; + } + return CloudConnectionStateConnecting; } void CloudManager::pairDevice(const QString &idToken, const QString &userId) @@ -191,10 +258,10 @@ void CloudManager::onJanusWebRtcHandshakeMessageReceived(const QString &transact void CloudManager::awsConnected() { - emit connectedChanged(true); + emit connectionStateChanged(); } void CloudManager::awsDisconnected() { - emit connectedChanged(false); + emit connectionStateChanged(); } diff --git a/libnymea-core/cloud/cloudmanager.h b/libnymea-core/cloud/cloudmanager.h index ed7f9ba3..3855fd1a 100644 --- a/libnymea-core/cloud/cloudmanager.h +++ b/libnymea-core/cloud/cloudmanager.h @@ -37,22 +37,33 @@ class RemoteProxyConnection; namespace nymeaserver { +class NymeaConfiguration; class CloudTransport; class CloudManager : public QObject { Q_OBJECT public: - explicit CloudManager(NetworkManager *networkManager, QObject *parent = nullptr); + enum CloudConnectionState { + CloudConnectionStateDisabled, + CloudConnectionStateUnconfigured, + CloudConnectionStateConnecting, + CloudConnectionStateConnected + }; + Q_ENUM(CloudConnectionState) + + explicit CloudManager(NymeaConfiguration *configuration, NetworkManager *networkManager, QObject *parent = nullptr); ~CloudManager(); - void setServerUrl(const QString &serverUrl); - void setDeviceId(const QUuid &deviceId); - void setDeviceName(const QString &name); - void setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey); +// void setServerUrl(const QString &serverUrl); +// void setDeviceId(const QUuid &deviceId); +// void setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey); bool enabled() const; void setEnabled(bool enabled); - bool connected() const; + + bool installClientCertificates(const QByteArray &rootCA, const QByteArray &certificatePEM, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint); + + CloudConnectionState connectionState() const; void pairDevice(const QString &idToken, const QString &userId); @@ -62,7 +73,7 @@ public: CloudTransport* createTransportInterface() const; signals: - void connectedChanged(bool connected); + void connectionStateChanged(); void pairingReply(QString cognitoUserId, int status, const QString &message); @@ -76,12 +87,14 @@ private slots: void onJanusWebRtcHandshakeMessageReceived(const QString &transactionId, const QVariantMap &data); void awsConnected(); void awsDisconnected(); + void setDeviceName(const QString &name); private: QTimer m_reconnectTimer; bool m_enabled = false; AWSConnector *m_awsConnector = nullptr; JanusConnector *m_janusConnector = nullptr; + NymeaConfiguration *m_configuration = nullptr; NetworkManager *m_networkManager = nullptr; QString m_serverUrl; diff --git a/libnymea-core/jsonrpc/jsonrpcserver.cpp b/libnymea-core/jsonrpc/jsonrpcserver.cpp index 879ead5b..417705df 100644 --- a/libnymea-core/jsonrpc/jsonrpcserver.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserver.cpp @@ -165,6 +165,17 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject returns.insert("error", JsonTypes::userErrorRef()); setReturns("RemoveToken", returns); + params.clear(); returns.clear(); + setDescription("SetupCloudConnection", "Sets up the cloud connection by deploying a certificate and its configuration."); + params.insert("rootCA", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("certificatePEM", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("publicKey", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("privateKey", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("endpoint", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("SetupCloudConnection", params); + returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::Bool)); + setReturns("SetupCloudConnection", returns); + params.clear(); returns.clear(); setDescription("SetupRemoteAccess", "Setup the remote connection by providing AWS token information. This requires the cloud to be connected."); params.insert("idToken", JsonTypes::basicTypeToString(JsonTypes::String)); @@ -175,9 +186,10 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject setReturns("SetupRemoteAccess", returns); params.clear(); returns.clear(); - setDescription("IsCloudConnected", "Check whether the cloud is currently connected."); + setDescription("IsCloudConnected", "Check whether the cloud is currently connected. \"connected\" will be true whenever connectionState equals CloudConnectionStateConnected and is deprecated. Please use the connectionState value instead."); setParams("IsCloudConnected", params); returns.insert("connected", JsonTypes::basicTypeToString(JsonTypes::Bool)); + returns.insert("connectionState", JsonTypes::cloudConnectionStateRef()); setReturns("IsCloudConnected", returns); params.clear(); returns.clear(); @@ -329,6 +341,25 @@ JsonReply *JsonRPCServer::RemoveToken(const QVariantMap ¶ms) return createReply(ret); } +JsonReply *JsonRPCServer::SetupCloudConnection(const QVariantMap ¶ms) +{ + if (NymeaCore::instance()->cloudManager()->connectionState() != CloudManager::CloudConnectionStateUnconfigured) { + qCDebug(dcCloud) << "Cloud already configured. Not changing configuration as it won't work anyways. If you want to reconfigure this instance to a different cloud, change the system UUID and wipe the cloud settings from the config."; + QVariantMap data; + data.insert("success", false); + return createReply(data); + } + QByteArray rootCA = params.value("rootCA").toByteArray(); + QByteArray certificatePEM = params.value("certificatePEM").toByteArray(); + QByteArray publicKey = params.value("publicKey").toByteArray(); + QByteArray privateKey = params.value("privateKey").toByteArray(); + QString endpoint = params.value("endpoint").toString(); + bool status = NymeaCore::instance()->cloudManager()->installClientCertificates(rootCA, certificatePEM, publicKey, privateKey, endpoint); + QVariantMap ret; + ret.insert("success", status); + return createReply(ret); +} + JsonReply *JsonRPCServer::SetupRemoteAccess(const QVariantMap ¶ms) { QString idToken = params.value("idToken").toString(); @@ -345,9 +376,10 @@ JsonReply *JsonRPCServer::SetupRemoteAccess(const QVariantMap ¶ms) JsonReply *JsonRPCServer::IsCloudConnected(const QVariantMap ¶ms) { Q_UNUSED(params) - bool connected = NymeaCore::instance()->cloudManager()->connected(); + bool connected = NymeaCore::instance()->cloudManager()->connectionState() == CloudManager::CloudConnectionStateConnected; QVariantMap data; data.insert("connected", connected); + data.insert("connectionState", JsonTypes::cloudConnectionStateToString(NymeaCore::instance()->cloudManager()->connectionState())); return createReply(data); } @@ -457,7 +489,7 @@ void JsonRPCServer::setup() registerHandler(new TagsHandler(this)); connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServer::pairingFinished); - connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectedChanged, this, &JsonRPCServer::onCloudConnectedChanged); + connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectionStateChanged, this, &JsonRPCServer::onCloudConnectionStateChanged); } void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) @@ -616,10 +648,11 @@ void JsonRPCServer::pairingFinished(QString cognitoUserId, int status, const QSt reply->finished(); } -void JsonRPCServer::onCloudConnectedChanged(bool connected) +void JsonRPCServer::onCloudConnectionStateChanged() { QVariantMap params; - params.insert("connected", connected); + params.insert("connected", NymeaCore::instance()->cloudManager()->connectionState() == CloudManager::CloudConnectionStateConnected); + params.insert("connectionState", JsonTypes::cloudConnectionStateToString(NymeaCore::instance()->cloudManager()->connectionState())); emit CloudConnectedChanged(params); } diff --git a/libnymea-core/jsonrpc/jsonrpcserver.h b/libnymea-core/jsonrpc/jsonrpcserver.h index f53e5985..9472896c 100644 --- a/libnymea-core/jsonrpc/jsonrpcserver.h +++ b/libnymea-core/jsonrpc/jsonrpcserver.h @@ -43,7 +43,7 @@ class JsonRPCServer: public JsonHandler { Q_OBJECT public: - JsonRPCServer(const QSslConfiguration &sslConfiguration = QSslConfiguration(), QObject *parent = 0); + JsonRPCServer(const QSslConfiguration &sslConfiguration = QSslConfiguration(), QObject *parent = nullptr); // JsonHandler API implementation QString name() const; @@ -57,6 +57,7 @@ public: Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *SetupCloudConnection(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *IsCloudConnected(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *KeepAlive(const QVariantMap ¶ms); @@ -91,7 +92,7 @@ private slots: void asyncReplyFinished(); void pairingFinished(QString cognitoUserId, int status, const QString &message); - void onCloudConnectedChanged(bool connected); + void onCloudConnectionStateChanged(); void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); private: diff --git a/libnymea-core/jsonrpc/jsontypes.cpp b/libnymea-core/jsonrpc/jsontypes.cpp index 0609c600..17f1676c 100644 --- a/libnymea-core/jsonrpc/jsontypes.cpp +++ b/libnymea-core/jsonrpc/jsontypes.cpp @@ -90,6 +90,7 @@ QVariantList JsonTypes::s_networkManagerState; QVariantList JsonTypes::s_networkDeviceState; QVariantList JsonTypes::s_userError; QVariantList JsonTypes::s_tagError; +QVariantList JsonTypes::s_cloudConnectionState; QVariantMap JsonTypes::s_paramType; QVariantMap JsonTypes::s_param; @@ -151,6 +152,7 @@ void JsonTypes::init() s_networkDeviceState = enumToStrings(NetworkDevice::staticMetaObject, "NetworkDeviceState"); s_userError = enumToStrings(UserManager::staticMetaObject, "UserError"); s_tagError = enumToStrings(TagsStorage::staticMetaObject, "TagError"); + s_cloudConnectionState = enumToStrings(CloudManager::staticMetaObject, "CloudConnectionState"); // ParamType s_paramType.insert("id", basicTypeToString(Uuid)); @@ -2124,6 +2126,12 @@ QPair JsonTypes::validateVariant(const QVariant &templateVariant, qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(logEntryRef()); return result; } + } else if (refName == cloudConnectionStateRef()) { + QPair result = validateEnum(s_cloudConnectionState, variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(cloudConnectionStateRef()); + return result; + } } else { Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data()); return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName)); diff --git a/libnymea-core/jsonrpc/jsontypes.h b/libnymea-core/jsonrpc/jsontypes.h index ad9cf1b2..b9896306 100644 --- a/libnymea-core/jsonrpc/jsontypes.h +++ b/libnymea-core/jsonrpc/jsontypes.h @@ -56,6 +56,8 @@ #include "networkmanager/wirelessnetworkdevice.h" #include "networkmanager/wirelessaccesspoint.h" +#include "cloud/cloudmanager.h" + #include #include @@ -139,6 +141,7 @@ public: DECLARE_TYPE(networkDeviceState, "NetworkDeviceState", NetworkDevice, NetworkDeviceState) DECLARE_TYPE(userError, "UserError", UserManager, UserError) DECLARE_TYPE(tagError, "TagError", TagsStorage, TagError) + DECLARE_TYPE(cloudConnectionState, "CloudConnectionState", CloudManager, CloudConnectionState) DECLARE_OBJECT(paramType, "ParamType") DECLARE_OBJECT(param, "Param") diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index fd769000..8f25d35a 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -549,12 +549,7 @@ void NymeaCore::init() { m_debugServerHandler = new DebugServerHandler(this); qCDebug(dcApplication) << "Creating Cloud Manager"; - m_cloudManager = new CloudManager(m_networkManager, this); - m_cloudManager->setDeviceId(m_configuration->serverUuid()); - m_cloudManager->setDeviceName(m_configuration->serverName()); - m_cloudManager->setServerUrl(m_configuration->cloudServerUrl()); - m_cloudManager->setClientCertificates(m_configuration->cloudCertificateCA(), m_configuration->cloudCertificate(), m_configuration->cloudCertificateKey()); - m_cloudManager->setEnabled(m_configuration->cloudEnabled()); + m_cloudManager = new CloudManager(m_configuration, m_networkManager, this); CloudNotifications *cloudNotifications = m_cloudManager->createNotificationsPlugin(); m_deviceManager->registerStaticPlugin(cloudNotifications, cloudNotifications->metaData()); @@ -563,8 +558,6 @@ void NymeaCore::init() { m_serverManager->jsonServer()->registerTransportInterface(cloudTransport, false); connect(m_configuration, &NymeaConfiguration::localeChanged, this, &NymeaCore::onLocaleChanged); - connect(m_configuration, &NymeaConfiguration::cloudEnabledChanged, m_cloudManager, &CloudManager::setEnabled); - connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_cloudManager, &CloudManager::setDeviceName); connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName); connect(m_deviceManager, &DeviceManager::pluginConfigChanged, this, &NymeaCore::pluginConfigChanged);