diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index 996e972d..116c0ee8 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "qmqtt.h" #include "sigv4utils.h" @@ -15,7 +16,38 @@ static QByteArray region = "eu-west-1"; //static QByteArray service = "iotdevicegateway"; static QByteArray service = "iotdata"; -AWSClient::AWSClient(QObject *parent) : QObject(parent) +static QByteArray rootCA = "-----BEGIN CERTIFICATE-----\n\ +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n\ +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n\ +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\n\ +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\n\ +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\n\ +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL\n\ +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\n\ +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln\n\ +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\n\ +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\n\ +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1\n\ +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex\n\ +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz\n\ +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG\n\ +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+\n\ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/\n\ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E\n\ +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH\n\ +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy\n\ +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv\n\ +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE\n\ +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y\n\ +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK\n\ +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ\n\ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N\n\ +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq\n\ +-----END CERTIFICATE-----\n\ +"; + +AWSClient::AWSClient(QObject *parent) : QObject(parent), + m_devices(new AWSDevices(this)) { m_nam = new QNetworkAccessManager(this); @@ -46,6 +78,16 @@ QString AWSClient::username() const return m_username; } +QByteArray AWSClient::userId() const +{ + return m_identityId; +} + +AWSDevices *AWSClient::awsDevices() const +{ + return m_devices; +} + void AWSClient::login(const QString &username, const QString &password) { m_username = username; @@ -189,6 +231,46 @@ QByteArray AWSClient::idToken() const return m_idToken; } +QString AWSClient::cognitoIdentityId() const +{ + return m_identityId; +} + +void AWSClient::fetchCertificate(const QString &uuid, std::function callback) +{ + QString fixedUuid = uuid; + fixedUuid.remove(QRegExp("[{}]")); + QNetworkRequest request(QUrl("https://testproductionservice-cloud.guh.io/certificatews/certificate")); + request.setRawHeader("X-api-key", "BJMq4h19dB5yjVKwagTvk9u72FqLecEoWPJIkyDj"); + request.setRawHeader("X-api-vendorId", "testVendor001"); + request.setRawHeader("X-api-deviceId", fixedUuid.toUtf8()); + request.setRawHeader("X-api-serialId", "69696969"); + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, this, [reply, callback]() { + reply->deleteLater(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Error deploying certificate" << data; + return; + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing certificate json" << data; + return; + } + + QByteArray certificate = jsonDoc.toVariant().toMap().value("certificatePem").toByteArray(); + QByteArray publicKey = jsonDoc.toVariant().toMap().value("keyPair").toMap().value("PublicKey").toByteArray(); + QByteArray privateKey = jsonDoc.toVariant().toMap().value("keyPair").toMap().value("PrivateKey").toByteArray(); + qDebug() << "Certificate received" << certificate; + qDebug() << "Public key" << publicKey; + qDebug() << "Private key" << privateKey; + callback(rootCA, certificate, publicKey, privateKey, "a2addxakg5juii.iot.eu-west-1.amazonaws.com"); + }); + +} + void AWSClient::getCredentialsForIdentity(const QString &identityId) { QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/"); @@ -367,15 +449,21 @@ void AWSClient::fetchDevices() qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data); return; } - QList ret; foreach (const QVariant &entry, jsonDoc.toVariant().toMap().value("devices").toList()) { - AWSDevice d; - d.id = entry.toMap().value("deviceId").toString(); - d.name = entry.toMap().value("name").toString(); - d.online = entry.toMap().value("online").toBool(); - ret.append(d); + QString deviceId = entry.toMap().value("deviceId").toString(); + QString name = entry.toMap().value("name").toString(); + bool online = entry.toMap().value("online").toBool(); + qDebug() << "Have cloud device:" << deviceId << name << "online:" << online; + + AWSDevice *d = m_devices->getDevice(deviceId); + if (!d) { + d = new AWSDevice(deviceId, name); + m_devices->insert(d); + } + d->setOnline(online); } - emit devicesFetched(ret); + + emit devicesFetched(); }); } @@ -455,3 +543,96 @@ void AWSClient::refreshAccessToken() }); } + +AWSDevices::AWSDevices(QObject *parent): + QAbstractListModel(parent) +{ + +} + +int AWSDevices::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant AWSDevices::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleName: + return m_list.at(index.row())->name(); + case RoleId: + return m_list.at(index.row())->id(); + case RoleOnline: + return m_list.at(index.row())->online(); + } + return QVariant(); +} + +QHash AWSDevices::roleNames() const +{ + QHash roles; + roles.insert(RoleName, "name"); + roles.insert(RoleId, "id"); + roles.insert(RoleOnline, "online"); + return roles; +} + +AWSDevice *AWSDevices::getDevice(const QString &uuid) const +{ + for (int i = 0; i < m_list.count(); i++) { + if (m_list.at(i)->id() == uuid) { + return m_list.at(i); + } + } + return nullptr; +} + +AWSDevice *AWSDevices::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +void AWSDevices::insert(AWSDevice *device) +{ + device->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(device); + endInsertRows(); + emit countChanged(); +} + +AWSDevice::AWSDevice(const QString &id, const QString &name, bool online, QObject *parent): + QObject (parent), + m_id(id), + m_name(name), + m_online(online) +{ + +} + +QString AWSDevice::id() const +{ + return m_id; +} + +QString AWSDevice::name() const +{ + return m_name; +} + +bool AWSDevice::online() const +{ + return m_online; +} + +void AWSDevice::setOnline(bool online) +{ + if (m_online != online) { + m_online = online; + emit onlineChanged(); + } +} diff --git a/libnymea-app-core/connection/awsclient.h b/libnymea-app-core/connection/awsclient.h index 3e3a6a77..2ee63fa3 100644 --- a/libnymea-app-core/connection/awsclient.h +++ b/libnymea-app-core/connection/awsclient.h @@ -4,14 +4,52 @@ #include #include #include +#include class QNetworkAccessManager; -class AWSDevice { +class AWSDevice: public QObject { + Q_OBJECT + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(bool online READ online NOTIFY onlineChanged) + public: - QString id; - QString name; - bool online; + AWSDevice(const QString &id, const QString &name, bool online = false, QObject *parent = nullptr); + QString id() const; + QString name() const; + bool online() const; + void setOnline(bool online); + +signals: + void onlineChanged(); + +private: + QString m_id; + QString m_name; + bool m_online; +}; + +class AWSDevices: public QAbstractListModel { + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleName, + RoleId, + RoleOnline + }; + AWSDevices(QObject *parent = nullptr); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const; + Q_INVOKABLE AWSDevice* get(int index) const; + void insert(AWSDevice *device); +signals: + void countChanged(); +private: + QList m_list; }; class AWSClient : public QObject @@ -19,12 +57,17 @@ class AWSClient : public QObject Q_OBJECT Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged) Q_PROPERTY(QString username READ username NOTIFY isLoggedInChanged) + Q_PROPERTY(QByteArray userId READ userId NOTIFY isLoggedInChanged) + Q_PROPERTY(QByteArray idToken READ idToken NOTIFY isLoggedInChanged) + Q_PROPERTY(AWSDevices* awsDevices READ awsDevices CONSTANT) public: explicit AWSClient(QObject *parent = nullptr); bool isLoggedIn() const; QString username() const; + QByteArray userId() const; + AWSDevices* awsDevices() const; Q_INVOKABLE void login(const QString &username, const QString &password); Q_INVOKABLE void logout(); @@ -36,11 +79,13 @@ public: bool tokensExpired() const; QByteArray idToken() const; + QString cognitoIdentityId() const; + + void fetchCertificate(const QString &uuid, std::function callback); signals: void isLoggedInChanged(); - - void devicesFetched(QList devices); + void devicesFetched(); private: void refreshAccessToken(); @@ -76,6 +121,8 @@ private: }; QList m_callQueue; + + AWSDevices *m_devices; }; #endif // AWSCLIENT_H diff --git a/libnymea-app-core/connection/cloudtransport.cpp b/libnymea-app-core/connection/cloudtransport.cpp index 9c972cbd..9cdc6437 100644 --- a/libnymea-app-core/connection/cloudtransport.cpp +++ b/libnymea-app-core/connection/cloudtransport.cpp @@ -13,7 +13,6 @@ CloudTransport::CloudTransport(AWSClient *awsClient, QObject *parent): m_awsClient(awsClient) { m_remoteproxyConnection = new RemoteProxyConnection(QUuid::createUuid(), "nymea:app", this); - m_remoteproxyConnection->setInsecureConnection(true); QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::remoteConnectionEstablished, this,[this]() { qDebug() << "CloudTransport: Remote connection established."; @@ -37,8 +36,9 @@ CloudTransport::CloudTransport(AWSClient *awsClient, QObject *parent): }); QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::errorOccured, this, [this] (RemoteProxyConnection::Error error) { qDebug() << "Remote proxy Error:" << error; - emit NymeaTransportInterface::error(QAbstractSocket::ConnectionRefusedError); +// emit NymeaTransportInterface::error(QAbstractSocket::ConnectionRefusedError); }); + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::sslErrors, this, &CloudTransport::sslErrors); } QStringList CloudTransport::supportedSchemes() const @@ -57,7 +57,8 @@ bool CloudTransport::connect(const QUrl &url) bool postResult = m_awsClient->postToMQTT(url.host(), [this](bool success) { if (success) { - m_remoteproxyConnection->connectServer(QHostAddress("34.244.242.103"), 443); + m_remoteproxyConnection->connectServer(QUrl("wss://dev-remoteproxy.nymea.io")); +// m_remoteproxyConnection->connectServer(QUrl("wss://127.0.0.1:1212")); } }); @@ -81,6 +82,7 @@ NymeaTransportInterface::ConnectionState CloudTransport::connectionState() const case RemoteProxyConnection::StateRemoteConnected: return NymeaTransportInterface::ConnectionStateConnected; case RemoteProxyConnection::StateInitializing: + case RemoteProxyConnection::StateHostLookup: case RemoteProxyConnection::StateConnecting: case RemoteProxyConnection::StateConnected: case RemoteProxyConnection::StateAuthenticating: @@ -98,3 +100,9 @@ void CloudTransport::sendData(const QByteArray &data) qDebug() << "should send" << data; m_remoteproxyConnection->sendData(data); } + +void CloudTransport::ignoreSslErrors(const QList &errors) +{ + qDebug() << "Ignoring SSL errors" << errors; + m_remoteproxyConnection->ignoreSslErrors(errors); +} diff --git a/libnymea-app-core/connection/cloudtransport.h b/libnymea-app-core/connection/cloudtransport.h index acd040a8..196a4afe 100644 --- a/libnymea-app-core/connection/cloudtransport.h +++ b/libnymea-app-core/connection/cloudtransport.h @@ -23,6 +23,7 @@ public: ConnectionState connectionState() const override; void sendData(const QByteArray &data) override; + void ignoreSslErrors(const QList &errors) override; private: AWSClient *m_awsClient = nullptr; remoteproxyclient::RemoteProxyConnection *m_remoteproxyConnection = nullptr; diff --git a/libnymea-app-core/discovery/discoverydevice.cpp b/libnymea-app-core/discovery/discoverydevice.cpp index 835b178b..fce6cd93 100644 --- a/libnymea-app-core/discovery/discoverydevice.cpp +++ b/libnymea-app-core/discovery/discoverydevice.cpp @@ -162,3 +162,16 @@ QString Connection::displayName() const { return m_displayName; } + +bool Connection::online() const +{ + return m_online; +} + +void Connection::setOnline(bool online) +{ + if (m_online != online) { + m_online = online; + emit onlineChanged(); + } +} diff --git a/libnymea-app-core/discovery/discoverydevice.h b/libnymea-app-core/discovery/discoverydevice.h index 4a340d5c..bae24a61 100644 --- a/libnymea-app-core/discovery/discoverydevice.h +++ b/libnymea-app-core/discovery/discoverydevice.h @@ -35,6 +35,7 @@ class Connection: public QObject { Q_PROPERTY(BearerType bearerType READ bearerType CONSTANT) Q_PROPERTY(bool secure READ secure CONSTANT) Q_PROPERTY(QString displayName READ displayName CONSTANT) + Q_PROPERTY(bool online READ online NOTIFY onlineChanged) public: enum BearerType { BearerTypeUnknown, @@ -51,12 +52,18 @@ public: BearerType bearerType() const; bool secure() const; QString displayName() const; + bool online() const; + void setOnline(bool online); + +signals: + void onlineChanged(); private: QUrl m_url; BearerType m_bearerType = BearerTypeUnknown; bool m_secure = false; QString m_displayName; + bool m_online = false; }; class Connections: public QAbstractListModel diff --git a/libnymea-app-core/discovery/discoverymodel.h b/libnymea-app-core/discovery/discoverymodel.h index 906fa340..12f6aaab 100644 --- a/libnymea-app-core/discovery/discoverymodel.h +++ b/libnymea-app-core/discovery/discoverymodel.h @@ -40,7 +40,7 @@ public: }; Q_ENUM(DeviceRole) - explicit DiscoveryModel(QObject *parent = 0); + explicit DiscoveryModel(QObject *parent = nullptr); int rowCount(const QModelIndex & parent = QModelIndex()) const; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; diff --git a/libnymea-app-core/discovery/nymeadiscovery.cpp b/libnymea-app-core/discovery/nymeadiscovery.cpp index 3fffa375..cfe6a971 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/discovery/nymeadiscovery.cpp @@ -20,7 +20,7 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this); #endif - connect(Engine::instance()->awsClient(), &AWSClient::devicesFetched, this, &NymeaDiscovery::cloudDevicesFetched); + connect(Engine::instance()->awsClient()->awsDevices(), &AWSDevices::countChanged, this, &NymeaDiscovery::syncCloudDevices); } bool NymeaDiscovery::discovering() const @@ -41,6 +41,7 @@ void NymeaDiscovery::setDiscovering(bool discovering) m_bluetooth->discover(); } if (Engine::instance()->awsClient()->isLoggedIn()) { + syncCloudDevices(); Engine::instance()->awsClient()->fetchDevices(); } } else { @@ -58,23 +59,26 @@ DiscoveryModel *NymeaDiscovery::discoveryModel() const return m_discoveryModel; } -void NymeaDiscovery::cloudDevicesFetched(const QList &devices) +void NymeaDiscovery::syncCloudDevices() { qDebug() << "Cloud devices fetched"; - foreach (const AWSDevice &d, devices) { - DiscoveryDevice *device = m_discoveryModel->find(d.id); + for (int i = 0; i < Engine::instance()->awsClient()->awsDevices()->rowCount(); i++) { + AWSDevice *d = Engine::instance()->awsClient()->awsDevices()->get(i); + DiscoveryDevice *device = m_discoveryModel->find(d->id()); if (!device) { device = new DiscoveryDevice(); - device->setUuid(d.id); - device->setName(d.name); + device->setUuid(d->id()); + device->setName(d->name()); m_discoveryModel->addDevice(device); } QUrl url; url.setScheme("cloud"); - url.setHost(d.id); + url.setHost(d->id()); if (!device->connections()->find(url)) { - Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d.id); + Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d->id()); + conn->setOnline(d->online()); device->connections()->addConnection(conn); } } } + diff --git a/libnymea-app-core/discovery/nymeadiscovery.h b/libnymea-app-core/discovery/nymeadiscovery.h index 215656b2..95af1d07 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.h +++ b/libnymea-app-core/discovery/nymeadiscovery.h @@ -28,7 +28,7 @@ signals: void discoveringChanged(); private slots: - void cloudDevicesFetched(const QList &devices); + void syncCloudDevices(); private: bool m_discovering = false; diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index 01501134..d3b0f950 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -81,6 +81,22 @@ AWSClient *Engine::awsClient() const return m_aws; } +void Engine::deployCertificate() +{ + if (!m_jsonRpcClient->connected()) { + qWarning() << "JSONRPC not connected. Cannot deploy certificate"; + return; + } + if (!m_aws->isLoggedIn()) { + qWarning() << "Not logged in at AWS. Cannot deploy certificate"; + return; + } + m_aws->fetchCertificate(m_jsonRpcClient->serverUuid(), [this](const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint){ + qDebug() << "Certificate received" << certificate << publicKey << privateKey; + m_jsonRpcClient->deployCertificate(rootCA, certificate, publicKey, privateKey, endpoint); + }); +} + NymeaConnection *Engine::connection() const { return m_connection; @@ -107,6 +123,22 @@ Engine::Engine(QObject *parent) : connect(m_jsonRpcClient, &JsonRpcClient::authenticationRequiredChanged, this, &Engine::onConnectedChanged); connect(m_deviceManager, &DeviceManager::fetchingDataChanged, this, &Engine::onDeviceManagerFetchingChanged); + + connect(m_aws, &AWSClient::devicesFetched, this, [this]() { + if (m_jsonRpcClient->connected() && m_jsonRpcClient->cloudConnectionState() == JsonRpcClient::CloudConnectionStateConnected) { + if (m_aws->awsDevices()->getDevice(m_jsonRpcClient->serverUuid()) == nullptr) { + m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->cognitoIdentityId()); + } + } + }); + connect(m_jsonRpcClient, &JsonRpcClient::connectedChanged, this, [this]() { + if (m_jsonRpcClient->connected() && m_jsonRpcClient->cloudConnectionState() == JsonRpcClient::CloudConnectionStateConnected) { + if (m_aws->awsDevices()->getDevice(m_jsonRpcClient->serverUuid()) == nullptr) { + m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->cognitoIdentityId()); + } + } + }); + } void Engine::onConnectedChanged() diff --git a/libnymea-app-core/engine.h b/libnymea-app-core/engine.h index 87a6cd51..cd46c176 100644 --- a/libnymea-app-core/engine.h +++ b/libnymea-app-core/engine.h @@ -62,6 +62,8 @@ public: BluetoothDiscovery *bluetoothDiscovery() const; AWSClient* awsClient() const; + Q_INVOKABLE void deployCertificate(); + private: explicit Engine(QObject *parent = nullptr); static Engine *s_instance; diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index a4c9d0b4..ccdab59f 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -29,6 +29,7 @@ #include #include #include +#include JsonRpcClient::JsonRpcClient(NymeaConnection *connection, QObject *parent) : JsonHandler(parent), @@ -120,8 +121,9 @@ void JsonRpcClient::notificationReceived(const QVariantMap &data) } if (data.value("notification").toString() == "JSONRPC.CloudConnectedChanged") { - m_cloudConnected = data.value("params").toMap().value("connected").toBool(); - emit cloudConnectedChanged(); + QMetaEnum connectionStateEnum = QMetaEnum::fromType(); + m_cloudConnectionState = static_cast(connectionStateEnum.keyToValue(data.value("params").toMap().value("connectionState").toByteArray().data())); + emit cloudConnectionStateChanged(); return; } @@ -131,8 +133,19 @@ void JsonRpcClient::notificationReceived(const QVariantMap &data) void JsonRpcClient::isCloudConnectedReply(const QVariantMap &data) { qDebug() << "Cloud is connected" << data; - m_cloudConnected = data.value("params").toMap().value("connected").toBool(); - emit cloudConnectedChanged(); + 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 @@ -155,9 +168,21 @@ bool JsonRpcClient::pushButtonAuthAvailable() const return m_pushButtonAuthAvailable; } -bool JsonRpcClient::cloudConnected() const +JsonRpcClient::CloudConnectionState JsonRpcClient::cloudConnectionState() const { - return m_cloudConnected; + 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 @@ -209,6 +234,14 @@ int JsonRpcClient::requestPushButtonAuth(const QString &deviceName) return reply->commandId(); } +void JsonRpcClient::setupRemoteAccess(const QString &idToken, const QString &userId) +{ + 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); diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.h b/libnymea-app-core/jsonrpc/jsonrpcclient.h index f41e5cb5..7b534da3 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.h +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.h @@ -40,13 +40,21 @@ class JsonRpcClient : public JsonHandler Q_PROPERTY(bool initialSetupRequired READ initialSetupRequired NOTIFY initialSetupRequiredChanged) Q_PROPERTY(bool authenticationRequired READ authenticationRequired NOTIFY authenticationRequiredChanged) Q_PROPERTY(bool pushButtonAuthAvailable READ pushButtonAuthAvailable NOTIFY pushButtonAuthAvailableChanged) - Q_PROPERTY(bool cloudConnected READ cloudConnected NOTIFY cloudConnectedChanged) + Q_PROPERTY(CloudConnectionState cloudConnectionState READ cloudConnectionState NOTIFY cloudConnectionStateChanged) Q_PROPERTY(QString serverVersion READ serverVersion NOTIFY handshakeReceived) Q_PROPERTY(QString jsonRpcVersion READ jsonRpcVersion NOTIFY handshakeReceived) Q_PROPERTY(QString serverUuid READ serverUuid NOTIFY handshakeReceived) public: - explicit JsonRpcClient(NymeaConnection *connection, QObject *parent = 0); + enum CloudConnectionState { + CloudConnectionStateDisabled, + CloudConnectionStateUnconfigured, + CloudConnectionStateConnecting, + CloudConnectionStateConnected + }; + Q_ENUM(CloudConnectionState) + + explicit JsonRpcClient(NymeaConnection *connection, QObject *parent = nullptr); QString nameSpace() const override; @@ -60,7 +68,8 @@ public: bool initialSetupRequired() const; bool authenticationRequired() const; bool pushButtonAuthAvailable() const; - bool cloudConnected() const; + CloudConnectionState cloudConnectionState() const; + void deployCertificate(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint); QString serverVersion() const; QString jsonRpcVersion() const; @@ -70,6 +79,7 @@ public: Q_INVOKABLE int createUser(const QString &username, const QString &password); Q_INVOKABLE int authenticate(const QString &username, const QString &password, const QString &deviceName); Q_INVOKABLE int requestPushButtonAuth(const QString &deviceName); + Q_INVOKABLE void setupRemoteAccess(const QString &idToken, const QString &userId); Q_INVOKABLE bool ensureServerVersion(const QString &jsonRpcVersion); @@ -84,7 +94,7 @@ signals: void authenticationFailed(); void pushButtonAuthFailed(); void createUserFailed(const QString &error); - void cloudConnectedChanged(); + void cloudConnectionStateChanged(); void responseReceived(const int &commandId, const QVariantMap &response); @@ -105,7 +115,7 @@ private: bool m_initialSetupRequired = false; bool m_authenticationRequired = false; bool m_pushButtonAuthAvailable = false; - bool m_cloudConnected = false; + CloudConnectionState m_cloudConnectionState = CloudConnectionStateDisabled; int m_pendingPushButtonTransaction = -1; QString m_serverUuid; QVersionNumber m_jsonRpcVersion; @@ -124,6 +134,8 @@ private: Q_INVOKABLE void setNotificationsEnabledResponse(const QVariantMap ¶ms); Q_INVOKABLE void notificationReceived(const QVariantMap &data); Q_INVOKABLE void isCloudConnectedReply(const QVariantMap &data); + Q_INVOKABLE void setupRemoteAccessReply(const QVariantMap &data); + Q_INVOKABLE void deployCertificateReply(const QVariantMap &data); void sendRequest(const QVariantMap &request); diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index d9f3efa9..560668b4 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -174,6 +174,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "WirelessAccessPoints", "Can't create this in QML. Get it from the Engine instance."); qmlRegisterUncreatableType(uri, 1, 0, "AWSClient", "Can't create this in QML. Get it from Engine"); + qmlRegisterUncreatableType(uri, 1, 0, "AWSDevice", "Can't create this in QML. Get it from AWSClient"); qmlRegisterType(uri, 1, 0, "RuleTemplates"); qmlRegisterType(uri, 1, 0, "RuleTemplatesFilterModel"); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 30036a24..ca2c45f6 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -243,5 +243,7 @@ ui/images/cloud.svg ui/system/CloudSettingsPage.qml ui/connection/CloudLoginPage.qml + ui/images/cloud-offline.svg + ui/images/cloud-error.svg diff --git a/nymea-app/ui/AppSettingsPage.qml b/nymea-app/ui/AppSettingsPage.qml index 93fc0259..ed5c8517 100644 --- a/nymea-app/ui/AppSettingsPage.qml +++ b/nymea-app/ui/AppSettingsPage.qml @@ -110,6 +110,12 @@ Page { } } ThinDivider {} + MeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Cloud login") + iconName: "../images/cloud.svg" + onClicked: pageStack.push(Qt.resolvedUrl("connection/CloudLoginPage.qml")) + } MeaListItemDelegate { Layout.fillWidth: true text: qsTr("About %1").arg(app.appName) diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 96373b7c..3f737188 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -369,6 +369,5 @@ ApplicationWindow { KeyboardLoader { anchors { left: parent.left; bottom: parent.bottom; right: parent.right } - } } diff --git a/nymea-app/ui/connection/CloudLoginPage.qml b/nymea-app/ui/connection/CloudLoginPage.qml index 0f1d6ae9..dc2a2819 100644 --- a/nymea-app/ui/connection/CloudLoginPage.qml +++ b/nymea-app/ui/connection/CloudLoginPage.qml @@ -16,16 +16,44 @@ Page { visible: Engine.awsClient.isLoggedIn Label { Layout.fillWidth: true - Layout.margins: app.margins + Layout.topMargin: app.margins + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins wrapMode: Text.WordWrap text: qsTr("Logged in as %1").arg(Engine.awsClient.username) } + Button { Layout.fillWidth: true Layout.margins: app.margins text: qsTr("Log out") onClicked: Engine.awsClient.logout(); } + + ThinDivider {} + + Label { + Layout.fillWidth: true + Layout.topMargin: app.margins + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + text: Engine.awsClient.awsDevices.count === 0 ? + qsTr("There are no boxes connected to your cloud yet.") : + qsTr("There (are|is) %1 boxe(s) connected to your cloud", "", Engine.awsClient.awsDevices.count) + } + Repeater { + model: Engine.awsClient.awsDevices + delegate: MeaListItemDelegate { + Layout.fillWidth: true + text: model.name + subText: model.uuid + progressive: false + prominentSubText: false + iconName: "../images/cloud.svg" + secondaryIconName: model.online ? "../images/cloud.svg" : "../images/cloud-offline.svg" + } + } } ColumnLayout { diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index b4bbbc5e..9d012ba4 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -86,11 +86,19 @@ Page { model: ListModel { ListElement { iconSource: "../images/network-vpn.svg"; text: qsTr("Manual connection"); page: "ManualConnectPage.qml" } ListElement { iconSource: "../images/bluetooth.svg"; text: qsTr("Wireless setup"); page: "BluetoothDiscoveryPage.qml"; } - ListElement { iconSource: "../images/cloud.svg"; text: qsTr("Cloud login"); page: "CloudLoginPage.qml" } + ListElement { iconSource: "../images/private-browsing.svg"; text: qsTr("Demo mode"); page: "" } ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "../AppSettingsPage.qml" } } onClicked: { - pageStack.push(model.get(index).page); + if (index === 2) { + Engine.connection.connect("nymea://nymea.nymea.io:2222") + var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) + page.cancel.connect(function() { + Engine.connection.disconnect() + }) + } else { + pageStack.push(model.get(index).page); + } } } @@ -261,23 +269,32 @@ Page { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins - visible: discovery.discoveryModel.count === 0 +// visible: discovery.discoveryModel.count === 0 text: qsTr("Start wireless setup") - onClicked: pageStack.push(Qt.resolvedUrl("connection/BluetoothDiscoveryPage.qml")) + onClicked: pageStack.push(Qt.resolvedUrl("BluetoothDiscoveryPage.qml")) } + Button { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Cloud login") + visible: !Engine.awsClient.isLoggedIn + onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml")) + } + Button { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.bottomMargin: app.margins - visible: discovery.discoveryModel.count === 0 +// visible: discovery.discoveryModel.count === 0 text: qsTr("Demo mode (online)") onClicked: { + Engine.connection.connect("nymea://nymea.nymea.io:2222") var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) page.cancel.connect(function() { Engine.connection.disconnect() }) - Engine.connection.connect("nymea://nymea.nymea.io:2222") } } diff --git a/nymea-app/ui/images/cloud-error.svg b/nymea-app/ui/images/cloud-error.svg new file mode 100644 index 00000000..874e90d7 --- /dev/null +++ b/nymea-app/ui/images/cloud-error.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/cloud-offline.svg b/nymea-app/ui/images/cloud-offline.svg new file mode 100644 index 00000000..0f11fddd --- /dev/null +++ b/nymea-app/ui/images/cloud-offline.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/nymea-app/ui/system/CloudSettingsPage.qml b/nymea-app/ui/system/CloudSettingsPage.qml index aaf74b8b..c0d481d0 100644 --- a/nymea-app/ui/system/CloudSettingsPage.qml +++ b/nymea-app/ui/system/CloudSettingsPage.qml @@ -11,12 +11,35 @@ Page { onBackPressed: pageStack.pop(); } + Connections { + target: Engine.basicConfiguration + onCloudEnabledChanged: { + if (Engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured) { + Engine.deployCertificate(); + } + } + } + + Connections { + target: Engine.jsonRpcClient + onCloudConnectionStateChanged: { + if (Engine.awsClient.isLoggedIn && Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) === null) { + print("Pairing user and box...") + Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId); + } + } + } + ColumnLayout { anchors { left: parent.left; top: parent.top; right: parent.right } Label { Layout.fillWidth: true - text: Engine.jsonRpcClient.cloudConnected + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Layout.topMargin: app.margins + text: qsTr("You can connect a nymea:box to a nymea:cloud in order to access it from anywhere") + wrapMode: Text.WordWrap } SwitchDelegate { @@ -27,5 +50,45 @@ Page { Engine.basicConfiguration.cloudEnabled = checked; } } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + visible: Engine.basicConfiguration.cloudEnabled + + BusyIndicator { + visible: Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured || + Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting + } + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: { + switch (Engine.jsonRpcClient.cloudConnectionState) { + case JsonRpcClient.CloudConnectionStateDisabled: + return "" + case JsonRpcClient.CloudConnectionStateUnconfigured: + return qsTr("Configuring the box to connect to nymea:cloud..."); + case JsonRpcClient.CloudConnectionStateConnecting: + return qsTr("Connecting the box to nymea:cloud..."); + case JsonRpcClient.CloudConnectionStateConnected: + return qsTr("The box is connected to nymea:cloud."); + } + return Engine.jsonRpcClient.cloudConnectionState + } + } + } + + +// Label { +// Layout.fillWidth: true +// Layout.leftMargin: app.margins +// Layout.rightMargin: app.margins +// visible: Engine.basicConfiguration.cloudEnabled && Engine.awsClient.isLoggedIn +// text: Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) !== null ? +// qsTr("This box is connected to a nymea:cloud.") : +// qsTr("Connecting to nymea:cloud...") +// } } } diff --git a/nymea-remoteproxy b/nymea-remoteproxy index 0a18897d..f6e2d9b3 160000 --- a/nymea-remoteproxy +++ b/nymea-remoteproxy @@ -1 +1 @@ -Subproject commit 0a18897de79606543b0e7a68ac9b1990a5dd761c +Subproject commit f6e2d9b3b208362159dd22a4eb11527db25d8760