From 080a47932a0535465938b42ce711d3c618749c56 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 15 Aug 2018 21:19:55 +0200 Subject: [PATCH] it's working! --- libnymea-app-core/connection/awsclient.cpp | 126 +++++++++++------- libnymea-app-core/connection/awsclient.h | 17 ++- .../connection/cloudtransport.cpp | 45 ++++--- libnymea-app-core/connection/cloudtransport.h | 2 - .../discovery/nymeadiscovery.cpp | 2 - libnymea-app-core/libnymea-app-core.pro | 3 - nymea-app/main.cpp | 3 +- nymea-app/nymea-app.pro | 2 - nymea-app/ui/connection/CloudLoginPage.qml | 38 ++++-- nymea-remoteproxy | 2 +- 10 files changed, 147 insertions(+), 93 deletions(-) diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index 219bb935..996e972d 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -22,11 +22,14 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent) QSettings settings; settings.beginGroup("cloud"); m_username = settings.value("username").toString(); + m_password = settings.value("password").toString(); m_accessToken = settings.value("accessToken").toByteArray(); m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime(); m_idToken = settings.value("idToken").toByteArray(); m_refreshToken = settings.value("refreshToken").toByteArray(); + m_identityId = settings.value("identityId").toByteArray(); + m_accessKeyId = settings.value("accessKeyId").toByteArray(); m_secretKey = settings.value("secretKey").toByteArray(); m_sessionToken = settings.value("sessionToken").toByteArray(); @@ -38,6 +41,11 @@ bool AWSClient::isLoggedIn() const return !m_username.isEmpty() && !m_password.isEmpty(); } +QString AWSClient::username() const +{ + return m_username; +} + void AWSClient::login(const QString &username, const QString &password) { m_username = username; @@ -47,12 +55,6 @@ void AWSClient::login(const QString &username, const QString &password) // See: https://forums.aws.amazon.com/thread.jspa?threadID=287978 m_password = password; - QSettings settings; - settings.remove("cloud"); - settings.beginGroup("cloud"); - settings.setValue("username", username); - settings.setValue("password", password); - QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/"); QUrlQuery query; @@ -85,6 +87,8 @@ void AWSClient::login(const QString &username, const QString &password) reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qWarning() << "Error logging in to aws:" << reply->error() << reply->errorString(); + m_username.clear(); + m_password.clear(); return; } QByteArray data = reply->readAll(); @@ -92,6 +96,8 @@ void AWSClient::login(const QString &username, const QString &password) QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "Failed to parse AWS login response" << error.errorString(); + m_username.clear(); + m_password.clear(); return; } @@ -102,7 +108,11 @@ void AWSClient::login(const QString &username, const QString &password) m_refreshToken = authenticationResult.value("RefreshToken").toByteArray(); QSettings settings; + settings.remove("cloud"); + settings.beginGroup("cloud"); + settings.setValue("username", m_username); + settings.setValue("password", m_password); settings.setValue("accessToken", m_accessToken); settings.setValue("accessTokenExpiry", m_accessTokenExpiry); settings.setValue("idToken", m_idToken); @@ -116,6 +126,15 @@ void AWSClient::login(const QString &username, const QString &password) }); } +void AWSClient::logout() +{ + m_username.clear(); + m_password.clear(); + QSettings settings; + settings.remove("cloud"); + emit isLoggedInChanged(); +} + void AWSClient::getId() { QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/"); @@ -154,14 +173,22 @@ void AWSClient::getId() qWarning() << "Error parsing json reply for GetId" << error.errorString(); return; } - QByteArray identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); + m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); + QSettings settings; + settings.beginGroup("cloud"); + settings.setValue("identityId", m_identityId); - qDebug() << "Received cognito identity id" << identityId; - getCredentialsForIdentity(identityId); + qDebug() << "Received cognito identity id" << m_identityId; + getCredentialsForIdentity(m_identityId); }); } +QByteArray AWSClient::idToken() const +{ + return m_idToken; +} + void AWSClient::getCredentialsForIdentity(const QString &identityId) { QUrl url("https://cognito-identity.eu-west-1.amazonaws.com/"); @@ -225,42 +252,34 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) while (!m_callQueue.isEmpty()) { QueuedCall qc = m_callQueue.takeFirst(); - switch (qc.args.count()) { - case 0: - QMetaObject::invokeMethod(this, qc.method.toUtf8().data()); - break; - case 1: - QMetaObject::invokeMethod(this, qc.method.toUtf8().data(), Q_ARG(QString, qc.args.first())); - break; - default: - Q_ASSERT_X(false, "AWSClient", "Call queue handling does not yet support multiple arguments"); - break; + if (qc.method == "fetchDevices") { + fetchDevices(); + } else if (qc.method == "postToMQTT") { + postToMQTT(qc.boxId, qc.callback); } } }); } -bool AWSClient::tokenExpired() const +bool AWSClient::tokensExpired() const { - qDebug() << "access token expirty:" << m_accessTokenExpiry.toString() << "session token expiry" << m_sessionTokenExpiry.toString() << "current:" << QDateTime::currentDateTime().toString(); - qDebug() << "access token expired:" << (m_accessTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()); return (m_accessTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()) || (m_sessionTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()); } -void AWSClient::postToMQTT(const QString &token) +bool AWSClient::postToMQTT(const QString &boxId, std::function callback) { if (!isLoggedIn()) { qWarning() << "Cannot post to MQTT. Not logged in to AWS"; - return; + return false; } - if (tokenExpired()) { - qDebug() << "Cannot post to MQTT. Need to refresh the token first"; + if (tokensExpired()) { + qDebug() << "Cannot post to MQTT. Need to refresh the tokens first"; refreshAccessToken(); - m_callQueue.append(QueuedCall("postToMQTT", token)); - return; - } + m_callQueue.append(QueuedCall("postToMQTT", boxId, callback)); + return true; // So far it looks we're doing ok... let's return true + } QString host = "a2addxakg5juii.iot.eu-west-1.amazonaws.com"; - QString topic = "850593e9-f2ab-4e89-913a-16f848d48867/eu-west-1:88c8b0f1-3f26-46cb-81f3-ccc37dcb543a/proxy"; + QString topic = QString("%1/%2/proxy").arg(boxId).arg(QString(m_identityId)); // This is somehow broken in AWS... // The Signature needs to be created with having the topic percentage-encoded twice @@ -268,11 +287,11 @@ void AWSClient::postToMQTT(const QString &token) // Now one could think this is an issue in how the signature is made, but it can't really // be fixed there as this concerns only the actual topic, not /topics/ // so we can't percentage-encode the whole path inside the signature helper... - QString path = "/topics/" + topic.toUtf8().toPercentEncoding().toPercentEncoding() + "?qos=0"; - QString path1 = "/topics/" + topic.toUtf8().toPercentEncoding() + "?qos=0"; + QString path = "/topics/" + topic.toUtf8().toPercentEncoding().toPercentEncoding() + "?qos=1"; + QString path1 = "/topics/" + topic.toUtf8().toPercentEncoding() + "?qos=1"; QVariantMap params; - params.insert("token", token); + params.insert("token", m_idToken); params.insert("timestamp", QDateTime::currentDateTime().toSecsSinceEpoch()); QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact); @@ -293,10 +312,27 @@ void AWSClient::postToMQTT(const QString &token) } qDebug() << "Payload:" << payload; QNetworkReply *reply = m_nam->post(request, payload); - connect(reply, &QNetworkReply::finished, this, [reply]() { + connect(reply, &QNetworkReply::finished, this, [reply, callback]() { reply->deleteLater(); - qDebug() << "post reply" << reply->readAll(); + QByteArray data = reply->readAll(); + qDebug() << "post reply" << data; + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse reply" << error.error << error.errorString() << data; + callback(false); + return; + } + if (jsonDoc.toVariant().toMap().value("message").toString() != "OK") { + qWarning() << "Something went wrong posting to MQTT:" << jsonDoc.toVariant().toMap().value("message").toString(); + callback(false); + return; + } + callback(true); + }); + + return true; } void AWSClient::fetchDevices() @@ -305,8 +341,8 @@ void AWSClient::fetchDevices() qWarning() << "Not logged in at AWS. Can't fetch paired devices"; return; } - if (tokenExpired()) { - qDebug() << "Need to refresh our tokens"; + if (tokensExpired()) { + qDebug() << "Cannot fetch devices. Need to refresh our tokens"; refreshAccessToken(); m_callQueue.append(QueuedCall("fetchDevices")); return; @@ -337,20 +373,11 @@ void AWSClient::fetchDevices() d.id = entry.toMap().value("deviceId").toString(); d.name = entry.toMap().value("name").toString(); d.online = entry.toMap().value("online").toBool(); - d.token = m_accessToken; ret.append(d); } emit devicesFetched(ret); - - }); - -} - -QByteArray AWSClient::accessToken() const -{ - return m_accessToken; } void AWSClient::refreshAccessToken() @@ -359,12 +386,15 @@ void AWSClient::refreshAccessToken() qDebug() << "Cannot refresh tokens. Not logged in to AWS"; return; } + + // We should use REFRESH_TOKEN_AUTH to refresh our tokens but it's not working + // https://forums.aws.amazon.com/thread.jspa?threadID=287978 + // Let's re-login instead with user & pass login(m_username, m_password); return; - // We should use this to refresh our tokens but it's not working - // https://forums.aws.amazon.com/thread.jspa?threadID=287978 + // Non-working block... Enable this if Amazon ever fixes their API... QUrl url("https://cognito-idp.eu-west-1.amazonaws.com/"); QUrlQuery query; diff --git a/libnymea-app-core/connection/awsclient.h b/libnymea-app-core/connection/awsclient.h index 5167e987..3e3a6a77 100644 --- a/libnymea-app-core/connection/awsclient.h +++ b/libnymea-app-core/connection/awsclient.h @@ -12,27 +12,30 @@ public: QString id; QString name; bool online; - QByteArray token; }; class AWSClient : public QObject { Q_OBJECT Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged) + Q_PROPERTY(QString username READ username NOTIFY isLoggedInChanged) public: explicit AWSClient(QObject *parent = nullptr); bool isLoggedIn() const; + QString username() const; Q_INVOKABLE void login(const QString &username, const QString &password); + Q_INVOKABLE void logout(); Q_INVOKABLE void fetchDevices(); - Q_INVOKABLE void postToMQTT(const QString &token); + Q_INVOKABLE bool postToMQTT(const QString &boxId, std::function callback); Q_INVOKABLE void getId(); - QByteArray accessToken() const; + bool tokensExpired() const; + QByteArray idToken() const; signals: void isLoggedInChanged(); @@ -44,7 +47,6 @@ private: void getCredentialsForIdentity(const QString &identityId); void connectMQTT(); - bool tokenExpired() const; private: QNetworkAccessManager *m_nam = nullptr; @@ -57,6 +59,8 @@ private: QByteArray m_idToken; QByteArray m_refreshToken; + QByteArray m_identityId; + QByteArray m_accessKeyId; QByteArray m_secretKey; QByteArray m_sessionToken; @@ -65,9 +69,10 @@ private: class QueuedCall { public: QueuedCall(const QString &method): method(method) { } - QueuedCall(const QString &method, const QString &arg1): method(method) { args.append(arg1); } + QueuedCall(const QString &method, const QString &boxId, std::function callback): method(method), boxId(boxId), callback(callback) {} QString method; - QStringList args; + QString boxId; + std::function callback; }; QList m_callQueue; diff --git a/libnymea-app-core/connection/cloudtransport.cpp b/libnymea-app-core/connection/cloudtransport.cpp index 295a8422..9c972cbd 100644 --- a/libnymea-app-core/connection/cloudtransport.cpp +++ b/libnymea-app-core/connection/cloudtransport.cpp @@ -4,6 +4,7 @@ #include "remoteproxyconnection.h" #include +#include using namespace remoteproxyclient; @@ -11,26 +12,33 @@ CloudTransport::CloudTransport(AWSClient *awsClient, QObject *parent): NymeaTransportInterface(parent), m_awsClient(awsClient) { - m_remoteproxyConnection = new RemoteProxyConnection(QUuid::createUuid(), "nymea:app", RemoteProxyConnection::ConnectionTypeWebSocket, this); + m_remoteproxyConnection = new RemoteProxyConnection(QUuid::createUuid(), "nymea:app", this); m_remoteproxyConnection->setInsecureConnection(true); - QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::connected, this,[this]() { - qDebug() << "Connected to remote proxy"; + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::remoteConnectionEstablished, this,[this]() { + qDebug() << "CloudTransport: Remote connection established."; + emit connected(); }); - QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::stateChanged, this,[this](RemoteProxyConnection::State state) { + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::disconnected, this,[this]() { + qDebug() << "CloudTransport: Disconnected."; + emit disconnected(); + }); + + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::stateChanged, this,[](RemoteProxyConnection::State state) { qDebug() << "Proxy state changed:" << state; - if (state == RemoteProxyConnection::StateRemoteConnected) { - emit connected(); - } }); + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::ready, this,[this]() { - qDebug() << "Proxy ready:"; - m_remoteproxyConnection->authenticate(m_token); + qDebug() << "Proxy ready. Authenticating channel."; + m_remoteproxyConnection->authenticate(m_awsClient->idToken()); }); QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::dataReady, this, [this](const QByteArray &data) { - qDebug() << "Remote connection data received"; emit dataReady(data); }); + QObject::connect(m_remoteproxyConnection, &RemoteProxyConnection::errorOccured, this, [this] (RemoteProxyConnection::Error error) { + qDebug() << "Remote proxy Error:" << error; + emit NymeaTransportInterface::error(QAbstractSocket::ConnectionRefusedError); + }); } QStringList CloudTransport::supportedSchemes() const @@ -44,20 +52,27 @@ bool CloudTransport::connect(const QUrl &url) qWarning() << "Not logged in to AWS, cannot connect"; return false; } + qDebug() << "Connecting to" << url; - QUrlQuery query(url.query()); - QString m_token = query.queryItemValue("token"); - m_awsClient->postToMQTT(m_token); + bool postResult = m_awsClient->postToMQTT(url.host(), [this](bool success) { + if (success) { + m_remoteproxyConnection->connectServer(QHostAddress("34.244.242.103"), 443); + } + }); - m_remoteproxyConnection->connectServer(QHostAddress("127.0.0.1"), 1212); + if (!postResult) { + qWarning() << "Failed to post to MQTT. Cannot continue"; + return false; + } return true; } void CloudTransport::disconnect() { - qDebug() << "should disconnect"; + qDebug() << "CloudTransport: Disconnecting from server."; + m_remoteproxyConnection->disconnectServer(); } NymeaTransportInterface::ConnectionState CloudTransport::connectionState() const diff --git a/libnymea-app-core/connection/cloudtransport.h b/libnymea-app-core/connection/cloudtransport.h index 5b19c799..acd040a8 100644 --- a/libnymea-app-core/connection/cloudtransport.h +++ b/libnymea-app-core/connection/cloudtransport.h @@ -26,8 +26,6 @@ public: private: AWSClient *m_awsClient = nullptr; remoteproxyclient::RemoteProxyConnection *m_remoteproxyConnection = nullptr; - - QString m_token; }; #endif // CLOUDTRANSPORT_H diff --git a/libnymea-app-core/discovery/nymeadiscovery.cpp b/libnymea-app-core/discovery/nymeadiscovery.cpp index d246cc9c..3fffa375 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/discovery/nymeadiscovery.cpp @@ -72,8 +72,6 @@ void NymeaDiscovery::cloudDevicesFetched(const QList &devices) QUrl url; url.setScheme("cloud"); url.setHost(d.id); - QUrlQuery query; - query.addQueryItem("token", d.token); if (!device->connections()->find(url)) { Connection *conn = new Connection(url, Connection::BearerTypeCloud, true, d.id); device->connections()->addConnection(conn); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 18fde7b5..ecbe52f0 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -10,9 +10,6 @@ include(../config.pri) DEFINES += WITH_ZEROCONF include(../QtZeroConf/qtzeroconf.pri) } -DEFINES += QT_STATIC -include(../qmqtt/src/mqtt/mqtt.pri) -HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS include(../nymea-remoteproxy/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri) diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 871f32b6..e3895e19 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -42,8 +42,7 @@ int main(int argc, char *argv[]) application.setOrganizationName("nymea"); foreach (const QFileInfo &fi, QDir(":/ui/fonts/").entryInfoList()) { - int id = QFontDatabase::addApplicationFont(fi.absoluteFilePath()); -// qDebug() << "Added font" << fi.absoluteFilePath() << QFontDatabase::applicationFontFamilies(id); + QFontDatabase::addApplicationFont(fi.absoluteFilePath()); } QFont applicationFont; diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 4e8dd38a..cea9b13e 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -9,8 +9,6 @@ INCLUDEPATH += $$top_srcdir/libnymea-common \ LIBS += -L$$top_builddir/libnymea-app-core/ -lnymea-app-core \ -L$$top_builddir/libnymea-common/ -lnymea-common \ -LIBS += -lssl -lcrypto - win32:Debug:LIBS += -L$$top_builddir/libnymea-app-core/debug \ -L$$top_builddir/libnymea-common/debug win32:Release:LIBS += -L$$top_builddir/libnymea-app-core/release \ diff --git a/nymea-app/ui/connection/CloudLoginPage.qml b/nymea-app/ui/connection/CloudLoginPage.qml index 4c1d4718..0f1d6ae9 100644 --- a/nymea-app/ui/connection/CloudLoginPage.qml +++ b/nymea-app/ui/connection/CloudLoginPage.qml @@ -12,43 +12,57 @@ Page { } ColumnLayout { - anchors.fill: parent + anchors { left: parent.left; top: parent.top; right: parent.right } + visible: Engine.awsClient.isLoggedIn Label { Layout.fillWidth: true + Layout.margins: 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(); + } + } + + ColumnLayout { + anchors { left: parent.left; right: parent.right; top: parent.top } + visible: !Engine.awsClient.isLoggedIn + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins text: "Username (e-mail)" } TextField { id: usernameTextField Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins placeholderText: "john@dummy.com" + inputMethodHints: Qt.ImhEmailCharactersOnly } Label { Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins text: qsTr("Password") } TextField { id: passwordTextField + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins Layout.fillWidth: true + echoMode: TextInput.Password } Button { Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins text: qsTr("OK") enabled: usernameTextField.displayText.length > 0 && passwordTextField.displayText.length > 0 onClicked: { Engine.awsClient.login(usernameTextField.text, passwordTextField.text); } } - - Button { - Layout.fillWidth: true - text: "GetId" - onClicked: Engine.awsClient.getId(); - } - Button { - Layout.fillWidth: true - text: "Post to MQTT" - onClicked: Engine.awsClient.postToMQTT(); - } } } diff --git a/nymea-remoteproxy b/nymea-remoteproxy index a9e79bde..0a18897d 160000 --- a/nymea-remoteproxy +++ b/nymea-remoteproxy @@ -1 +1 @@ -Subproject commit a9e79bde9703c842a7ca7adead22d84bcb9d6f53 +Subproject commit 0a18897de79606543b0e7a68ac9b1990a5dd761c