From 8af4cd49d52bb91919dcbef07ee67006eedcdfec Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 28 Sep 2018 11:23:27 +0200 Subject: [PATCH] fix aws login, workaround wss certificate issue --- libnymea-app-core/connection/awsclient.cpp | 18 ++-- .../connection/nymeaconnection.cpp | 97 ++++++++++++++++--- .../connection/nymeaconnection.h | 9 +- .../connection/websockettransport.cpp | 11 ++- nymea-app/ui/appsettings/CloudLoginPage.qml | 16 +-- nymea-app/ui/connection/ConnectPage.qml | 6 +- 6 files changed, 117 insertions(+), 40 deletions(-) diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index 9d2f396f..fd74326f 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -104,10 +104,9 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent), m_userId = settings.value("userId").toString(); m_password = settings.value("password").toString(); m_accessToken = settings.value("accessToken").toByteArray(); - m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime(); +// m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime(); m_idToken = settings.value("idToken").toByteArray(); m_refreshToken = settings.value("refreshToken").toByteArray(); - m_confirmationPending = settings.value("confirmationPending", false).toBool(); m_identityId = settings.value("identityId").toByteArray(); @@ -302,12 +301,6 @@ void AWSClient::signup(const QString &username, const QString &password) emit signupResult(LoginErrorNoError); - QSettings settings; - settings.beginGroup("cloud"); - settings.setValue("username", m_username); - settings.setValue("password", m_password); - settings.setValue("confirmationPending", true); - m_confirmationPending = true; emit confirmationPendingChanged(); }); @@ -364,6 +357,8 @@ void AWSClient::confirmRegistration(const QString &code) return; } + m_confirmationPending = false; + emit confirmationPendingChanged(); emit confirmationResult(LoginErrorNoError); login(m_username, m_password); fetchDevices(); @@ -591,9 +586,6 @@ void AWSClient::getId() return; } m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); - QSettings settings; - settings.beginGroup("cloud"); - settings.setValue("identityId", m_identityId); qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data); getCredentialsForIdentity(m_identityId); @@ -767,6 +759,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) QSettings settings; + qDebug() << "settings has:" << settings.childGroups(); bool newLogin = !settings.childGroups().contains("cloud"); @@ -777,6 +770,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) settings.setValue("password", m_password); settings.setValue("accessToken", m_accessToken); settings.setValue("accessTokenExpiry", m_accessTokenExpiry); + settings.setValue("identityId", m_identityId); settings.setValue("idToken", m_idToken); settings.setValue("refreshToken", m_refreshToken); settings.setValue("accessKeyId", m_accessKeyId); @@ -787,7 +781,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) emit loginResult(LoginErrorNoError); if (newLogin) { -// qDebug() << "new login!"; + qDebug() << "new login!"; emit isLoggedInChanged(); } diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 3679d03e..0f7033f3 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include "nymeatransportinterface.h" @@ -21,21 +24,31 @@ bool NymeaConnection::connect(const QString &url) } m_currentUrl = QUrl(url); + emit currentUrlChanged(); if (!m_transports.contains(m_currentUrl.scheme())) { qWarning() << "Cannot connect to urls of scheme" << m_currentUrl.scheme() << "Supported schemes are" << m_transports.keys(); return false; } - m_currentTransport = m_transports.value(m_currentUrl.scheme())->createTransport(); + // Create a new transport + m_currentTransport = m_transports.value(m_currentUrl.scheme())->createTransport(); QObject::connect(m_currentTransport, &NymeaTransportInterface::sslErrors, this, &NymeaConnection::onSslErrors); QObject::connect(m_currentTransport, &NymeaTransportInterface::error, this, &NymeaConnection::onError); QObject::connect(m_currentTransport, &NymeaTransportInterface::connected, this, &NymeaConnection::onConnected); QObject::connect(m_currentTransport, &NymeaTransportInterface::disconnected, this, &NymeaConnection::onDisconnected); - - // signal forwarding QObject::connect(m_currentTransport, &NymeaTransportInterface::dataReady, this, &NymeaConnection::dataAvailable); - qDebug() << "Should connect to url" << m_currentUrl; + // Load any certificate we might have for this url + QByteArray pem; + if (loadPem(m_currentUrl, pem)) { + qDebug() << "Loaded SSL certificate for" << m_currentUrl.host(); + QList expectedSslErrors; + expectedSslErrors.append(QSslError::HostNameMismatch); + expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, QSslCertificate(pem))); + m_currentTransport->ignoreSslErrors(expectedSslErrors); + } + + qDebug() << "Connecting to:" << m_currentUrl; return m_currentTransport->connect(m_currentUrl); } @@ -48,19 +61,27 @@ void NymeaConnection::disconnect() m_currentTransport->disconnect(); } -void NymeaConnection::acceptCertificate(const QString &url, const QByteArray &fingerprint) +void NymeaConnection::acceptCertificate(const QString &url, const QByteArray &pem) { - QSettings settings; - settings.beginGroup("acceptedCertificates"); - settings.setValue(QUrl(url).host(), fingerprint); - settings.endGroup(); + storePem(url, pem); } bool NymeaConnection::isTrusted(const QString &url) { + // Do we have a legacy fingerprint QSettings settings; settings.beginGroup("acceptedCertificates"); - return settings.contains(QUrl(url).host()); + if (settings.contains(QUrl(url).host())) { + return true; + } + + // Do we have a PEM file? + QByteArray pem; + if (loadPem(url, pem)) { + return true; + } + + return false; } bool NymeaConnection::connected() @@ -103,8 +124,15 @@ void NymeaConnection::onSslErrors(const QList &errors) qDebug() << "Ignoring host mismatch on certificate."; ignoredErrors.append(error); } else if (error.error() == QSslError::SelfSignedCertificate || error.error() == QSslError::CertificateUntrusted) { - qDebug() << "have a self signed certificate." << error.certificate() << error.certificate().issuerInfoAttributes(); + qDebug() << "have a self signed certificate." << error.certificate(); + // Check our cert DB + QByteArray pem; + + + // Keep this for compatibility with old versions for a bit... + // New code will look up the PEM instead and set it before the connection attempt + // However, we want to emit verifyConnectionCertificate in any case here. QSettings settings; settings.beginGroup("acceptedCertificates"); QByteArray storedFingerPrint = settings.value(m_currentUrl.host()).toByteArray(); @@ -119,8 +147,20 @@ void NymeaConnection::onSslErrors(const QList &errors) certificateFingerprint.append(digest.mid(i,1).toHex().toUpper()); } + // Check old style fingerprint storage if (storedFingerPrint == certificateFingerprint) { + qDebug() << "This fingerprint is known to us."; ignoredErrors.append(error); + + // Update the config to use the new system: + storePem(m_currentUrl, error.certificate().toPem()); + + // Check new style PEM storage + } else if (loadPem(m_currentUrl, pem) && pem == error.certificate().toPem()) { + qDebug() << "Found a SSL certificate for this host. Ignoring error."; + ignoredErrors.append(error); + + // Ok... nothing found... Pop up the message } else { QStringList info; info << tr("Common Name:") << error.certificate().issuerInfo(QSslCertificate::CommonName); @@ -132,14 +172,18 @@ void NymeaConnection::onSslErrors(const QList &errors) // info << tr("Name Qualifier:")<< error.certificate().issuerInfo(QSslCertificate::DistinguishedNameQualifier); // info << tr("Email:")<< error.certificate().issuerInfo(QSslCertificate::EmailAddress); - emit verifyConnectionCertificate(m_currentUrl.toString(), info, certificateFingerprint); + emit verifyConnectionCertificate(m_currentUrl.toString(), info, certificateFingerprint, error.certificate().toPem()); } } else { // Reject the connection on all other errors... qDebug() << "SSL Error:" << error.errorString() << error.certificate(); } } - m_currentTransport->ignoreSslErrors(ignoredErrors); + if (ignoredErrors == errors) { + // Note, due to a workaround in the WebSocketTransport we must not call this + // unless we've handled all the errors or the websocket will ignore unhandled errors too... + m_currentTransport->ignoreSslErrors(ignoredErrors); + } } void NymeaConnection::onError(QAbstractSocket::SocketError error) @@ -170,6 +214,33 @@ void NymeaConnection::onDisconnected() emit connectedChanged(false); } +bool NymeaConnection::storePem(const QUrl &host, const QByteArray &pem) +{ + QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + if (!dir.exists()) { + dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + } + QFile certFile(dir.absoluteFilePath(host.host() + ".pem")); + if (!certFile.open(QFile::WriteOnly)) { + return false; + } + certFile.write(pem); + certFile.close(); + return true; +} + +bool NymeaConnection::loadPem(const QUrl &host, QByteArray &pem) +{ + QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + QFile certFile(dir.absoluteFilePath(host.host() + ".pem")); + if (!certFile.open(QFile::ReadOnly)) { + return false; + } + pem.clear(); + pem.append(certFile.readAll()); + return true; +} + void NymeaConnection::registerTransport(NymeaTransportInterfaceFactory *transportFactory) { foreach (const QString &scheme, transportFactory->supportedSchemes()) { diff --git a/libnymea-app-core/connection/nymeaconnection.h b/libnymea-app-core/connection/nymeaconnection.h index 0d3afe1a..c43fb76a 100644 --- a/libnymea-app-core/connection/nymeaconnection.h +++ b/libnymea-app-core/connection/nymeaconnection.h @@ -14,7 +14,7 @@ class NymeaConnection : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) - Q_PROPERTY(QString url READ url NOTIFY connectedChanged) + Q_PROPERTY(QString url READ url NOTIFY currentUrlChanged) Q_PROPERTY(QString hostAddress READ hostAddress NOTIFY connectedChanged) Q_PROPERTY(QString bluetoothAddress READ bluetoothAddress NOTIFY connectedChanged) @@ -25,7 +25,7 @@ public: Q_INVOKABLE bool connect(const QString &url); Q_INVOKABLE void disconnect(); - Q_INVOKABLE void acceptCertificate(const QString &url, const QByteArray &fingerprint); + Q_INVOKABLE void acceptCertificate(const QString &url, const QByteArray &pem); Q_INVOKABLE bool isTrusted(const QString &url); bool connected(); @@ -37,7 +37,8 @@ public: void sendData(const QByteArray &data); signals: - void verifyConnectionCertificate(const QString &url, const QStringList &issuerInfo, const QByteArray &fingerprint); + void currentUrlChanged(); + void verifyConnectionCertificate(const QString &url, const QStringList &issuerInfo, const QByteArray &fingerprint, const QByteArray &pem); void connectedChanged(bool connected); void connectionError(const QString &error); void dataAvailable(const QByteArray &data); @@ -49,6 +50,8 @@ private slots: void onDisconnected(); private: + bool storePem(const QUrl &host, const QByteArray &pem); + bool loadPem(const QUrl &host, QByteArray &pem); private: QHash m_transports; diff --git a/libnymea-app-core/connection/websockettransport.cpp b/libnymea-app-core/connection/websockettransport.cpp index e7ea200c..ff2c56f7 100644 --- a/libnymea-app-core/connection/websockettransport.cpp +++ b/libnymea-app-core/connection/websockettransport.cpp @@ -28,7 +28,7 @@ WebsocketTransport::WebsocketTransport(QObject *parent) : NymeaTransportInterface(parent) { - m_socket = new QWebSocket(QCoreApplication::applicationName(), QWebSocketProtocol::Version13, this); + m_socket = new QWebSocket(QCoreApplication::applicationName(), QWebSocketProtocol::VersionLatest, this); QObject::connect(m_socket, &QWebSocket::connected, this, &WebsocketTransport::connected); QObject::connect(m_socket, &QWebSocket::disconnected, this, &WebsocketTransport::disconnected); @@ -73,7 +73,14 @@ void WebsocketTransport::sendData(const QByteArray &data) void WebsocketTransport::ignoreSslErrors(const QList &errors) { - m_socket->ignoreSslErrors(errors); + // FIXME: We really should provide the exact errors here, like we do on other transports, + // however, for some reason I just fail to connect to any wss:// socket if I specify the + // errors. It would only continue if calling it without errors parameter... + +// m_socket->ignoreSslErrors(errors); + + Q_UNUSED(errors) + m_socket->ignoreSslErrors(); } void WebsocketTransport::onTextMessageReceived(const QString &data) diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index 05e96b1c..30a09d6e 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -86,13 +86,15 @@ Page { secondaryIconName: !model.online ? "../images/cloud-error.svg" : "" onClicked: { - var page = pageStack.push(Qt.resolvedUrl("../connection/ConnectingPage.qml")) - page.cancel.connect(function() { - Engine.connection.disconnect() - pageStack.pop(root, StackView.Immediate); - pageStack.push(discoveryPage) - }) - Engine.connection.connect("cloud://" + model.id) + if (!Engine.connection.connected) { + var page = pageStack.push(Qt.resolvedUrl("../connection/ConnectingPage.qml")) + page.cancel.connect(function() { + Engine.connection.disconnect() + pageStack.pop(root, StackView.Immediate); + pageStack.push(discoveryPage) + }) + Engine.connection.connect("cloud://" + model.id) + } } onDeleteClicked: { diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index 9f093811..f72c488d 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -44,7 +44,7 @@ Page { target: Engine.connection onVerifyConnectionCertificate: { print("verify cert!") - var popup = certDialogComponent.createObject(app, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint}); + var popup = certDialogComponent.createObject(root, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint, pem: pem}); popup.open(); } onConnectionError: { @@ -331,6 +331,7 @@ Page { property string url property var fingerprint property var issuerInfo + property var pem readonly property bool hasOldFingerprint: Engine.connection.isTrusted(url) @@ -419,9 +420,8 @@ Page { } } - onAccepted: { - Engine.connection.acceptCertificate(certDialog.url, certDialog.fingerprint) + Engine.connection.acceptCertificate(certDialog.url, certDialog.pem) root.connectToHost(certDialog.url) } }