diff --git a/mea/engine.cpp b/mea/engine.cpp index ad9f34b5..ddbb4256 100644 --- a/mea/engine.cpp +++ b/mea/engine.cpp @@ -92,6 +92,7 @@ void Engine::onConnectedChanged() m_deviceManager->clear(); m_ruleManager->clear(); if (m_jsonRpcClient->connected()) { + qDebug() << "Engine: inital setup required:" << m_jsonRpcClient->initialSetupRequired() << "auth required:" << m_jsonRpcClient->authenticationRequired(); if (!m_jsonRpcClient->initialSetupRequired() && !m_jsonRpcClient->authenticationRequired()) { m_deviceManager->init(); m_ruleManager->init(); diff --git a/mea/jsonrpc/jsonrpcclient.cpp b/mea/jsonrpc/jsonrpcclient.cpp index 39849eed..3d54550e 100644 --- a/mea/jsonrpc/jsonrpcclient.cpp +++ b/mea/jsonrpc/jsonrpcclient.cpp @@ -37,6 +37,8 @@ JsonRpcClient::JsonRpcClient(NymeaConnection *connection, QObject *parent) : { connect(m_connection, &NymeaConnection::connectedChanged, this, &JsonRpcClient::onInterfaceConnectedChanged); connect(m_connection, &NymeaConnection::dataAvailable, this, &JsonRpcClient::dataReceived); + + registerNotificationHandler(this, "notificationReceived"); } QString JsonRpcClient::nameSpace() const @@ -78,6 +80,38 @@ void JsonRpcClient::setNotificationsEnabled(bool enabled) void JsonRpcClient::setNotificationsEnabledResponse(const QVariantMap ¶ms) { qDebug() << "Notifications enabled:" << params; + + m_connected = true; + emit connectedChanged(true); + +} + +void JsonRpcClient::notificationReceived(const QVariantMap &data) +{ + qDebug() << "JsonRpcClient: Notification received" << data; + + //JsonRpcClient: Notification received QMap(("id", QVariant(double, 2))("notification", QVariant(QString, "JSONRPC.PushButtonAuthFinished"))("params", QVariant(QVariantMap, QMap(("success", QVariant(bool, true))("token", QVariant(QString, "FJPaAJ8FEtrqcC+/s0s/lAcDubz0OyEtwbRsyFIWM9c="))("transactionId", QVariant(double, 2)))))) + if (data.value("notification").toString() == "JSONRPC.PushButtonAuthFinished") { + qDebug() << "Push button auth finished."; + if (data.value("params").toMap().value("transactionId").toInt() != m_pendingPushButtonTransaction) { + qDebug() << "This push button transaction is not what we're waiting for..."; + return; + } + m_pendingPushButtonTransaction = -1; + if (data.value("params").toMap().value("success").toBool()) { + qDebug() << "Push button auth succeeded"; + m_token = data.value("params").toMap().value("token").toByteArray(); + QSettings settings; + settings.beginGroup("jsonTokens"); + settings.setValue(m_serverUuid, m_token); + settings.endGroup(); + emit authenticationRequiredChanged(); + + setNotificationsEnabled(true); + } else { + emit pushButtonAuthFailed(); + } + } } bool JsonRpcClient::connected() const @@ -95,6 +129,11 @@ bool JsonRpcClient::authenticationRequired() const return m_authenticationRequired && m_token.isEmpty(); } +bool JsonRpcClient::pushButtonAuthAvailable() const +{ + return m_pushButtonAuthAvailable; +} + int JsonRpcClient::createUser(const QString &username, const QString &password) { QVariantMap params; @@ -118,11 +157,22 @@ int JsonRpcClient::authenticate(const QString &username, const QString &password return reply->commandId(); } +int JsonRpcClient::requestPushButtonAuth(const QString &deviceName) +{ + qDebug() << "Requesting push button auth for device:" << deviceName; + QVariantMap params; + params.insert("deviceName", deviceName); + JsonRpcReply *reply = createReply("JSONRPC.RequestPushButtonAuth", params, this, "processRequestPushButtonAuth"); + m_replies.insert(reply->commandId(), reply); + m_connection->sendData(QJsonDocument::fromVariant(reply->requestMap()).toJson()); + return reply->commandId(); +} + void JsonRpcClient::processAuthenticate(const QVariantMap &data) { - qDebug() << "authenticate response" << data; if (data.value("status").toString() == "success" && data.value("params").toMap().value("success").toBool()) { + qDebug() << "authentication successful"; m_token = data.value("params").toMap().value("token").toByteArray(); QSettings settings; settings.beginGroup("jsonTokens"); @@ -131,6 +181,9 @@ void JsonRpcClient::processAuthenticate(const QVariantMap &data) emit authenticationRequiredChanged(); setNotificationsEnabled(true); + } else { + qWarning() << "Authentication failed"; + emit authenticationFailed(); } } @@ -140,6 +193,19 @@ void JsonRpcClient::processCreateUser(const QVariantMap &data) if (data.value("status").toString() == "success" && data.value("params").toMap().value("error").toString() == "UserErrorNoError") { m_initialSetupRequired = false; emit initialSetupRequiredChanged(); + } else { + qDebug() << "Emitting create user failed"; + emit createUserFailed(data.value("params").toMap().value("error").toString()); + } +} + +void JsonRpcClient::processRequestPushButtonAuth(const QVariantMap &data) +{ + qDebug() << "requestPushButtonAuth response" << data; + if (data.value("status").toString() == "success" && data.value("params").toMap().value("success").toBool()) { + m_pendingPushButtonTransaction = data.value("params").toMap().value("transactionId").toInt(); + } else { + emit pushButtonAuthFailed(); } } @@ -201,48 +267,58 @@ void JsonRpcClient::dataReceived(const QByteArray &data) if (dataMap.value("id").toInt() == 0) { m_initialSetupRequired = dataMap.value("initialSetupRequired").toBool(); m_authenticationRequired = dataMap.value("authenticationRequired").toBool(); + m_pushButtonAuthAvailable = dataMap.value("pushButtonAuthAvailable").toBool(); + qDebug() << "Handshake received" << "initRequired:" << m_initialSetupRequired << "authRequired:" << m_authenticationRequired << "pushButtonAvailable:" << m_pushButtonAuthAvailable;; m_serverUuid = dataMap.value("uuid").toString(); + emit pushButtonAuthAvailableChanged(); QString protoVersionString = dataMap.value("protocol version").toString(); if (!protoVersionString.contains('.')) { protoVersionString.prepend("0."); } + QVersionNumber minimumRequiredVersion = QVersionNumber(1, 0); + QVersionNumber protocolVersion = QVersionNumber::fromString(protoVersionString); + if (protocolVersion < minimumRequiredVersion) { + m_connection->disconnect(); + emit invalidProtocolVersion(protocolVersion.toString(), minimumRequiredVersion.toString()); + return; + } + if (m_initialSetupRequired) { emit initialSetupRequiredChanged(); - } else if (m_authenticationRequired) { + return; + } + + if (m_authenticationRequired) { QSettings settings; settings.beginGroup("jsonTokens"); m_token = settings.value(m_serverUuid).toByteArray(); settings.endGroup(); emit authenticationRequiredChanged(); - if (!m_token.isEmpty()) { - setNotificationsEnabled(true); + if (m_token.isEmpty()) { + return; } } - m_connected = true; - emit connectedChanged(true); - - QVersionNumber minimumRequiredVersion = QVersionNumber(1, 0); - QVersionNumber protocolVersion = QVersionNumber::fromString(protoVersionString); - if (protocolVersion < minimumRequiredVersion) { - m_connection->disconnect(); - emit invalidProtocolVersion(protocolVersion.toString(), minimumRequiredVersion.toString()); - } + setNotificationsEnabled(true); } // check if this is a reply to a request int commandId = dataMap.value("id").toInt(); JsonRpcReply *reply = m_replies.take(commandId); if (reply) { -// qDebug() << QString("JsonRpc: got response for %1.%2: %3").arg(reply->nameSpace(), reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))) << reply->callback() << reply->callback(); + qDebug() << QString("JsonRpc: got response for %1.%2: %3").arg(reply->nameSpace(), reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))) << reply->callback() << reply->callback(); if (dataMap.value("status").toString() == "unauthorized") { qWarning() << "Something's off with the token"; m_authenticationRequired = true; m_token.clear(); + QSettings settings; + settings.beginGroup("jsonTokens"); + settings.setValue(m_serverUuid, m_token); + settings.endGroup(); emit authenticationRequiredChanged(); } diff --git a/mea/jsonrpc/jsonrpcclient.h b/mea/jsonrpc/jsonrpcclient.h index 2f292fe0..06a994a5 100644 --- a/mea/jsonrpc/jsonrpcclient.h +++ b/mea/jsonrpc/jsonrpcclient.h @@ -37,6 +37,7 @@ class JsonRpcClient : public JsonHandler Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(bool initialSetupRequired READ initialSetupRequired NOTIFY initialSetupRequiredChanged) Q_PROPERTY(bool authenticationRequired READ authenticationRequired NOTIFY authenticationRequiredChanged) + Q_PROPERTY(bool pushButtonAuthAvailable READ pushButtonAuthAvailable NOTIFY pushButtonAuthAvailableChanged) public: explicit JsonRpcClient(NymeaConnection *connection, QObject *parent = 0); @@ -52,21 +53,24 @@ public: bool connected() const; bool initialSetupRequired() const; bool authenticationRequired() const; + bool pushButtonAuthAvailable() const; // ui methods 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); - // json handler - Q_INVOKABLE void processAuthenticate(const QVariantMap &data); - Q_INVOKABLE void processCreateUser(const QVariantMap &data); signals: void initialSetupRequiredChanged(); void authenticationRequiredChanged(); + void pushButtonAuthAvailableChanged(); void connectedChanged(bool connected); void tokenChanged(); void invalidProtocolVersion(const QString &actualVersion, const QString &minimumVersion); + void authenticationFailed(); + void pushButtonAuthFailed(); + void createUserFailed(const QString &error); void responseReceived(const int &commandId, const QVariantMap &response); @@ -86,12 +90,22 @@ private: bool m_connected = false; bool m_initialSetupRequired = false; bool m_authenticationRequired = false; + bool m_pushButtonAuthAvailable = false; + int m_pendingPushButtonTransaction = -1; QString m_serverUuid; QByteArray m_token; QByteArray m_receiveBuffer; void setNotificationsEnabled(bool enabled); + + // json handler + Q_INVOKABLE void processAuthenticate(const QVariantMap &data); + Q_INVOKABLE void processCreateUser(const QVariantMap &data); + Q_INVOKABLE void processRequestPushButtonAuth(const QVariantMap &data); + Q_INVOKABLE void setNotificationsEnabledResponse(const QVariantMap ¶ms); + Q_INVOKABLE void notificationReceived(const QVariantMap &data); + void sendRequest(const QVariantMap &request); }; diff --git a/mea/nymeaconnection.cpp b/mea/nymeaconnection.cpp index 260cc327..44f89143 100644 --- a/mea/nymeaconnection.cpp +++ b/mea/nymeaconnection.cpp @@ -88,11 +88,29 @@ void NymeaConnection::onSslErrors(const QList &errors) QByteArray storedFingerPrint = settings.value(m_currentUrl.toString()).toByteArray(); settings.endGroup(); - if (storedFingerPrint == error.certificate().digest(QCryptographicHash::Sha256).toBase64()) { + QByteArray certificateFingerprint; + QByteArray digest = error.certificate().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()); + } + + if (storedFingerPrint == certificateFingerprint) { ignoredErrors.append(error); } else { -// QString cn = error.certificate().issuerInfo(QSslCertificate::CommonName); - emit verifyConnectionCertificate(error.certificate().issuerInfo(QSslCertificate::CommonName).first(), error.certificate().digest(QCryptographicHash::Sha256).toBase64()); + QStringList info; + info << tr("Common Name:") << error.certificate().issuerInfo(QSslCertificate::CommonName); + info << tr("Oragnisation:") <ui/magic/SelectStateDescriptorPage.qml ui/images/select-none.svg ui/images/edit.svg + ui/PushButtonAuthPage.qml + ui/images/dialog-error-symbolic.svg diff --git a/mea/ui/ConnectPage.qml b/mea/ui/ConnectPage.qml index f9ce233b..8d111c8c 100644 --- a/mea/ui/ConnectPage.qml +++ b/mea/ui/ConnectPage.qml @@ -22,7 +22,7 @@ Page { target: Engine.connection onVerifyConnectionCertificate: { print("verify cert!") - certDialog.commonName = commonName + certDialog.issuerInfo = issuerInfo certDialog.fingerprint = fingerprint certDialog.open(); } @@ -156,32 +156,78 @@ Page { Dialog { id: certDialog - width: parent.width * .8 - height: parent.height * .8 + width: Math.min(parent.width * .9, 400) + height: content.height x: (parent.width - width) / 2 y: (parent.height - height) / 2 standardButtons: Dialog.Yes | Dialog.No property var fingerprint - property string commonName + property var issuerInfo ColumnLayout { - anchors.fill: parent + anchors { left: parent.left; right: parent.right; top: parent.top } + height: childrenRect.height + spacing: app.margins + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + name: "../images/dialog-warning-symbolic.svg" + color: app.guhAccent + } + + Label { + id: titleLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: qsTr("Warning") + color: app.guhAccent + font.pixelSize: app.largeFont + } + } + Label { Layout.fillWidth: true wrapMode: Text.WordWrap - text: "The authenticity of this nymea box cannot be verified. Do you want to trust this device?" + text: "The authenticity of this nymea box cannot be verified." } + Label { Layout.fillWidth: true wrapMode: Text.WordWrap - text: "Device name: " + certDialog.commonName + text: "If this is the first time you connect to this box, this is expected. Once you trust a box, you should never see this message again for that one. If you see this message multiple times for the same box, something suspicious is going on!" } + + GridLayout { + columns: 2 + + Repeater { + model: certDialog.issuerInfo + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: modelData + } + } + } + Label { Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: "Fingerprint: " + certDialog.fingerprint } + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: "Do you want to trust this device?" + font.bold: true + } } onAccepted: { diff --git a/mea/ui/LoginPage.qml b/mea/ui/LoginPage.qml index f03e1337..39783abf 100644 --- a/mea/ui/LoginPage.qml +++ b/mea/ui/LoginPage.qml @@ -14,9 +14,37 @@ Page { onBackPressed: root.backPressed() } + + Connections { + target: Engine.jsonRpcClient + onAuthenticationFailed: { + var popup = errorDialog.createObject(root) + popup.text = qsTr("Sorry, that wasn't right. Try again please.") + popup.open(); + } + onCreateUserFailed: { + print("create user failed") + var text + switch (error) { + case "UserErrorInvalidUserId": + text = qsTr("The email you've entered isn't valid."); + break; + case "UserErrorBadPassword": + text = qsTr("The password you've chose is too weak."); + break; + default: + text = qsTr("An error happened creating the user."); + } +// var popup = errorDialog.createObject(root, {title: qsTr("Error creating user"), text: text}) + var popup = errorDialog.createObject(root, {title: "Error creating user", text: text}) + popup.open(); + } + } + ColumnLayout { anchors.fill: parent anchors.margins: app.margins + spacing: app.margins Label { Layout.fillWidth: true @@ -26,29 +54,66 @@ Page { wrapMode: Text.WordWrap } - Label { - text: "Username:" + ColumnLayout { Layout.fillWidth: true + + Label { + text: "Your e-mail address:" + Layout.fillWidth: true + } + TextField { + id: usernameTextField + Layout.fillWidth: true + inputMethodHints: Qt.ImhEmailCharactersOnly + placeholderText: "john.smith@cooldomain.com" + } } - TextField { - id: usernameTextField + ColumnLayout { Layout.fillWidth: true - inputMethodHints: Qt.ImhEmailCharactersOnly + + Label { + Layout.fillWidth: true + text: "Password:" + } + TextField { + id: passwordTextField + Layout.fillWidth: true + echoMode: TextInput.Password + } } + + ColumnLayout { + Layout.fillWidth: true + visible: Engine.jsonRpcClient.initialSetupRequired + + Label { + Layout.fillWidth: true + text: "Confirm password:" + } + TextField { + id: confirmPasswordTextField + Layout.fillWidth: true + echoMode: TextInput.Password + } + } + Label { Layout.fillWidth: true - text: "Password:" - } - TextField { - id: passwordTextField - Layout.fillWidth: true - echoMode: TextInput.Password + visible: Engine.jsonRpcClient.initialSetupRequired + opacity: (passwordTextField.text.length > 0 && passwordTextField.text.length < 8) || passwordTextField.text != confirmPasswordTextField.text ? 1 : 0 + text: passwordTextField.text.length < 8 ? qsTr("This password isn't long enought to be secure, add some more characters please.") + : qsTr("The passwords don't match.") + wrapMode: Text.WordWrap + Layout.preferredHeight: confirmPasswordTextField.height * 2 + color: app.guhAccent } + Button { Layout.fillWidth: true text: "OK" + enabled: usernameTextField.text.length >= 5 && passwordTextField.text.length >= 8 + && (!Engine.jsonRpcClient.initialSetupRequired || confirmPasswordTextField.text == passwordTextField.text) onClicked: { - console.log("foooo") if (Engine.jsonRpcClient.initialSetupRequired) { print("create user") Engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.text); @@ -63,4 +128,11 @@ Page { Layout.fillWidth: true } } + + Component { + id: errorDialog + ErrorDialog { + + } + } } diff --git a/mea/ui/PushButtonAuthPage.qml b/mea/ui/PushButtonAuthPage.qml new file mode 100644 index 00000000..b78fc767 --- /dev/null +++ b/mea/ui/PushButtonAuthPage.qml @@ -0,0 +1,77 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Mea 1.0 +import "components" + +Page { + id: root + signal backPressed(); + + header: GuhHeader { + text: "Welcome to nymea!" + backButtonVisible: true + onBackPressed: { + root.backPressed(); + } + } + + Component.objectName: { + Engine.jsonRpcClient.requestPushButtonAuth(""); + } + + Connections { + target: Engine.jsonRpcClient + onPushButtonAuthFailed: { + var popup = errorDialog.createObject(root) + popup.text = qsTr("Sorry, something went wrong during the setup. Try again please.") + popup.open(); + popup.accepted.connect(function() {root.backPressed()}) + } + } + + ColumnLayout { + anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } + anchors.margins: app.margins + spacing: app.margins + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + + ColorIcon { + height: app.iconSize * 2 + width: height + color: app.guhAccent + name: "../images/info.svg" + } + + Label { + color: app.guhAccent + text: qsTr("Authentication required") + wrapMode: Text.WordWrap + Layout.fillWidth: true + font.pixelSize: app.largeFont + } + } + + + Label { + Layout.fillWidth: true + text: "Please press the button on your nymea box to authenticate this device." + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } + + Component { + id: errorDialog + ErrorDialog { + + } + } +} diff --git a/mea/ui/components/ErrorDialog.qml b/mea/ui/components/ErrorDialog.qml index bf6bf41d..4e9e0931 100644 --- a/mea/ui/components/ErrorDialog.qml +++ b/mea/ui/components/ErrorDialog.qml @@ -1,9 +1,11 @@ import QtQuick 2.8 import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 Dialog { - width: parent.width * .6 - height: parent.height * .6 + id: root + width: Math.min(parent.width * .6, 400) +// height: content.height x: (parent.width - width) / 2 y: (parent.height - height) / 2 @@ -12,9 +14,40 @@ Dialog { standardButtons: Dialog.Ok - Label { - id: contentLabel - width: parent.width - wrapMode: Text.WordWrap + header: Item { + implicitHeight: headerRow.height + app.margins * 2 + implicitWidth: parent.width + RowLayout { + id: headerRow + anchors { left: parent.left; right: parent.right; top: parent.top; margins: app.margins } + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + name: "../images/dialog-error-symbolic.svg" + color: app.guhAccent + } + + Label { + id: titleLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: root.title + color: app.guhAccent + font.pixelSize: app.largeFont + } + } + } + + ColumnLayout { + id: content + anchors { left: parent.left; top: parent.top; right: parent.right } + height: childrenRect.height + + Label { + id: contentLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + } } } diff --git a/mea/ui/images/dialog-error-symbolic.svg b/mea/ui/images/dialog-error-symbolic.svg new file mode 100644 index 00000000..7a9a5d4c --- /dev/null +++ b/mea/ui/images/dialog-error-symbolic.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/mea/ui/main.qml b/mea/ui/main.qml index 3575978f..028ac7b3 100644 --- a/mea/ui/main.qml +++ b/mea/ui/main.qml @@ -42,7 +42,7 @@ ApplicationWindow { Connections { target: Engine.jsonRpcClient onConnectedChanged: { - print("json client connected changed") + print("json client connected changed", Engine.jsonRpcClient.connected) if (Engine.jsonRpcClient.connected) { settings.lastConnectedHost = Engine.connection.url } @@ -68,14 +68,33 @@ ApplicationWindow { } function init() { + print("calling init. Auth required:", Engine.jsonRpcClient.authenticationRequired, "initial setup required:", Engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", Engine.jsonRpcClient.connected) pageStack.clear() discovery.discovering = false; + if (!Engine.connection.connected) { + pageStack.push(Qt.resolvedUrl("ConnectPage.qml")) + print("starting discovery") + discovery.discovering = true; + return; + } + if (Engine.jsonRpcClient.authenticationRequired || Engine.jsonRpcClient.initialSetupRequired) { - var page = pageStack.push(Qt.resolvedUrl("LoginPage.qml")); - page.backPressed.connect(function() { - settings.lastConnectedHost = ""; - Engine.connection.disconnect() - }) + if (Engine.jsonRpcClient.pushButtonAuthAvailable) { + print("opening push button auth") + var page = pageStack.push(Qt.resolvedUrl("PushButtonAuthPage.qml")) + page.backPressed.connect(function() { + settings.lastConnectedHost = ""; + Engine.connection.disconnect(); + init(); + }) + } else { + var page = pageStack.push(Qt.resolvedUrl("LoginPage.qml")); + page.backPressed.connect(function() { + settings.lastConnectedHost = ""; + Engine.connection.disconnect() + init(); + }) + } } else if (Engine.jsonRpcClient.connected) { pageStack.push(Qt.resolvedUrl("MainPage.qml")) } else {