/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2017 Michael Zanetti * * * * This file is part of guh. * * * * Guh is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, version 2 of the License. * * * * Guh 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 guh. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "awsconnector.h" #include "loggingcategories.h" #include "nymeasettings.h" #include #include #include #include #include #include using namespace awsiotsdk; using namespace awsiotsdk::network; using namespace awsiotsdk::mqtt; QHash AWSConnector::s_requestMap; // Somehow the linker fails to find this... missing in awsiotsdk? DisconnectCallbackContextData::~DisconnectCallbackContextData() {} AWSConnector::AWSConnector(QObject *parent) : QObject(parent) { // Enable some AWS logging (does not regard our logging categories) std::shared_ptr p_log_system = std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); m_disconnectContextData = std::shared_ptr(new DisconnectContext(this)); m_subscriptionContextData = std::shared_ptr(new SubscriptionContext(this)); qRegisterMetaType(); m_clientName = readSyncedNameCache(); } AWSConnector::~AWSConnector() { qCDebug(dcAWS()) << "Stopping AWS connection. This might take a while..."; m_client.reset(); m_networkConnection.reset(); qCDebug(dcAWS()) << "AWS connection stopped."; } void AWSConnector::connect2AWS(const QString &endpoint, const QString &clientId, const QString &clientName, const QString &caFile, const QString &clientCertFile, const QString &clientPrivKeyFile) { m_shouldReconnect = true; m_currentEndpoint = endpoint; m_caFile = caFile; m_clientCertFile = clientCertFile; m_clientPrivKeyFile = clientPrivKeyFile; m_clientId = clientId; m_clientName = clientName; m_client.reset(); m_networkConnection.reset(); doConnect(); } void AWSConnector::doConnect() { m_setupInProgress = true; m_subscriptionCache.clear(); m_networkConnection = std::shared_ptr(new OpenSSLConnection( m_currentEndpoint.toStdString(), 8883, m_caFile.toStdString(), m_clientCertFile.toStdString(), m_clientPrivKeyFile.toStdString(), std::chrono::milliseconds(3000), std::chrono::milliseconds(3000), std::chrono::milliseconds(3000), true )); m_networkConnection->Initialize(); m_client = MqttClient::Create(m_networkConnection, std::chrono::milliseconds(2800), &onDisconnectedCallback, m_disconnectContextData); m_client->SetAutoReconnectEnabled(false); m_client->SetMaxReconnectBackoffTimeout(std::chrono::seconds(10)); qCDebug(dcAWS()) << "Connecting to AWS with ID:" << m_clientId << "endpoint:" << m_currentEndpoint << "Min reconnect timeout:" << m_client->GetMinReconnectBackoffTimeout().count() << "Max reconnect timeout:" << (quint32)m_client->GetMaxReconnectBackoffTimeout().count(); m_connectingFuture = QtConcurrent::run([&]() { ResponseCode rc = m_client->Connect(std::chrono::milliseconds(10000), true, mqtt::Version::MQTT_3_1_1, std::chrono::seconds(30), Utf8String::Create(m_clientId.toStdString()), nullptr, nullptr, nullptr); if (rc == ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED) { staticMetaObject.invokeMethod(this, "onConnected", Qt::QueuedConnection); } else { qCWarning(dcAWS) << "Error connecting to AWS. Response code:" << QString::fromStdString(ResponseHelper::ToString(rc)); m_client.reset(); m_networkConnection.reset(); QTimer::singleShot(10000, this, &AWSConnector::doConnect); } }); } void AWSConnector::onConnected() { if (!readRegisteredFlag()) { qCDebug(dcAWS()) << "AWS connected. Device not registered yet. Registering..."; registerDevice(); return; } qCDebug(dcAWS()) << "AWS connected. Device already registered in cloud."; // OK, we're registerd already, go straight to subscription setup setupSubscriptions(); } void AWSConnector::registerDevice() { // We create a temporary UUID for which will be used by the server to post the reply to our create/device call. // Before the first create/device call the cloud doesn't know about us. In order to receive the reply for the // call we need to subscribe to a topic every device can subscribe to. If we'd use our deviceId, a potential // black hat could snoop in all the devices we register on the system. So in case someone actually does that // let's give him meaningless IDs instead of real device ids. m_createDeviceId = QUuid::createUuid().toString().remove(QRegExp("[{}]*")); // first subscribe to this tmp id topic m_createDeviceSubscriptionId = subscribe({QString("create/device/%1").arg(m_createDeviceId)}); } void AWSConnector::onDeviceRegistered(bool needsReconnect) { storeRegisteredFlag(true); if (needsReconnect) { qCDebug(dcAWS()) << "Disconnecting from AWS and reconnecting to use new policies"; QtConcurrent::run([&]() { m_client->Disconnect(std::chrono::milliseconds(500)); m_client.reset(); m_networkConnection.reset(); QTimer::singleShot(1000, this, &AWSConnector::doConnect); }); return; } setupSubscriptions(); } void AWSConnector::setupSubscriptions() { // Subscribe to pairing info topics QStringList subscriptions; subscriptions.append(QString("%1/device/name/response").arg(m_clientId)); subscriptions.append(QString("%1/device/users/response").arg(m_clientId)); subscriptions.append(QString("%1/pair/response").arg(m_clientId)); subscriptions.append(QString("%1/notify/response").arg(m_clientId)); subscriptions.append(QString("%1/notify/info/endpoint").arg(m_clientId)); subscribe(subscriptions); // fetch previous pairings fetchPairings(); } void AWSConnector::fetchPairings() { QVariantMap params; params.insert("timestamp", QDateTime::currentMSecsSinceEpoch()); params.insert("id", ++m_transactionId); params.insert("command", "getUsers"); publish(QString("%1/device/users").arg(m_clientId), params); } void AWSConnector::onPairingsRetrieved(const QVariantMap &pairings) { qCDebug(dcAWS) << pairings.value("users").toList().count() << "devices paired in cloud."; if (pairings.value("users").toList().count() > 0) { QStringList topics; foreach (const QVariant &pairing, pairings.value("users").toList()) { topics << QString("%1/%2/#").arg(m_clientId).arg(pairing.toString()); } subscribe(topics); } qCDebug(dcAWS) << pairings.value("pushNotificationsEndpoints").toList().count() << "push notification enabled users paired in cloud."; QList pushNotificationEndpoints; if (pairings.value("pushNotificationsEndpoints").toList().count() > 0) { foreach (const QVariant &pairing, pairings.value("pushNotificationsEndpoints").toList()) { foreach (const QString &cognitoUserId, pairing.toMap().keys()) { qCDebug(dcAWS()) << "User:" << cognitoUserId << "has" << pairing.toMap().value(cognitoUserId).toList().count() << "push notifications enabled devices."; foreach (const QVariant &endpoint, pairing.toMap().value(cognitoUserId).toList()) { PushNotificationsEndpoint ep; ep.userId = cognitoUserId; ep.endpointId = endpoint.toMap().value("endpointId").toString(); ep.displayName = endpoint.toMap().value("displayName").toString(); pushNotificationEndpoints.append(ep); qCDebug(dcAWS) << "Device:" << ep.displayName << "endpoint:" << ep.endpointId << "user:" << ep.userId; } } } } emit pushNotificationEndpointsUpdated(pushNotificationEndpoints); if (readSyncedNameCache() != m_clientName) { setName(); } m_setupInProgress = false; emit connected(); } void AWSConnector::disconnectAWS() { m_shouldReconnect = false; if (isConnected()) { m_client->Disconnect(std::chrono::milliseconds(2000)); m_client.reset(); m_networkConnection.reset(); qCDebug(dcAWS()) << "Disconnected from AWS."; emit disconnected(); } } bool AWSConnector::isConnected() const { return m_connectingFuture.isFinished() && m_networkConnection && m_client && m_client->IsConnected() && !m_setupInProgress; } void AWSConnector::setDeviceName(const QString &deviceName) { if (m_clientName != deviceName) { m_clientName = deviceName; storeSyncedNameCache(QString()); if (isConnected()) { setName(); } } } void AWSConnector::pairDevice(const QString &idToken, const QString &userId) { QVariantMap map; map.insert("idToken", idToken); map.insert("userId", userId); map.insert("id", ++m_transactionId); map.insert("timestamp", QDateTime::currentMSecsSinceEpoch()); publish(QString("%1/pair").arg(m_clientId), map); m_pairingRequests.insert(m_transactionId, userId); } void AWSConnector::sendWebRtcHandshakeMessage(const QString &sessionId, const QVariantMap &map) { publish(sessionId + "/reply", map); } int AWSConnector::sendPushNotification(const QString &userId, const QString &endpointId, const QString &title, const QString &text) { QVariantMap params; params.insert("id", ++m_transactionId); params.insert("command", "sendPushNotification"); params.insert("title", title); params.insert("body", text); params.insert("timestamp", QDateTime::currentMSecsSinceEpoch()); // publish(QString("%1/notify/user/%2/%3").arg(m_clientId, userId, endpointId), params); Q_UNUSED(userId) publish(QString("%1/notify/user/%2").arg(m_clientId, endpointId), params); return m_transactionId; } quint16 AWSConnector::publish(const QString &topic, const QVariantMap &message) { if (!m_setupInProgress && !isConnected()) { qCWarning(dcAWS()) << "Can't publish to AWS: Not connected."; return -1; } QString fullTopic = topic; QJsonDocument jsonDoc = QJsonDocument::fromVariant(message); uint16_t packetId = 0; ResponseCode res = m_client->PublishAsync(Utf8String::Create(fullTopic.toStdString()), false, false, mqtt::QoS::QOS1, jsonDoc.toJson(QJsonDocument::Compact).toStdString(), &publishCallback, packetId); qCDebug(dcAWSTraffic()) << "publish call queued with status:" << QString::fromStdString(ResponseHelper::ToString(res)) << packetId << "for topic" << topic << jsonDoc.toJson(); s_requestMap.insert(packetId, this); return packetId; } void AWSConnector::onDisconnected() { qCDebug(dcAWS) << "AWS disconnected."; m_client.reset(); m_networkConnection.reset(); emit disconnected(); bool needReRegistering = false; if (m_setupInProgress) { qCWarning(dcAWS()) << "Setup process interrupted by disconnect."; needReRegistering = true; } else { if (m_lastConnectionDrop.addSecs(60) > QDateTime::currentDateTime()) { m_reconnectCounter++; } else { m_reconnectCounter = 0; } m_lastConnectionDrop = QDateTime::currentDateTime(); if (m_reconnectCounter > 5) { qCWarning(dcAWS()) << "Connection dropped 5 times in a row within a minute."; needReRegistering = true; } } if (needReRegistering) { qCDebug(dcAWS) << "Trying to reregister the device in the cloud"; storeRegisteredFlag(false); storeSyncedNameCache(QString()); } if (m_shouldReconnect) { qCDebug(dcAWS()) << "Reconnecting to AWS..."; doConnect(); } } void AWSConnector::setName() { QVariantMap params; params.insert("id", ++m_transactionId); params.insert("timestamp", QDateTime::currentMSecsSinceEpoch() / 1000); params.insert("command", "postName"); params.insert("name", m_clientName); publish(QString("%1/device/name").arg(m_clientId), params); } quint16 AWSConnector::subscribe(const QStringList &topics) { util::Vector> subscription_list; foreach (const QString &topic, topics) { if (m_subscriptionCache.contains(topic)) { qCDebug(dcAWS()) << "Already subscribed to topic:" << topic << ". Not resubscribing"; continue; } qCDebug(dcAWSTraffic()) << "Topic to subscribe is" << topic; if (!Subscription::IsValidTopicName(topic.toStdString())) { qCWarning(dcAWS()) << "Trying to subscribe to invalid topic:" << topic; continue; } auto subscription = mqtt::Subscription::Create(Utf8String::Create(topic.toStdString()), mqtt::QoS::QOS1, &onSubscriptionReceivedCallback, m_subscriptionContextData); subscription_list.push_back(subscription); m_subscriptionCache.append(topic); } if (subscription_list.size() == 0) { return 0; } uint16_t packetId; ResponseCode res = m_client->SubscribeAsync(subscription_list, subscribeCallback, packetId); qCDebug(dcAWSTraffic()) << "Subscribe call queued with status:" << QString::fromStdString(ResponseHelper::ToString(res)) << "Packet ID:" << packetId; s_requestMap.insert(packetId, this); return packetId; } void AWSConnector::publishCallback(uint16_t actionId, ResponseCode rc) { AWSConnector* obj = s_requestMap.take(actionId); if (!obj) { qCWarning(dcAWS())<< "Received a response callback but don't have an object waiting for it."; return; } switch (rc) { case ResponseCode::SUCCESS: qCDebug(dcAWSTraffic()) << "Successfully published" << actionId; break; default: qCDebug(dcAWS())<< "Error publishing data to AWS:" << QString::fromStdString(ResponseHelper::ToString(rc)); } } void AWSConnector::subscribeCallback(uint16_t actionId, ResponseCode rc) { if (rc != ResponseCode::SUCCESS) { qCWarning(dcAWS()) << "Error subscribing to" << actionId << QString::fromStdString(ResponseHelper::ToString(rc)); return; } AWSConnector *connector = s_requestMap.take(actionId); if (!connector) { qCWarning(dcAWS()) << "received a subscribe callback but don't have a request id for it."; return; } if (actionId == connector->m_createDeviceSubscriptionId) { qCDebug(dcAWS()) << "subscribed to create/device/response"; // We might get this callback even if we didn't explicitly ask for it as the // library automatically resubscribes to all the topics upon reconnect. if (!connector->readRegisteredFlag()) { QVariantMap params; params.insert("id", connector->m_createDeviceId); params.insert("UUID", connector->m_clientId); connector->publish("create/device", params); } return; } qCDebug(dcAWSTraffic()) << "Successfully subscribed (actionId:" << actionId << ")"; } ResponseCode AWSConnector::onSubscriptionReceivedCallback(util::String topic_name, util::String payload, std::shared_ptr p_app_handler_data) { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(QByteArray::fromStdString(payload), &error); if (error.error != QJsonParseError::NoError) { qCDebug(dcAWS()) << "Failed to parse JSON from AWS subscription on topic" << QString::fromStdString(topic_name) << ":" << error.errorString() << "\n" << QString::fromStdString(payload); return ResponseCode::JSON_PARSING_ERROR; } qCDebug(dcAWSTraffic()) << "Subscription received: Topic:" << QString::fromStdString(topic_name) << "payload:" << QString::fromStdString(payload); AWSConnector *connector = dynamic_cast(p_app_handler_data.get())->c; QString topic = QString::fromStdString(topic_name); if (topic.startsWith("create/device/")) { int statusCode = jsonDoc.toVariant().toMap().value("result").toMap().value("code").toInt(); switch (statusCode) { case 201: qCDebug(dcAWS()) << "Device successfully registered to the cloud server:" << statusCode << jsonDoc.toVariant().toMap().value("result").toMap().value("message").toString(); connector->staticMetaObject.invokeMethod(connector, "onDeviceRegistered", Qt::QueuedConnection, Q_ARG(bool, true)); return ResponseCode::SUCCESS; case 200: qCDebug(dcAWS()) << "Device already known to the cloud server:" << statusCode << jsonDoc.toVariant().toMap().value("result").toMap().value("message").toString(); // Ok, we have confirmation that everything went fine and we can proceed, let's remember that to minimize traffic. connector->staticMetaObject.invokeMethod(connector, "onDeviceRegistered", Qt::QueuedConnection, Q_ARG(bool, false)); break; default: qCWarning(dcAWS()) << "Error registering device in the cloud. AWS connetion will not work:" << statusCode << jsonDoc.toVariant().toMap().value("result").toMap().value("message").toString(); return ResponseCode::SUCCESS; } } else if (topic == QString("%1/pair/response").arg(connector->m_clientId)) { int statusCode = jsonDoc.toVariant().toMap().value("status").toInt(); int id = jsonDoc.toVariant().toMap().value("id").toInt(); QString message = jsonDoc.toVariant().toMap().value("result").toMap().value("message").toString(); QString userId = connector->m_pairingRequests.take(id); if (statusCode != 200) { qCWarning(dcAWS()) << "Pairing failed:" << statusCode << message; emit connector->devicePaired(userId, statusCode, message); } else if (!userId.isEmpty()) { qCDebug(dcAWS()) << "Pairing response for id:" << userId << statusCode; emit connector->devicePaired(userId, statusCode, message); connector->staticMetaObject.invokeMethod(connector, "fetchPairings", Qt::QueuedConnection); } else { qCWarning(dcAWS()) << "Received a pairing response for a transaction we didn't start"; } } else if (topic == QString("%1/device/users/response").arg(connector->m_clientId)) { connector->staticMetaObject.invokeMethod(connector, "onPairingsRetrieved", Qt::QueuedConnection, Q_ARG(QVariantMap, jsonDoc.toVariant().toMap())); } else if (topic == QString("%1/device/name/response").arg(connector->m_clientId)) { qCDebug(dcAWS) << "Set device name in cloud with status:" << jsonDoc.toVariant().toMap().value("status").toInt(); if (jsonDoc.toVariant().toMap().value("status").toInt() == 200) { connector->storeSyncedNameCache(connector->m_clientName); } } else if (topic.startsWith(QString("%1/eu-west-1:").arg(connector->m_clientId)) && !topic.contains("reply")) { static QStringList dupes; QString id = jsonDoc.toVariant().toMap().value("id").toString(); QString type = jsonDoc.toVariant().toMap().value("type").toString(); if (dupes.contains(id+type)) { qCDebug(dcAWS()) << "Dropping duplicate packet"; return ResponseCode::SUCCESS; } dupes.append(id+type); qCDebug(dcAWS) << "received webrtc handshake message" << topic << jsonDoc.toJson(); connector->webRtcHandshakeMessageReceived(topic, jsonDoc.toVariant().toMap()); } else if (topic.startsWith(QString("%1/eu-west-1:").arg(connector->m_clientId)) && topic.contains("reply")) { // silently drop our own things (should not be subscribed to that in the first place) } else if (topic == QString("%1/notify/response").arg(connector->m_clientId)) { int transactionId = jsonDoc.toVariant().toMap().value("id").toInt(); int status = jsonDoc.toVariant().toMap().value("status").toInt(); qCDebug(dcAWS()) << "Push notification reply for transaction" << transactionId << " Status:" << status << jsonDoc.toVariant().toMap().value("message").toString(); emit connector->pushNotificationSent(transactionId, status); } else if (topic == QString("%1/notify/info/endpoint").arg(connector->m_clientId)) { QVariantMap endpoint = jsonDoc.toVariant().toMap().value("newPushNotificationsEndpoint").toMap(); Q_ASSERT(endpoint.keys().count() == 1); QString cognitoId = endpoint.keys().first(); PushNotificationsEndpoint ep; ep.userId = cognitoId; ep.endpointId = endpoint.value(cognitoId).toMap().value("endpointId").toString(); ep.displayName = endpoint.value(cognitoId).toMap().value("displayName").toString(); emit connector->pushNotificationEndpointAdded(ep); } else { qCWarning(dcAWS) << "Unhandled subscription received!" << topic << QString::fromStdString(payload); } return ResponseCode::SUCCESS; } ResponseCode AWSConnector::onDisconnectedCallback(util::String mqtt_client_id, std::shared_ptr p_app_handler_data) { Q_UNUSED(mqtt_client_id) AWSConnector* connector = dynamic_cast(p_app_handler_data.get())->c; connector->staticMetaObject.invokeMethod(connector, "onDisconnected", Qt::QueuedConnection); return ResponseCode::SUCCESS; } void AWSConnector::storeRegisteredFlag(bool registered) { QSettings settings(NymeaSettings::storagePath() + "/cloudstatus.conf", QSettings::IniFormat); settings.setValue("registered", registered); } bool AWSConnector::readRegisteredFlag() const { QSettings settings(NymeaSettings::storagePath() + "/cloudstatus.conf", QSettings::IniFormat); return settings.value("registered", false).toBool(); } void AWSConnector::storeSyncedNameCache(const QString &syncedName) { QSettings settings(NymeaSettings::storagePath() + "/cloudstatus.conf", QSettings::IniFormat); settings.setValue("syncedName", syncedName); } QString AWSConnector::readSyncedNameCache() { QSettings settings(NymeaSettings::storagePath() + "/cloudstatus.conf", QSettings::IniFormat); return settings.value("syncedName", QString()).toString(); }