diff --git a/libnymea-app/connection/nymeaconnection.cpp b/libnymea-app/connection/nymeaconnection.cpp index 2a061bc0..9ed2c2bd 100644 --- a/libnymea-app/connection/nymeaconnection.cpp +++ b/libnymea-app/connection/nymeaconnection.cpp @@ -69,32 +69,6 @@ NymeaConnection::NymeaConnection(QObject *parent) : QObject(parent) updateActiveBearers(); } -void NymeaConnection::acceptCertificate(const QString &url, const QByteArray &pem) -{ - storePem(url, pem); - if (m_currentHost) { - connectInternal(m_currentHost); - } -} - -bool NymeaConnection::isTrusted(const QString &url) -{ - // Do we have a legacy fingerprint - QSettings settings; - settings.beginGroup("acceptedCertificates"); - if (settings.contains(QUrl(url).host())) { - return true; - } - - // Do we have a PEM file? - QByteArray pem; - if (loadPem(url, pem)) { - return true; - } - - return false; -} - NymeaConnection::BearerTypes NymeaConnection::availableBearerTypes() const { return m_availableBearerTypes; @@ -161,7 +135,6 @@ Connection *NymeaConnection::currentConnection() const if (!m_currentHost || !m_currentTransport) { return nullptr; } - qDebug() << "secure:" << m_transportCandidates.value(m_currentTransport)->secure(); return m_transportCandidates.value(m_currentTransport); } @@ -187,63 +160,8 @@ 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) { - // 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(transport->url().host()).toByteArray(); - settings.endGroup(); - - 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()); - } - - // Ignore self signed certs for connections to localhost - if (QHostAddress(transport->url().host()) == QHostAddress::LocalHost || QHostAddress(transport->url().host()) == QHostAddress::LocalHostIPv6) { - ignoredErrors.append(error); - - // Check old style fingerprint storage - } else if (storedFingerPrint == certificateFingerprint) { - qDebug() << "This fingerprint is known to us."; - ignoredErrors.append(error); - - // Update the config to use the new system: - storePem(transport->url(), error.certificate().toPem()); - - // Check new style PEM storage - } else if (loadPem(transport->url(), 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 { - qDebug() << "Host presents an unknown self signed certificate:" << error.certificate(); - qDebug() << "Asking user for confirmation."; - - QStringList info; - info << tr("Common Name:") << error.certificate().issuerInfo(QSslCertificate::CommonName); - info << tr("Oragnisation:") <url().toString(), info, certificateFingerprint, error.certificate().toPem()); - } + qDebug() << "Ignoring self signed certificate."; + ignoredErrors.append(error); } else { // Reject the connection on all other errors... qDebug() << "SSL Error:" << error.errorString() << error.certificate(); @@ -475,35 +393,6 @@ void NymeaConnection::updateActiveBearers() } } - -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/"); -// qDebug() << "Loading certificates from:" << dir.absoluteFilePath(host.host() + ".pem"); - 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()) { @@ -511,7 +400,7 @@ void NymeaConnection::registerTransport(NymeaTransportInterfaceFactory *transpor } } -void NymeaConnection::connect(NymeaHost *nymeaHost, Connection *connection) +void NymeaConnection::connectToHost(NymeaHost *nymeaHost, Connection *connection) { if (!nymeaHost) { return; @@ -602,15 +491,15 @@ bool NymeaConnection::connectInternal(Connection *connection) QObject::connect(newTransport, &NymeaTransportInterface::disconnected, this, &NymeaConnection::onDisconnected); QObject::connect(newTransport, &NymeaTransportInterface::dataReady, this, &NymeaConnection::dataAvailable); - // Load any certificate we might have for this url - QByteArray pem; - if (loadPem(connection->url(), pem)) { - qDebug() << "Loaded SSL certificate for" << connection->url().host(); - QList expectedSslErrors; - expectedSslErrors.append(QSslError::HostNameMismatch); - expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, QSslCertificate(pem))); - newTransport->ignoreSslErrors(expectedSslErrors); - } +// // Load any certificate we might have for this url +// QByteArray pem; +// if (loadPem(connection->url(), pem)) { +// qDebug() << "Loaded SSL certificate for" << connection->url().host(); +// QList expectedSslErrors; +// expectedSslErrors.append(QSslError::HostNameMismatch); +// expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, QSslCertificate(pem))); +// newTransport->ignoreSslErrors(expectedSslErrors); +// } m_transportCandidates.insert(newTransport, connection); qDebug() << "Connecting to:" << connection->url() << newTransport << m_transportCandidates.value(newTransport); @@ -666,7 +555,20 @@ bool NymeaConnection::isConnectionBearerAvailable(Connection::BearerType connect return false; } -void NymeaConnection::disconnect() +void NymeaConnection::disconnectFromHost() { setCurrentHost(nullptr); } + +bool NymeaConnection::isEncrypted() const +{ + return m_currentTransport && m_currentTransport->isEncrypted(); +} + +QSslCertificate NymeaConnection::sslCertificate() const +{ + if (!m_currentTransport) { + return QSslCertificate(); + } + return m_currentTransport->serverCertificate(); +} diff --git a/libnymea-app/connection/nymeaconnection.h b/libnymea-app/connection/nymeaconnection.h index b8138646..f2a233b7 100644 --- a/libnymea-app/connection/nymeaconnection.h +++ b/libnymea-app/connection/nymeaconnection.h @@ -85,10 +85,11 @@ public: void registerTransport(NymeaTransportInterfaceFactory *transportFactory); - Q_INVOKABLE void connect(NymeaHost* nymeaHost, Connection *connection = nullptr); - Q_INVOKABLE void disconnect(); - Q_INVOKABLE void acceptCertificate(const QString &url, const QByteArray &pem); - Q_INVOKABLE bool isTrusted(const QString &url); + Q_INVOKABLE void connectToHost(NymeaHost* nymeaHost, Connection *connection = nullptr); + Q_INVOKABLE void disconnectFromHost(); + + bool isEncrypted() const; + QSslCertificate sslCertificate() const; NymeaConnection::BearerTypes availableBearerTypes() const; @@ -120,9 +121,6 @@ private slots: void updateActiveBearers(); private: - bool storePem(const QUrl &host, const QByteArray &pem); - bool loadPem(const QUrl &host, QByteArray &pem); - void connectInternal(NymeaHost *host); bool connectInternal(Connection *connection); diff --git a/libnymea-app/connection/nymeahost.cpp b/libnymea-app/connection/nymeahost.cpp index a0816267..b8d437ca 100644 --- a/libnymea-app/connection/nymeahost.cpp +++ b/libnymea-app/connection/nymeahost.cpp @@ -243,6 +243,11 @@ QString Connection::hostAddress() const return m_url.host(); } +int Connection::port() const +{ + return m_url.port(); +} + Connection::BearerType Connection::bearerType() const { return m_bearerType; diff --git a/libnymea-app/connection/nymeahost.h b/libnymea-app/connection/nymeahost.h index 69c135bb..829e6620 100644 --- a/libnymea-app/connection/nymeahost.h +++ b/libnymea-app/connection/nymeahost.h @@ -43,6 +43,7 @@ class Connection: public QObject { Q_OBJECT Q_PROPERTY(QUrl url READ url CONSTANT) Q_PROPERTY(QString hostAddress READ hostAddress CONSTANT) + Q_PROPERTY(int port READ port CONSTANT) Q_PROPERTY(BearerType bearerType READ bearerType CONSTANT) Q_PROPERTY(bool secure READ secure CONSTANT) Q_PROPERTY(QString displayName READ displayName CONSTANT) @@ -68,6 +69,7 @@ public: QUrl url() const; QString hostAddress() const; + int port() const; BearerType bearerType() const; bool secure() const; QString displayName() const; diff --git a/libnymea-app/connection/nymeahosts.cpp b/libnymea-app/connection/nymeahosts.cpp index d11a9d56..6ed0c14f 100644 --- a/libnymea-app/connection/nymeahosts.cpp +++ b/libnymea-app/connection/nymeahosts.cpp @@ -31,7 +31,7 @@ #include "nymeahosts.h" #include "connection/discovery/nymeadiscovery.h" #include "nymeahost.h" -#include "connection/nymeaconnection.h" +#include "jsonrpc/jsonrpcclient.h" #include NymeaHosts::NymeaHosts(QObject *parent) : @@ -186,18 +186,18 @@ void NymeaHostsFilterModel::setDiscovery(NymeaDiscovery *discovery) } } -NymeaConnection *NymeaHostsFilterModel::nymeaConnection() const +JsonRpcClient *NymeaHostsFilterModel::jsonRpcClient() const { - return m_nymeaConnection; + return m_jsonRpcClient; } -void NymeaHostsFilterModel::setNymeaConnection(NymeaConnection *nymeaConnection) +void NymeaHostsFilterModel::setJsonRpcClient(JsonRpcClient *jsonRpcClient) { - if (m_nymeaConnection != nymeaConnection) { - m_nymeaConnection = nymeaConnection; - emit nymeaConnectionChanged(); + if (m_jsonRpcClient != jsonRpcClient) { + m_jsonRpcClient = jsonRpcClient; + emit jsonRpcClientChanged(); - connect(m_nymeaConnection, &NymeaConnection::availableBearerTypesChanged, this, [this](){ + connect(m_jsonRpcClient, &JsonRpcClient::availableBearerTypesChanged, this, [this](){ // qDebug() << "Bearer Types Changed!"; invalidateFilter(); emit countChanged(); @@ -247,24 +247,24 @@ bool NymeaHostsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &s { Q_UNUSED(sourceParent) NymeaHost *host = m_nymeaDiscovery->nymeaHosts()->get(sourceRow); - if (m_nymeaConnection && !m_showUneachableBearers) { + if (m_jsonRpcClient && !m_showUneachableBearers) { bool hasReachableConnection = false; for (int i = 0; i < host->connections()->rowCount(); i++) { // qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url() << "available bearer types:" << m_nymeaConnection->availableBearerTypes() << "hosts bearer types" << host->connections()->get(i)->bearerType(); // Either enable a connection when the Bearer type is directly available switch (host->connections()->get(i)->bearerType()) { case Connection::BearerTypeLan: - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeEthernet); - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeWiFi); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeEthernet); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeWiFi); break; case Connection::BearerTypeWan: case Connection::BearerTypeCloud: - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeEthernet); - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeWiFi); - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeMobileData); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeEthernet); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeWiFi); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeMobileData); break; case Connection::BearerTypeBluetooth: - hasReachableConnection |= m_nymeaConnection->availableBearerTypes().testFlag(NymeaConnection::BearerTypeBluetooth); + hasReachableConnection |= m_jsonRpcClient->availableBearerTypes().testFlag(NymeaConnection::BearerTypeBluetooth); break; case Connection::BearerTypeUnknown: hasReachableConnection = true; diff --git a/libnymea-app/connection/nymeahosts.h b/libnymea-app/connection/nymeahosts.h index 54d42e80..b26cf3d7 100644 --- a/libnymea-app/connection/nymeahosts.h +++ b/libnymea-app/connection/nymeahosts.h @@ -38,7 +38,7 @@ #include "nymeahost.h" class NymeaDiscovery; -class NymeaConnection; +class JsonRpcClient; class NymeaHosts : public QAbstractListModel { @@ -87,7 +87,7 @@ class NymeaHostsFilterModel: public QSortFilterProxyModel Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(NymeaDiscovery* discovery READ discovery WRITE setDiscovery NOTIFY discoveryChanged) - Q_PROPERTY(NymeaConnection* nymeaConnection READ nymeaConnection WRITE setNymeaConnection NOTIFY nymeaConnectionChanged) + Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient WRITE setJsonRpcClient NOTIFY jsonRpcClientChanged) Q_PROPERTY(bool showUnreachableBearers READ showUnreachableBearers WRITE setShowUnreachableBearers NOTIFY showUnreachableBearersChanged) Q_PROPERTY(bool showUnreachableHosts READ showUnreachableHosts WRITE setShowUnreachableHosts NOTIFY showUnreachableHostsChanged) @@ -97,8 +97,8 @@ public: NymeaDiscovery* discovery() const; void setDiscovery(NymeaDiscovery *discovery); - NymeaConnection* nymeaConnection() const; - void setNymeaConnection(NymeaConnection* nymeaConnection); + JsonRpcClient* jsonRpcClient() const; + void setJsonRpcClient(JsonRpcClient* jsonRpcClient); bool showUnreachableBearers() const; void setShowUnreachableBearers(bool showUnreachableBearers); @@ -111,7 +111,7 @@ public: signals: void countChanged(); void discoveryChanged(); - void nymeaConnectionChanged(); + void jsonRpcClientChanged(); void showUnreachableBearersChanged(); void showUnreachableHostsChanged(); @@ -120,7 +120,7 @@ protected: private: NymeaDiscovery *m_nymeaDiscovery = nullptr; - NymeaConnection *m_nymeaConnection = nullptr; + JsonRpcClient *m_jsonRpcClient = nullptr; bool m_showUneachableBearers = false; bool m_showUneachableHosts = false; diff --git a/libnymea-app/connection/nymeatransportinterface.h b/libnymea-app/connection/nymeatransportinterface.h index 155bdda4..bc1ba961 100644 --- a/libnymea-app/connection/nymeatransportinterface.h +++ b/libnymea-app/connection/nymeatransportinterface.h @@ -66,6 +66,8 @@ public: virtual ConnectionState connectionState() const = 0; virtual void sendData(const QByteArray &data) = 0; virtual void ignoreSslErrors(const QList &errors) { Q_UNUSED(errors) } + virtual bool isEncrypted() const { return false; } + virtual QSslCertificate serverCertificate() const { return QSslCertificate(); } signals: void connected(); diff --git a/libnymea-app/connection/tcpsockettransport.cpp b/libnymea-app/connection/tcpsockettransport.cpp index a1c1f009..0a93c7e4 100644 --- a/libnymea-app/connection/tcpsockettransport.cpp +++ b/libnymea-app/connection/tcpsockettransport.cpp @@ -31,6 +31,7 @@ #include "tcpsockettransport.h" #include +#include TcpSocketTransport::TcpSocketTransport(QObject *parent) : NymeaTransportInterface(parent) { @@ -58,6 +59,16 @@ void TcpSocketTransport::ignoreSslErrors(const QList &errors) m_socket.ignoreSslErrors(errors); } +bool TcpSocketTransport::isEncrypted() const +{ + return m_socket.isEncrypted(); +} + +QSslCertificate TcpSocketTransport::serverCertificate() const +{ + return m_socket.peerCertificate(); +} + void TcpSocketTransport::onConnected() { if (m_url.scheme() == "nymea") { diff --git a/libnymea-app/connection/tcpsockettransport.h b/libnymea-app/connection/tcpsockettransport.h index 371766a6..06d01289 100644 --- a/libnymea-app/connection/tcpsockettransport.h +++ b/libnymea-app/connection/tcpsockettransport.h @@ -56,6 +56,8 @@ public: void disconnect() override; void sendData(const QByteArray &data) override; void ignoreSslErrors(const QList &errors) override; + bool isEncrypted() const override; + QSslCertificate serverCertificate() const override; private slots: void onConnected(); diff --git a/libnymea-app/connection/websockettransport.cpp b/libnymea-app/connection/websockettransport.cpp index a5056e37..033a4929 100644 --- a/libnymea-app/connection/websockettransport.cpp +++ b/libnymea-app/connection/websockettransport.cpp @@ -99,6 +99,16 @@ void WebsocketTransport::ignoreSslErrors(const QList &errors) m_socket->ignoreSslErrors(); } +bool WebsocketTransport::isEncrypted() const +{ + return !m_socket->sslConfiguration().isNull(); +} + +QSslCertificate WebsocketTransport::serverCertificate() const +{ + return m_socket->sslConfiguration().peerCertificate(); +} + void WebsocketTransport::onTextMessageReceived(const QString &data) { emit dataReady(data.toUtf8()); diff --git a/libnymea-app/connection/websockettransport.h b/libnymea-app/connection/websockettransport.h index d6ee8025..3764cd3b 100644 --- a/libnymea-app/connection/websockettransport.h +++ b/libnymea-app/connection/websockettransport.h @@ -56,6 +56,9 @@ public: void sendData(const QByteArray &data) override; void ignoreSslErrors(const QList &errors) override; + bool isEncrypted() const override; + QSslCertificate serverCertificate() const override; + private: QUrl m_url; QWebSocket *m_socket; diff --git a/libnymea-app/deviceclasses.cpp b/libnymea-app/deviceclasses.cpp index 713d3e49..24938f13 100644 --- a/libnymea-app/deviceclasses.cpp +++ b/libnymea-app/deviceclasses.cpp @@ -108,7 +108,6 @@ void DeviceClasses::addDeviceClass(DeviceClass *deviceClass) void DeviceClasses::clearModel() { beginResetModel(); - qDebug() << "Devices: delete all deviceClasses"; qDeleteAll(m_deviceClasses); m_deviceClasses.clear(); endResetModel(); diff --git a/libnymea-app/devices.cpp b/libnymea-app/devices.cpp index 9eafcb1f..43ea45b8 100644 --- a/libnymea-app/devices.cpp +++ b/libnymea-app/devices.cpp @@ -134,7 +134,6 @@ void Devices::removeDevice(Device *device) void Devices::clearModel() { beginResetModel(); - qDebug() << "Devices: delete all devices"; qDeleteAll(m_devices); m_devices.clear(); endResetModel(); diff --git a/libnymea-app/engine.cpp b/libnymea-app/engine.cpp index f3d07012..533504ee 100644 --- a/libnymea-app/engine.cpp +++ b/libnymea-app/engine.cpp @@ -39,15 +39,9 @@ #include "system/systemcontroller.h" #include "configuration/networkmanager.h" -#include "connection/tcpsockettransport.h" -#include "connection/websockettransport.h" -#include "connection/bluetoothtransport.h" -#include "connection/cloudtransport.h" - Engine::Engine(QObject *parent) : QObject(parent), - m_connection(new NymeaConnection(this)), - m_jsonRpcClient(new JsonRpcClient(m_connection, this)), + m_jsonRpcClient(new JsonRpcClient(this)), m_deviceManager(new DeviceManager(m_jsonRpcClient, this)), m_ruleManager(new RuleManager(m_jsonRpcClient, this)), m_scriptManager(new ScriptManager(m_jsonRpcClient, this)), @@ -56,10 +50,6 @@ Engine::Engine(QObject *parent) : m_nymeaConfiguration(new NymeaConfiguration(m_jsonRpcClient, this)), m_systemController(new SystemController(m_jsonRpcClient, this)) { - m_connection->registerTransport(new TcpSocketTransportFactory()); - m_connection->registerTransport(new WebsocketTransportFactory()); - m_connection->registerTransport(new BluetoothTransportFactoy()); - m_connection->registerTransport(new CloudTransportFactory()); connect(m_jsonRpcClient, &JsonRpcClient::connectedChanged, this, &Engine::onConnectedChanged); @@ -138,11 +128,6 @@ void Engine::deployCertificate() }); } -NymeaConnection *Engine::connection() const -{ - return m_connection; -} - void Engine::onConnectedChanged() { qDebug() << "Engine: connected changed:" << m_jsonRpcClient->connected(); diff --git a/libnymea-app/engine.h b/libnymea-app/engine.h index 70e87dd0..1f707854 100644 --- a/libnymea-app/engine.h +++ b/libnymea-app/engine.h @@ -49,7 +49,6 @@ class NetworkManager; class Engine : public QObject { Q_OBJECT - Q_PROPERTY(NymeaConnection* connection READ connection CONSTANT) Q_PROPERTY(DeviceManager* deviceManager READ deviceManager CONSTANT) Q_PROPERTY(RuleManager* ruleManager READ ruleManager CONSTANT) Q_PROPERTY(ScriptManager* scriptManager READ scriptManager CONSTANT) @@ -64,7 +63,6 @@ public: bool connected() const; QString connectedHost() const; - NymeaConnection *connection() const; DeviceManager *deviceManager() const; RuleManager *ruleManager() const; ScriptManager *scriptManager() const; @@ -77,7 +75,6 @@ public: Q_INVOKABLE void deployCertificate(); private: - NymeaConnection *m_connection; JsonRpcClient *m_jsonRpcClient; DeviceManager *m_deviceManager; RuleManager *m_ruleManager; diff --git a/libnymea-app/jsonrpc/jsonrpcclient.cpp b/libnymea-app/jsonrpc/jsonrpcclient.cpp index afd17e94..6df4d6ae 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app/jsonrpc/jsonrpcclient.cpp @@ -33,6 +33,11 @@ #include "types/param.h" #include "types/params.h" +#include "connection/tcpsockettransport.h" +#include "connection/websockettransport.h" +#include "connection/bluetoothtransport.h" +#include "connection/cloudtransport.h" + #include #include #include @@ -41,13 +46,24 @@ #include #include #include +#include +#include -JsonRpcClient::JsonRpcClient(NymeaConnection *connection, QObject *parent) : +JsonRpcClient::JsonRpcClient(QObject *parent) : JsonHandler(parent), - m_id(0), - m_connection(connection) + m_id(0) { + m_connection = new NymeaConnection(this); + m_connection->registerTransport(new TcpSocketTransportFactory()); + m_connection->registerTransport(new WebsocketTransportFactory()); + m_connection->registerTransport(new BluetoothTransportFactoy()); + m_connection->registerTransport(new CloudTransportFactory()); + + connect(m_connection, &NymeaConnection::availableBearerTypesChanged, this, &JsonRpcClient::availableBearerTypesChanged); + connect(m_connection, &NymeaConnection::connectionStatusChanged, this, &JsonRpcClient::connectionStatusChanged); connect(m_connection, &NymeaConnection::connectedChanged, this, &JsonRpcClient::onInterfaceConnectedChanged); + connect(m_connection, &NymeaConnection::currentHostChanged, this, &JsonRpcClient:: currentHostChanged); + connect(m_connection, &NymeaConnection::currentConnectionChanged, this, &JsonRpcClient:: currentConnectionChanged); connect(m_connection, &NymeaConnection::dataAvailable, this, &JsonRpcClient::dataReceived); registerNotificationHandler(this, "notificationReceived"); @@ -89,6 +105,32 @@ int JsonRpcClient::sendCommand(const QString &method, QObject *caller, const QSt return sendCommand(method, QVariantMap(), caller, callbackMethod); } +NymeaConnection::BearerTypes JsonRpcClient::availableBearerTypes() const +{ + return m_connection->availableBearerTypes(); +} + +NymeaConnection::ConnectionStatus JsonRpcClient::connectionStatus() const +{ + return m_connection->connectionStatus(); +} + +void JsonRpcClient::connectToHost(NymeaHost *host, Connection *connection) +{ + m_connection->connectToHost(host, connection); +} + +void JsonRpcClient::disconnectFromHost() +{ + m_connection->disconnectFromHost(); +} + +void JsonRpcClient::acceptCertificate(const QString &serverUuid, const QByteArray &pem) +{ + qDebug() << "Pinning new certificate for" << serverUuid; + storePem(serverUuid, pem); +} + void JsonRpcClient::getCloudConnectionStatus() { JsonRpcReply *reply = createReply("JSONRPC.IsCloudConnected", QVariantMap(), this, "isCloudConnectedReply"); @@ -174,6 +216,37 @@ bool JsonRpcClient::connected() const return m_connected; } +NymeaHost *JsonRpcClient::currentHost() const +{ + return m_connection->currentHost(); +} + +Connection *JsonRpcClient::currentConnection() const +{ + return m_connection->currentConnection(); +} + +QVariantMap JsonRpcClient::certificateIssuerInfo() const +{ + QVariantMap issuerInfo; + foreach (const QByteArray &attr, m_connection->sslCertificate().issuerInfoAttributes()) { + issuerInfo.insert(attr, m_connection->sslCertificate().issuerInfo(attr)); + } + + QByteArray certificateFingerprint; + QByteArray digest = m_connection->sslCertificate().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()); + } + + issuerInfo.insert("fingerprint", certificateFingerprint); + + return issuerInfo; +} + bool JsonRpcClient::initialSetupRequired() const { return m_initialSetupRequired; @@ -377,6 +450,33 @@ void JsonRpcClient::sendRequest(const QVariantMap &request) m_connection->sendData(QJsonDocument::fromVariant(newRequest).toJson(QJsonDocument::Compact) + "\n"); } +bool JsonRpcClient::loadPem(const QUuid &serverUud, QByteArray &pem) +{ + QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + QFile certFile(dir.absoluteFilePath(serverUud.toString().remove(QRegExp("[{}]")) + ".pem")); + if (!certFile.open(QFile::ReadOnly)) { + return false; + } + pem.clear(); + pem.append(certFile.readAll()); + return true; +} + +bool JsonRpcClient::storePem(const QUuid &serverUuid, const QByteArray &pem) +{ + QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + if (!dir.exists()) { + dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + } + QFile certFile(dir.absoluteFilePath(serverUuid.toString().remove(QRegExp("[{}]")) + ".pem")); + if (!certFile.open(QFile::WriteOnly | QFile::Truncate)) { + return false; + } + certFile.write(pem); + certFile.close(); + return true; +} + void JsonRpcClient::onInterfaceConnectedChanged(bool connected) { @@ -497,6 +597,34 @@ void JsonRpcClient::helloReply(const QVariantMap ¶ms) return; } + // Verify SSL certificate + if (m_connection->isEncrypted()) { + QByteArray pem; + if (!loadPem(m_serverUuid, pem)) { + qDebug() << "No SSL certificate for this host stored. Accepting and pinning new certificate."; + // No certificate yet! Inform ui about it. + emit newSslCertificate(); + storePem(m_serverUuid, m_connection->sslCertificate().toPem()); + } else { + // We have a certificate pinned already. Check if it's the same + if (m_connection->sslCertificate().toPem() != pem) { + // Uh oh, the certificate has changed + qWarning() << "This connections certificate has changed!"; + + QSslCertificate certificate = m_connection->sslCertificate(); + QVariantMap issuerInfo = certificateIssuerInfo(); + emit verifyConnectionCertificate(m_serverUuid, issuerInfo, certificate.toPem()); + + // Reject the connection until the UI explicitly accepts this... + m_connection->disconnectFromHost(); + + return; + } + qDebug() << "This connections certificate is trusted."; + } + } + + emit handshakeReceived(); if (m_connection->currentHost()->uuid().isNull()) { diff --git a/libnymea-app/jsonrpc/jsonrpcclient.h b/libnymea-app/jsonrpc/jsonrpcclient.h index 1b193c54..85854c23 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.h +++ b/libnymea-app/jsonrpc/jsonrpcclient.h @@ -46,7 +46,11 @@ class Params; class JsonRpcClient : public JsonHandler { Q_OBJECT + Q_PROPERTY(NymeaConnection::BearerTypes availableBearerTypes READ availableBearerTypes NOTIFY availableBearerTypesChanged) + Q_PROPERTY(NymeaConnection::ConnectionStatus connectionStatus READ connectionStatus NOTIFY connectionStatusChanged) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(NymeaHost* currentHost READ currentHost NOTIFY currentHostChanged) + Q_PROPERTY(Connection* currentConnection READ currentConnection NOTIFY currentConnectionChanged) Q_PROPERTY(bool initialSetupRequired READ initialSetupRequired NOTIFY initialSetupRequiredChanged) Q_PROPERTY(bool authenticationRequired READ authenticationRequired NOTIFY authenticationRequiredChanged) Q_PROPERTY(bool pushButtonAuthAvailable READ pushButtonAuthAvailable NOTIFY pushButtonAuthAvailableChanged) @@ -57,6 +61,7 @@ class JsonRpcClient : public JsonHandler Q_PROPERTY(QString serverUuid READ serverUuid NOTIFY handshakeReceived) Q_PROPERTY(QString serverQtVersion READ serverQtVersion NOTIFY serverQtVersionChanged) Q_PROPERTY(QString serverQtBuildVersion READ serverQtBuildVersion NOTIFY serverQtVersionChanged) + Q_PROPERTY(QVariantMap certificateIssuerInfo READ certificateIssuerInfo NOTIFY currentConnectionChanged) public: enum CloudConnectionState { @@ -67,7 +72,7 @@ public: }; Q_ENUM(CloudConnectionState) - explicit JsonRpcClient(NymeaConnection *connection, QObject *parent = nullptr); + explicit JsonRpcClient(QObject *parent = nullptr); QString nameSpace() const override; @@ -77,8 +82,12 @@ public: int sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller = nullptr, const QString &callbackMethod = QString()); int sendCommand(const QString &method, QObject *caller = nullptr, const QString &callbackMethod = QString()); - void setConnection(NymeaConnection *connection); + NymeaConnection::BearerTypes availableBearerTypes() const; + NymeaConnection::ConnectionStatus connectionStatus() const; bool connected() const; + NymeaHost* currentHost() const; + Connection* currentConnection() const; + QVariantMap certificateIssuerInfo() const; bool initialSetupRequired() const; bool authenticationRequired() const; bool pushButtonAuthAvailable() const; @@ -93,20 +102,31 @@ public: QString serverQtBuildVersion(); // ui methods + Q_INVOKABLE void connectToHost(NymeaHost *host, Connection *connection = nullptr); + Q_INVOKABLE void disconnectFromHost(); + Q_INVOKABLE void acceptCertificate(const QString &serverUuid, const QByteArray &pem); + + Q_INVOKABLE bool ensureServerVersion(const QString &jsonRpcVersion); + 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); signals: + void availableBearerTypesChanged(); + void connectionStatusChanged(); + void connectedChanged(bool connected); + void currentHostChanged(); + void currentConnectionChanged(); void handshakeReceived(); + void newSslCertificate(); + void verifyConnectionCertificate(const QString &serverUuid, const QVariantMap &issuerInfo, const QByteArray &pem); void initialSetupRequiredChanged(); void authenticationRequiredChanged(); void pushButtonAuthAvailableChanged(); void authenticatedChanged(); - void connectedChanged(bool connected); void tokenChanged(); void invalidProtocolVersion(const QString &actualVersion, const QString &minimumVersion); void authenticationFailed(); @@ -166,6 +186,9 @@ private: void sendRequest(const QVariantMap &request); + bool loadPem(const QUuid &serverUud, QByteArray &pem); + bool storePem(const QUuid &serverUuid, const QByteArray &pem); + }; diff --git a/libnymea-app/models/taglistmodel.cpp b/libnymea-app/models/taglistmodel.cpp index 2d1d177e..031306b9 100644 --- a/libnymea-app/models/taglistmodel.cpp +++ b/libnymea-app/models/taglistmodel.cpp @@ -124,7 +124,6 @@ void TagListModel::update() } } - qDebug() << "Model populated" << m_list.count() << this; endResetModel(); emit countChanged(); } diff --git a/libnymea-app/system/systemcontroller.cpp b/libnymea-app/system/systemcontroller.cpp index ef513d2b..e52293dc 100644 --- a/libnymea-app/system/systemcontroller.cpp +++ b/libnymea-app/system/systemcontroller.cpp @@ -200,7 +200,6 @@ void SystemController::setAutomaticTime(bool automaticTime) void SystemController::getCapabilitiesResponse(const QVariantMap &data) { - qDebug() << "capabilities received" << data; m_powerManagementAvailable = data.value("params").toMap().value("powerManagement").toBool(); emit powerManagementAvailableChanged(); @@ -219,6 +218,8 @@ void SystemController::getCapabilitiesResponse(const QVariantMap &data) if (m_jsonRpcClient->ensureServerVersion("4.1")) { m_jsonRpcClient->sendCommand("System.GetTime", this, "getServerTimeResponse"); } + + qDebug() << "nymea:core capabilities: Power management:" << m_powerManagementAvailable << "Update management:" << m_updateManagementAvailable << "Time management:" << m_timeManagementAvailable; } void SystemController::getUpdateStatusResponse(const QVariantMap &data) diff --git a/libnymea-app/types/plugins.cpp b/libnymea-app/types/plugins.cpp index be9ded3f..5cc7a243 100644 --- a/libnymea-app/types/plugins.cpp +++ b/libnymea-app/types/plugins.cpp @@ -97,7 +97,6 @@ void Plugins::addPlugin(Plugin *plugin) void Plugins::clearModel() { beginResetModel(); - qDebug() << "Plugins: delete all plugins"; qDeleteAll(m_plugins); m_plugins.clear(); endResetModel(); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 245fca5d..4ec8b6e3 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -212,5 +212,6 @@ ui/components/SettingsPageBase.qml ui/components/SettingsPageSectionHeader.qml ui/grouping/GroupInterfacesPage.qml + ui/connection/CertificateErrorDialog.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index d87f31b5..0bb0096c 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -45,10 +45,10 @@ Page { title: swipeView.currentItem.title leftButtonVisible: true leftButtonImageSource: { - switch (engine.connection.currentConnection.bearerType) { + switch (engine.jsonRpcClient.currentConnection.bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: - if (engine.connection.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { + if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "../images/network-wired.svg" } return "../images/network-wifi.svg"; @@ -62,7 +62,7 @@ Page { return "" } onLeftButtonClicked: { - var dialog = connectionDialogComponent.createObject(root, {headerIcon: leftButtonImageSource}) + var dialog = connectionDialogComponent.createObject(root) dialog.open(); } @@ -404,8 +404,25 @@ Page { id: connectionDialogComponent MeaDialog { id: connectionDialog - title: engine.connection.currentHost.name + title: engine.jsonRpcClient.currentHost.name standardButtons: Dialog.NoButton + headerIcon: { + switch (engine.jsonRpcClient.currentConnection.bearerType) { + case Connection.BearerTypeLan: + case Connection.BearerTypeWan: + if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { + return "../images/network-wired.svg" + } + return "../images/network-wifi.svg"; + case Connection.BearerTypeBluetooth: + return "../images/network-wifi.svg"; + case Connection.BearerTypeCloud: + return "../images/cloud.svg" + case Connection.BearerTypeLoopback: + return "../images/network-wired.svg" + } + return "" + } Label { Layout.fillWidth: true @@ -417,27 +434,11 @@ Page { } Label { Layout.fillWidth: true - text: engine.connection.currentHost.name + text: engine.jsonRpcClient.currentHost.name elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } - Label { - Layout.fillWidth: true - text: engine.connection.currentHost.uuid - font.pixelSize: app.smallFont - elide: Text.ElideRight - color: Material.color(Material.Grey) - horizontalAlignment: Text.AlignHCenter - } - Label { - Layout.fillWidth: true - text: engine.connection.currentConnection.url - font.pixelSize: app.smallFont - elide: Text.ElideRight - color: Material.color(Material.Grey) - horizontalAlignment: Text.AlignHCenter - } Item { Layout.fillWidth: true @@ -445,17 +446,46 @@ Page { } RowLayout { - Layout.fillWidth: true - Button { - id: cancelButton - text: qsTr("OK") - Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) - onClicked: connectionDialog.close() + ColumnLayout { + Label { + Layout.fillWidth: true + text: engine.jsonRpcClient.currentHost.uuid + font.pixelSize: app.smallFont + elide: Text.ElideRight + color: Material.color(Material.Grey) +// horizontalAlignment: Text.AlignHCenter + } + Label { + Layout.fillWidth: true + text: engine.jsonRpcClient.currentConnection.url + font.pixelSize: app.smallFont + elide: Text.ElideRight + color: Material.color(Material.Grey) +// horizontalAlignment: Text.AlignHCenter + } } + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: engine.jsonRpcClient.currentConnection.secure ? "../images/lock-closed.svg" : "../images/lock-open.svg" + MouseArea { + anchors.fill: parent + onClicked: { + var component = Qt.createComponent(Qt.resolvedUrl("connection/CertificateDialog.qml")); + var popup = component.createObject(app, {serverUuid: engine.jsonRpcClient.serverUuid, issuerInfo: engine.jsonRpcClient.certificateIssuerInfo}); + popup.open(); + } + } + } + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + Layout.preferredHeight: app.margins + } + + RowLayout { + Layout.fillWidth: true Button { id: disconnectButton @@ -463,9 +493,18 @@ Page { Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) onClicked: { tabSettings.lastConnectedHost = ""; - engine.connection.disconnect(); + engine.jsonRpcClient.disconnectFromHost(); } } + Item { + Layout.fillWidth: true + } + Button { + id: cancelButton + text: qsTr("OK") + Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) + onClicked: connectionDialog.close() + } } } } diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 10ce1858..09bb1b29 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -111,7 +111,7 @@ Item { readonly property Engine engine: engineObject readonly property Engine _engine: engineObject // In case a child cannot use "engine" property int connectionTabIndex: index -// onConnectionTabIndexChanged: tabSettings.lastConnectedHost = engine.connection.url +// onConnectionTabIndexChanged: tabSettings.lastConnectedHost = engine.jsonRpcClient.url Binding { target: AWSClient @@ -122,7 +122,7 @@ Item { Binding { target: _discovery property: "discovering" - value: engine.connection.currentHost === null + value: engine.jsonRpcClient.currentHost === null } StackView { @@ -136,12 +136,12 @@ Item { setupPushNotifications(); if (autoConnectHost.length > 0) { var host = discovery.nymeaHosts.createLanHost("Manual connection", autoConnectHost); - engine.connection.connect(host) + engine.jsonRpcClient.connectToHost(host) } else if (tabSettings.lastConnectedHost.length > 0) { print("Last connected host was", tabSettings.lastConnectedHost) var cachedHost = discovery.nymeaHosts.find(tabSettings.lastConnectedHost); if (cachedHost) { - engine.connection.connect(cachedHost) + engine.jsonRpcClient.connectToHost(cachedHost) return; } print("Warning: There is a last connected host but UUID is unknown to discovery...") @@ -153,9 +153,9 @@ Item { Timer { running: true; repeat: false; interval: 3000; onTriggered: PlatformHelper.hideSplashScreen(); } function init() { - print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.connection.currentHost) + print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.jsonRpcClient.currentHost) pageStack.clear() - if (!engine.connection.currentHost) { + if (!engine.jsonRpcClient.currentHost) { print("pushing ConnectPage") pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) PlatformHelper.hideSplashScreen(); @@ -169,7 +169,7 @@ Item { var page = pageStack.push(Qt.resolvedUrl("PushButtonAuthPage.qml")) page.backPressed.connect(function() { tabSettings.lastConnectedHost = ""; - engine.connection.disconnect(); + engine.jsonRpcClient.disconnectFromHost(); init(); }) return; @@ -178,7 +178,7 @@ Item { var page = pageStack.push(Qt.resolvedUrl("connection/SetupWizard.qml")); page.backPressed.connect(function() { tabSettings.lastConnectedHost = ""; - engine.connection.disconnect() + engine.jsonRpcClient.disconnectFromHost() init(); }) return; @@ -187,7 +187,7 @@ Item { var page = pageStack.push(Qt.resolvedUrl("connection/LoginPage.qml")); page.backPressed.connect(function() { tabSettings.lastConnectedHost = ""; - engine.connection.disconnect() + engine.jsonRpcClient.disconnectFromHost() init(); }) return; @@ -200,10 +200,9 @@ Item { return; } - print("pushing ConnectingPage") var page = pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml")); page.cancel.connect(function(){ - engine.connection.disconnect(); + engine.jsonRpcClient.disconnectFromHost(); }) } @@ -243,25 +242,24 @@ Item { } Connections { - target: engine.connection + target: engine.jsonRpcClient onCurrentHostChanged: { init(); } onVerifyConnectionCertificate: { - print("verify cert!") - var certDialogComponent = Qt.createComponent(Qt.resolvedUrl("connection/CertificateDialog.qml")); - var popup = certDialogComponent.createObject(root, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint, pem: pem}); + print("Asking user to verify certificate:", serverUuid, issuerInfo, pem) + var certDialogComponent = Qt.createComponent(Qt.resolvedUrl("connection/CertificateErrorDialog.qml")); + var popup = certDialogComponent.createObject(root); + popup.accepted.connect(function(){ + engine.jsonRpcClient.acceptCertificate(serverUuid, pem); + engine.jsonRpcClient.connectToHost(discovery.nymeaHosts.find(serverUuid)); + }) popup.open(); } - } - - - Connections { - target: engine.jsonRpcClient onConnectedChanged: { print("json client connected changed", engine.jsonRpcClient.connected) if (engine.jsonRpcClient.connected) { - discovery.cacheHost(engine.connection.currentHost) + discovery.cacheHost(engine.jsonRpcClient.currentHost) tabSettings.lastConnectedHost = engine.jsonRpcClient.serverUuid } init(); @@ -348,7 +346,7 @@ Item { Layout.fillWidth: true text: qsTr("OK") onClicked: { - engine.connection.disconnect(); + engine.jsonRpcClient.disconnectFromHost(); popup.close() } } diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index a3a7abf2..a1eabb67 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -124,10 +124,10 @@ SettingsPageBase { secondaryIconName: !model.online ? "../images/cloud-error.svg" : "" onClicked: { - print("clicked, connected:", engine.connection.connected, model.id) - if (!engine.connection.connected) { + print("clicked, connected:", engine.jsonRpcClient.connected, model.id) + if (!engine.jsonRpcClient.connected) { var host = discovery.nymeaHosts.find(model.id) - engine.connection.connect(host); + engine.jsonRpcClient.connectToHost(host); } } diff --git a/nymea-app/ui/connection/CertificateDialog.qml b/nymea-app/ui/connection/CertificateDialog.qml index 72886b7b..85fcf792 100644 --- a/nymea-app/ui/connection/CertificateDialog.qml +++ b/nymea-app/ui/connection/CertificateDialog.qml @@ -40,14 +40,10 @@ Dialog { width: Math.min(parent.width * .9, 400) x: (parent.width - width) / 2 y: (parent.height - height) / 2 - standardButtons: Dialog.Yes | Dialog.No + standardButtons: Dialog.Ok - property string url - property var fingerprint + property string serverUuid property var issuerInfo - property var pem - - readonly property bool hasOldFingerprint: engine.connection.isTrusted(url) ColumnLayout { id: certLayout @@ -60,84 +56,68 @@ Dialog { ColorIcon { Layout.preferredHeight: app.iconSize * 2 Layout.preferredWidth: height - name: certDialog.hasOldFingerprint ? "../images/lock-broken.svg" : "../images/info.svg" - color: certDialog.hasOldFingerprint ? "red" : app.accentColor + name: "../images/lock-closed.svg" + color: app.accentColor } Label { id: titleLabel Layout.fillWidth: true wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Warning") : qsTr("Hi there!") - color: certDialog.hasOldFingerprint ? "red" : app.accentColor + text: qsTr("Certificate information") + color: app.accentColor font.pixelSize: app.largeFont } } - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("The certificate of this %1:core has changed!").arg(app.systemName) : qsTr("It seems this is the first time you connect to this %1:core.").arg(app.systemName) - } - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Did you change the system's configuration? Verify if this information is correct.") : qsTr("This is the certificate for this %1:core. Once you trust it, an encrypted connection will be established.").arg(app.systemName) - } - - ThinDivider {} Item { Layout.fillWidth: true - Layout.fillHeight: true - implicitHeight: certGridLayout.implicitHeight - Flickable { - anchors.fill: parent - contentHeight: certGridLayout.implicitHeight - clip: true - - ScrollBar.vertical: ScrollBar { - policy: contentHeight > height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded - } - - GridLayout { - id: certGridLayout - columns: 2 - width: parent.width - - Repeater { - model: certDialog.issuerInfo - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: modelData - } - } - Label { - Layout.fillWidth: true - Layout.columnSpan: 2 - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - text: qsTr("Fingerprint: ") + certDialog.fingerprint - } - } - } + Layout.preferredHeight: app.margins } - ThinDivider {} - Label { + text: qsTr("nymea UUID:") Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Do you want to connect nevertheless?") : qsTr("Do you want to trust this device?") - font.bold: true + } + Label { + text: certDialog.serverUuid + Layout.fillWidth: true + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: app.margins + } + GridLayout { + columns: 2 + Label { + text: qsTr("Organisation:") + Layout.fillWidth: true + } + Label { + text: certDialog.issuerInfo["O"] + Layout.fillWidth: true + } + Label { + text: qsTr("Common name:") + Layout.fillWidth: true + } + Label { + text: certDialog.issuerInfo["CN"] + Layout.fillWidth: true + } + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: app.margins + } + Label { + text: qsTr("Fingerprint:") + Layout.fillWidth: true + } + Label { + text: certDialog.issuerInfo["fingerprint"] + Layout.fillWidth: true + wrapMode: Text.WrapAnywhere } } - - onAccepted: { - engine.connection.acceptCertificate(certDialog.url, certDialog.pem) - } - onRejected: { - engine.connection.disconnect(); - } } diff --git a/nymea-app/ui/connection/CertificateErrorDialog.qml b/nymea-app/ui/connection/CertificateErrorDialog.qml new file mode 100644 index 00000000..10987809 --- /dev/null +++ b/nymea-app/ui/connection/CertificateErrorDialog.qml @@ -0,0 +1,45 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.3 +import Nymea 1.0 +import "../components" + +MeaDialog { + id: root + + title: qsTr("Insecure connection") + headerIcon: "../images/lock-broken.svg" + text: qsTr("The certificate for this %1 system has changed. This could be because the configuration has been changed, but could also mean the system has been compromised. Do you want to accept the new certificate?").arg(app.systemName) + standardButtons: Dialog.Ok | Dialog.Cancel +} diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index 4670d21e..2e8a5ff8 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -45,33 +45,22 @@ Page { pageStack.push(discoveryPage, StackView.Immediate) } - - function connectToHost(url, noAnimations) { + function connectToHost(host, noAnimations) { var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"), noAnimations ? StackView.Immediate : StackView.PushTransition) page.cancel.connect(function() { - engine.connection.disconnect() - pageStack.pop(root, StackView.Immediate); - pageStack.push(discoveryPage) - }) - engine.connection.connect(url) - } - - function connectToHost2(host, noAnimations) { - var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"), noAnimations ? StackView.Immediate : StackView.PushTransition) - page.cancel.connect(function() { - engine.connection.disconnect() + engine.jsonRpcClient.disconnectFromHost() pageStack.pop(root, StackView.Immediate); pageStack.push(discoveryPage) }) print("Connecting to host", host) - engine.connection.connect(host) + engine.jsonRpcClient.connectToHost(host) } NymeaHostsFilterModel { id: hostsProxy discovery: _discovery showUnreachableBearers: false - nymeaConnection: engine.connection + jsonRpcClient: engine.jsonRpcClient showUnreachableHosts: false } @@ -91,7 +80,7 @@ Page { onClicked: { if (index === 2) { var host = discovery.nymeaHosts.createWanHost("Demo server", "nymea://nymea.nymea.io:2222") - engine.connection.connect(host) + engine.jsonRpcClient.connectToHost(host) } else { pageStack.push(model.get(index).page, {nymeaDiscovery: discovery}); } @@ -176,7 +165,7 @@ Page { switch (nymeaHost.connections.get(defaultConnectionIndex).bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: - if (engine.connection.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { + if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "../images/network-wired.svg" } return "../images/network-wifi.svg"; @@ -196,15 +185,13 @@ Page { prominentSubText: false progressive: false property bool isSecure: nymeaHost.connections.get(defaultConnectionIndex).secure - property bool isTrusted: engine.connection.isTrusted(nymeaHostDelegate.nymeaHost.connections.get(defaultConnectionIndex).url) property bool isOnline: nymeaHost.connections.get(defaultConnectionIndex).bearerType !== Connection.BearerTypeWan ? nymeaHost.connections.get(defaultConnectionIndex).online : true tertiaryIconName: isSecure ? "../images/network-secure.svg" : "" - tertiaryIconColor: isTrusted ? app.accentColor : Material.foreground secondaryIconName: !isOnline ? "../images/cloud-error.svg" : "" secondaryIconColor: "red" onClicked: { - root.connectToHost2(nymeaHostDelegate.nymeaHost) + root.connectToHost(nymeaHostDelegate.nymeaHost) } contextOptions: [ @@ -272,7 +259,7 @@ Page { text: qsTr("Demo mode (online)") onClicked: { var host = discovery.nymeaHosts.createWanHost("Demo server", "nymea://nymea.nymea.io:2222") - engine.connection.connect(host) + engine.jsonRpcClient.connectToHost(host) } } @@ -390,7 +377,7 @@ Page { switch (model.bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: - if (engine.connection.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { + if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "../images/network-wired.svg" } return "../images/network-wifi.svg"; @@ -398,19 +385,19 @@ Page { return "../images/bluetooth.svg"; case Connection.BearerTypeCloud: return "../images/cloud.svg" + case Connection.BearerTypeLoopback: + return "../images/network-wired.svg" } return "" } tertiaryIconName: model.secure ? "../images/network-secure.svg" : "" - tertiaryIconColor: isTrusted ? app.accentColor : "gray" - readonly property bool isTrusted: engine.connection.isTrusted(url) secondaryIconName: !model.online ? "../images/cloud-error.svg" : "" secondaryIconColor: "red" onClicked: { dialog.close() - engine.connection.connect(dialog.nymeaHost, dialog.nymeaHost.connections.get(index)) + engine.jsonRpcClient.connectToHost(dialog.nymeaHost, dialog.nymeaHost.connections.get(index)) } } } diff --git a/nymea-app/ui/connection/ConnectingPage.qml b/nymea-app/ui/connection/ConnectingPage.qml index 44fe2931..754430e1 100644 --- a/nymea-app/ui/connection/ConnectingPage.qml +++ b/nymea-app/ui/connection/ConnectingPage.qml @@ -72,7 +72,7 @@ Page { } Label { Layout.fillWidth: true - text: engine.connection.currentHost.name.length > 0 ? engine.connection.currentHost.name : engine.connection.currentHost.uuid + text: engine.jsonRpcClient.currentHost.name.length > 0 ? engine.jsonRpcClient.currentHost.name : engine.jsonRpcClient.currentHost.uuid font.pixelSize: app.smallFont wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter @@ -82,7 +82,7 @@ Page { Layout.preferredHeight: 150 text: { var errorMessage; - switch (engine.connection.connectionStatus) { + switch (engine.jsonRpcClient.connectionStatus) { case NymeaConnection.ConnectionStatusUnconnected: case NymeaConnection.ConnectionStatusConnecting: case NymeaConnection.ConnectionStatusConnected: @@ -115,7 +115,7 @@ Page { break; case NymeaConnection.ConnectionStatusUnknownError: default: - errorMessage = qsTr("An unknown error happened. We're very sorry for that. (Error code: %1)").arg(engine.connection.connectionStatus); + errorMessage = qsTr("An unknown error happened. We're very sorry for that. (Error code: %1)").arg(engine.jsonRpcClient.connectionStatus); } return errorMessage; } diff --git a/nymea-app/ui/connection/ManualConnectPage.qml b/nymea-app/ui/connection/ManualConnectPage.qml index 9c5de669..673cdecd 100644 --- a/nymea-app/ui/connection/ManualConnectPage.qml +++ b/nymea-app/ui/connection/ManualConnectPage.qml @@ -127,7 +127,7 @@ Page { print("Try to connect ", rpcUrl) var host = discovery.nymeaHosts.createLanHost("Manual connection", rpcUrl); - engine.connection.connect(host) + engine.jsonRpcClient.connectToHost(host) } } } diff --git a/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml b/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml index 4bf732f3..0738dd56 100644 --- a/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml +++ b/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml @@ -153,7 +153,7 @@ Page { text: qsTr("Connect to %1:core").arg(app.systemName) property string url onClicked: { - engine.connection.connect(url) + engine.jsonRpcClient.connectToHost(url) } } diff --git a/nymea-app/ui/system/AboutNymeaPage.qml b/nymea-app/ui/system/AboutNymeaPage.qml index f26d69aa..be19fc76 100644 --- a/nymea-app/ui/system/AboutNymeaPage.qml +++ b/nymea-app/ui/system/AboutNymeaPage.qml @@ -48,7 +48,7 @@ SettingsPageBase { NymeaListItemDelegate { Layout.fillWidth: true text: qsTr("Connection:") - subText: engine.connection.currentConnection.url + subText: engine.jsonRpcClient.currentConnection.url progressive: false prominentSubText: false } diff --git a/nymea-app/ui/system/ConnectionInterfaceDelegate.qml b/nymea-app/ui/system/ConnectionInterfaceDelegate.qml index 698ef8c6..c865c8d9 100644 --- a/nymea-app/ui/system/ConnectionInterfaceDelegate.qml +++ b/nymea-app/ui/system/ConnectionInterfaceDelegate.qml @@ -39,18 +39,8 @@ NymeaListItemDelegate { subText: qsTr("Port: %1").arg(model.port) iconName: "../images/network-vpn.svg" progressive: false - iconColor: { - if ((engine.connection.hostAddress === model.address || model.address === "0.0.0.0") - && engine.connection.port === model.port) { - return app.accentColor - } - return iconKeyColor - } - secondaryIconName: "../images/account.svg" secondaryIconColor: model.authenticationEnabled ? app.accentColor : secondaryIconKeyColor tertiaryIconName: "../images/network-secure.svg" tertiaryIconColor: model.sslEnabled ? app.accentColor : tertiaryIconKeyColor - -// canDelete: true } diff --git a/nymea-app/ui/system/ConnectionInterfacesPage.qml b/nymea-app/ui/system/ConnectionInterfacesPage.qml index ffc96382..a683e214 100644 --- a/nymea-app/ui/system/ConnectionInterfacesPage.qml +++ b/nymea-app/ui/system/ConnectionInterfacesPage.qml @@ -47,7 +47,10 @@ SettingsPageBase { model: engine.nymeaConfiguration.tcpServerConfigurations delegate: ConnectionInterfaceDelegate { Layout.fillWidth: true - canDelete: true + iconColor: inUse ? app.accentColor : iconKeyColor + readonly property bool inUse: (engine.jsonRpcClient.currentConnection.hostAddress === model.address || model.address === "0.0.0.0") + && engine.jsonRpcClient.currentConnection.port === model.port + canDelete: !inUse onClicked: { var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.tcpServerConfigurations.get(index).clone() }); @@ -61,7 +64,6 @@ SettingsPageBase { popup.open() } onDeleteClicked: { - print("should delete") engine.nymeaConfiguration.deleteTcpServerConfiguration(model.id) } } @@ -93,7 +95,10 @@ SettingsPageBase { model: engine.nymeaConfiguration.webSocketServerConfigurations delegate: ConnectionInterfaceDelegate { Layout.fillWidth: true - canDelete: true + iconColor: inUse ? app.accentColor : iconKeyColor + readonly property bool inUse: (engine.jsonRpcClient.currentConnection.hostAddress === model.address || model.address === "0.0.0.0") + && engine.jsonRpcClient.currentConnection.port === model.port + canDelete: !inUse onClicked: { var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.webSocketServerConfigurations.get(index).clone() }); diff --git a/nymea-app/ui/system/DeveloperTools.qml b/nymea-app/ui/system/DeveloperTools.qml index f96b2eaa..618e8d21 100644 --- a/nymea-app/ui/system/DeveloperTools.qml +++ b/nymea-app/ui/system/DeveloperTools.qml @@ -43,7 +43,7 @@ SettingsPageBase { for (var i = 0; i < engine.nymeaConfiguration.webServerConfigurations.count; i++) { var tmp = engine.nymeaConfiguration.webServerConfigurations.get(i) print("checking config:", tmp.id, tmp.address, tmp.port, tmp.sslEnabled) - if (tmp.address === engine.connection.currentConnection.hostAddress || tmp.address === "0.0.0.0") { + if (tmp.address === engine.jsonRpcClient.currentConnection.hostAddress || tmp.address === "0.0.0.0") { // This one prefers https over http... // if (config === null || (!config.sslEnabled && tmp.sslEnabled)) { @@ -86,7 +86,7 @@ SettingsPageBase { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins - text: qsTr("The web server cannot be reached on %1.").arg(engine.connection.currentConnection.hostAddress) + text: qsTr("The web server cannot be reached on %1.").arg(engine.jsonRpcClient.currentConnection.hostAddress) wrapMode: Text.WordWrap font.pixelSize: app.smallFont color: "red" @@ -111,10 +111,10 @@ SettingsPageBase { enabled: root.usedConfig !== null && engine.nymeaConfiguration.webServerConfigurations.count > 0 text: qsTr("Open debug interface") onClicked: { - print("opening:", engine.connection.currentConnection.url) + print("opening:", engine.jsonRpcClient.currentConnection.url) var proto = "http" + (root.usedConfig.sslEnabled ? "s" : "") + "://" - var path = engine.connection.currentConnection.hostAddress + ":" + root.usedConfig.port + "/debug" + var path = engine.jsonRpcClient.currentConnection.hostAddress + ":" + root.usedConfig.port + "/debug" print("opening:", proto + path) Qt.openUrlExternally(proto + path) }