Files
evcharger-app/deviceconnection.cpp
T

605 lines
18 KiB
C++

#include "deviceconnection.h"
#include <QDebug>
#include <QMetaEnum>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QPasswordDigestor>
#include <QMessageAuthenticationCode>
#include <QSslConfiguration>
#include <QSslCertificate>
#include <QSslKey>
#include <QTimer>
#include <utility>
#include "msgpack.h"
using namespace std::chrono_literals;
namespace {
template<typename QEnum>
auto enumToString(const QEnum value)
{
return QMetaEnum::fromType<QEnum>().valueToKey(std::to_underlying(value));
}
}
DeviceConnection::DeviceConnection(QObject *parent) :
QObject{parent}
{
connect(&m_websocket, &QWebSocket::connected,
this, &DeviceConnection::connected);
connect(&m_websocket, &QWebSocket::disconnected,
this, &DeviceConnection::disconnected);
connect(&m_websocket, &QWebSocket::stateChanged,
this, &DeviceConnection::stateChanged);
connect(&m_websocket, &QWebSocket::textMessageReceived,
this, &DeviceConnection::textMessageReceived);
connect(&m_websocket, &QWebSocket::binaryMessageReceived,
this, &DeviceConnection::binaryMessageReceived);
connect(&m_websocket, &QWebSocket::errorOccurred,
this, &DeviceConnection::errorOccurred);
connect(&m_websocket, &QWebSocket::peerVerifyError,
this, &DeviceConnection::peerVerifyError);
connect(&m_websocket, &QWebSocket::sslErrors,
this, &DeviceConnection::sslErrors);
// connect(&m_websocket, &QWebSocket::preSharedKeyAuthenticationRequired,
// this, &DeviceConnection::preSharedKeyAuthenticationRequired);
connect(&m_websocket, &QWebSocket::alertSent,
this, &DeviceConnection::alertSent);
connect(&m_websocket, &QWebSocket::alertReceived,
this, &DeviceConnection::alertReceived);
connect(&m_websocket, &QWebSocket::handshakeInterruptedOnError,
this, &DeviceConnection::handshakeInterruptedOnError);
}
void DeviceConnection::setUrl(const QString &url)
{
if (m_url == url)
return;
emit urlChanged(m_url = url);
}
void DeviceConnection::start()
{
emit logMessage(tr("Connecting to %0").arg(m_url));
if (m_settings && !m_settings->solalawebKey().isEmpty() && !m_settings->solalawebCert().isEmpty())
{
auto sslConfig = m_websocket.sslConfiguration();
{
QSslCertificate cert{m_settings->solalawebCert().toUtf8()};
if (cert.isNull())
{
emit logMessage(tr("Could not parse solalaweb certificate!"));
goto after;
}
qDebug() << "cert" << cert.issuerDisplayName();
sslConfig.setLocalCertificate(cert);
}
{
QSslKey key{m_settings->solalawebKey().toUtf8(), QSsl::KeyAlgorithm::Rsa, QSsl::Pem};
if (key.isNull())
{
emit logMessage(tr("Could not parse solalaweb key!"));
goto after;
}
qDebug() << "key" << key.algorithm();
sslConfig.setPrivateKey(key);
}
m_websocket.setSslConfiguration(sslConfig);
}
after:
m_websocket.open(QUrl{m_url});
}
void DeviceConnection::setPassword(const QString &password)
{
if (m_password == password)
return;
emit passwordChanged(m_password = password);
}
void DeviceConnection::messageReceived(const QVariant &variant)
{
if (variant.typeId() != QMetaType::QVariantMap)
{
emit logMessage(tr("Received something that is not a json object!"));
return;
}
const auto &map = variant.toMap();
auto typeIter = map.find("type");
if (typeIter == std::cend(map))
{
emit logMessage(tr("Received something that does not contain a type!"));
return;
}
const auto &typeVariant = *typeIter;
if (typeVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received something with a non-string type!"));
return;
}
const auto &type = typeVariant.toString();
qDebug() << type;
bool omitLog{};
if (type == "hello")
{
{
auto iter = map.find("serial");
if (iter == std::cend(map))
{
emit logMessage(tr("Received hello without serial!"));
return;
}
const auto &serialVariant = *iter;
if (serialVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received hello with a non-string serial!"));
return;
}
m_serial = serialVariant.toString();
}
if (auto iter = map.find("secured"); iter != std::cend(map))
{
const auto &securedVariant = *iter;
if (securedVariant.typeId() != QMetaType::Bool)
{
emit logMessage(tr("Received hello with a non-bool secured!"));
return;
}
m_secured = securedVariant.toBool();
}
else
m_secured = false;
{
auto manufacturerIter = map.find("manufacturer");
if (manufacturerIter == std::cend(map))
{
emit logMessage(tr("Received hello without manufacturer!"));
return;
}
const auto &manufacturerVariant = *manufacturerIter;
if (manufacturerVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received hello with a non-string manufacturer!"));
return;
}
m_manufacturer = manufacturerVariant.toString();
}
{
auto devicetypeIter = map.find("devicetype");
if (devicetypeIter == std::cend(map))
{
emit logMessage(tr("Received hello without devicetype!"));
return;
}
const auto &deviceTypeVariant = *devicetypeIter;
if (deviceTypeVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received hello with a non-string devicetype!"));
return;
}
m_deviceType = deviceTypeVariant.toString();
}
{
auto friendlyNameIter = map.find("friendly_name");
if (friendlyNameIter == std::cend(map))
{
emit logMessage(tr("Received hello without friendly_name!"));
return;
}
const auto &friendlyNameVariant = *friendlyNameIter;
if (friendlyNameVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received hello with a non-string friendly_name!"));
return;
}
m_friendlyName = friendlyNameVariant.toString();
}
if (m_settings)
m_settings->refreshSavedDevice(SavedDevice {
.serial = m_serial,
.manufacturer = m_manufacturer,
.deviceType = m_deviceType,
.friendlyName = m_friendlyName
});
}
else if (type == "authRequired")
{
{
auto iter = map.find("token1");
if (iter == std::cend(map))
{
emit logMessage(tr("Received authRequired without token1!"));
return;
}
const auto &token1Variant = *iter;
if (token1Variant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received authRequired with a non-string token1!"));
return;
}
m_token1 = token1Variant.toString();
}
{
auto iter = map.find("token2");
if (iter == std::cend(map))
{
emit logMessage(tr("Received authRequired without token2!"));
return;
}
const auto &token2Variant = *iter;
if (token2Variant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received authRequired with a non-string token2!"));
return;
}
m_token2 = token2Variant.toString();
}
if (m_password.isEmpty())
emit authRequired();
else
sendAuthInternal();
}
else if (type == "fullStatus")
{
omitLog = true;
emit hideDisturbed();
bool partial{};
if (auto iter = map.find("partial"); iter != std::cend(map))
{
const auto &partialVariant = *iter;
if (partialVariant.typeId() != QMetaType::Bool)
{
emit logMessage(tr("Received fullStatus with a non-bool partial!"));
return;
}
partial = partialVariant.toBool();
}
auto iter = map.find("status");
if (iter == std::cend(map))
{
emit logMessage(tr("Received fullStatus without a status!"));
return;
}
const auto &statusVariant = *iter;
if (statusVariant.typeId() != QMetaType::QVariantMap)
{
emit logMessage(tr("Received fullStatus with a non-object status!"));
return;
}
const auto &status = statusVariant.toMap();
for (auto iter = std::cbegin(status); iter != std::cend(status); iter++)
m_fullStatus[iter.key()] = iter.value();
emit logMessage(partial ? tr("Received partial fullStatus") : tr("Received last fullStatus"));
if (!partial)
emit fullStatusReceived();
}
else if (type == "deltaStatus")
{
omitLog = true;
auto iter = map.find("status");
if (iter == std::cend(map))
{
emit logMessage(tr("Received deltaStatus without a status!"));
return;
}
const auto &statusVariant = *iter;
if (statusVariant.typeId() != QMetaType::QVariantMap)
{
emit logMessage(tr("Received deltaStatus with a non-object status!"));
return;
}
const auto &status = statusVariant.toMap();
for (auto iter = std::cbegin(status); iter != std::cend(status); iter++)
{
m_fullStatus[iter.key()] = iter.value();
emit valueChanged(iter.key(), iter.value());
if (iter.key() == "fna")
{
const auto &friendlyNameVariant = iter.value();
if (friendlyNameVariant.typeId() == QMetaType::QString)
{
const auto &friendlyName = friendlyNameVariant.toString();
if (m_friendlyName != friendlyName)
{
m_friendlyName = friendlyName;
if (m_settings)
m_settings->refreshSavedDevice(SavedDevice {
.serial = m_serial,
.manufacturer = m_manufacturer,
.deviceType = m_deviceType,
.friendlyName = m_friendlyName,
.password = m_password
});
}
}
}
}
}
else if (type == "offline")
{
emit showDisturbed();
}
if (!omitLog)
{
emit logMessage(tr("Received message type %0 %1").arg(type).arg(QJsonDocument::fromVariant(variant).toJson()));
}
if (auto iter = map.find("requestId"); iter != std::cend(map))
{
const auto &requestIdVariant = *iter;
if (requestIdVariant.typeId() != QMetaType::QString)
{
emit logMessage(tr("Received message with a non-string requestId!"));
return;
}
const auto &requestId = requestIdVariant.toString();
emit responseReceived(requestId, map);
if (const auto callbackIter = m_callbacks.find(requestId); callbackIter != std::end(m_callbacks))
{
if (*callbackIter)
(*callbackIter)(type, map);
m_callbacks.erase(callbackIter);
}
}
}
void DeviceConnection::sendMessage(QVariantMap variant, callback_t &&callback)
{
if (callback)
{
auto requestId = generateRequestId();
variant["requestId"] = requestId;
m_callbacks[requestId] = std::move(callback);
}
auto message = QJsonDocument::fromVariant(variant).toJson(QJsonDocument::Compact);
if (const auto iter = variant.find("type"); m_secured && (iter == std::cend(variant) || *iter != "auth"))
{
if (m_password.isEmpty())
{
qCritical() << "would have needed secure wrapper but we dont have a password!";
}
else
{
QString requestId;
if (const auto iter = variant.find("requestId"); iter != std::cend(variant))
requestId = iter->toString();
variant = QVariantMap {
{ "type", "securedMsg" },
{ "data", message },
{ "hmac", QMessageAuthenticationCode::hash(message, m_password.toUtf8(), QCryptographicHash::Sha256).toHex() }
};
if (!requestId.isEmpty())
variant["requestId"] = requestId;
message = QJsonDocument::fromVariant(variant).toJson(QJsonDocument::Compact);
}
}
//TODO verify that we actually sent the message
m_websocket.sendTextMessage(message);
emit logMessage(tr("Sending %0").arg(message));
}
QString DeviceConnection::generateRequestId()
{
return QString("request-%0").arg(m_requestId++);
}
void DeviceConnection::sendMessage(const QVariantMap &variant)
{
sendMessage(variant, {});
}
void DeviceConnection::sendAuth(const QString &password)
{
const QString data = password;
const QString salt = m_serial;
constexpr auto iterations = 100000;
constexpr auto keylen = 64;
m_password = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha512, data.toUtf8(), salt.toUtf8(), iterations, keylen).toBase64();
if (m_password.size() > 32)
m_password.resize(32);
if (m_websocket.state() != QAbstractSocket::ConnectedState)
return;
sendAuthInternal();
}
void DeviceConnection::connected()
{
qDebug() << "called";
emit logMessage(tr("Connected!"));
}
void DeviceConnection::disconnected()
{
qDebug() << "called";
emit logMessage(tr("Disconnected!"));
emit showDisturbed();
emit logMessage(tr("Reconnecting to %0").arg(m_url));
QTimer::singleShot(1s, this, [this](){
m_websocket.open(QUrl{m_url});
});
}
void DeviceConnection::stateChanged(QAbstractSocket::SocketState state)
{
qDebug() << state;
emit logMessage(tr("state changed: %0").arg(enumToString(state)));
}
void DeviceConnection::textMessageReceived(const QString &message)
{
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error);
if (error.error != QJsonParseError::NoError)
{
emit logMessage(tr("could not parse received json: %0 at offset %1").arg(error.errorString()).arg(error.offset));
return;
}
messageReceived(document.toVariant());
}
void DeviceConnection::binaryMessageReceived(QByteArray message)
{
const auto type = message.at(0);
switch (type)
{
case 0: // Type msgpack
message.removeFirst();
messageReceived(MsgPack::unpack(message));
break;
default:
emit logMessage(tr("unknown binary message type %0 received").arg(int(type)));
break;
}
}
void DeviceConnection::errorOccurred(QAbstractSocket::SocketError error)
{
qDebug() << error;
emit logMessage(tr("error occured: %0").arg(enumToString(error)));
}
void DeviceConnection::peerVerifyError(const QSslError &error)
{
qDebug() << error;
emit logMessage(tr("ssl peer verify error"));
}
void DeviceConnection::sslErrors(const QList<QSslError> &errors)
{
qDebug() << errors;
emit logMessage(tr("ssl errors"));
}
// void DeviceConnection::preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator)
// {
// }
void DeviceConnection::alertSent(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description)
{
qDebug() << level << type << description;
emit logMessage(tr("ssl alert sent level=%0 type=%1 description=%2").arg(enumToString(level)).arg(enumToString(type)).arg(description));
}
void DeviceConnection::alertReceived(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description)
{
qDebug() << level << type << description;
emit logMessage(tr("ssl alert received level=%0 type=%1 description=%2").arg(enumToString(level)).arg(enumToString(type)).arg(description));
}
void DeviceConnection::handshakeInterruptedOnError(const QSslError &error)
{
qDebug() << error;
emit logMessage(tr("ssl handshake interrupted on error"));
}
void DeviceConnection::sendAuthInternal()
{
auto hash2 = QCryptographicHash::hash(m_token1.toUtf8() + m_password.toUtf8(), QCryptographicHash::Sha256).toHex().toLower();
const QString token3 = "hatschi";
auto hash3 = QCryptographicHash::hash(token3.toUtf8() + m_token2.toUtf8() + hash2, QCryptographicHash::Sha256).toHex().toLower();
sendMessage({
{ "type", "auth" },
{ "token3", token3 },
{ "hash", hash3 },
{ "requestId", "auth-request" }
}, [this](const QString &type, const QVariantMap &message){
authResponse(type, message);
});
}
void DeviceConnection::authResponse(const QString &type, const QVariantMap &message)
{
if (type == "authError")
{
m_password.clear();
emit authError(message.value("message").toString());
}
else if (type == "authImpossible")
{
emit authImpossible();
}
else if (type == "authSuccess")
{
if (m_settings)
m_settings->refreshSavedDevice(SavedDevice {
.serial = m_serial,
.manufacturer = m_manufacturer,
.deviceType = m_deviceType,
.friendlyName = m_friendlyName,
.password = m_password
});
emit authSuccess();
}
else
{
emit authError(tr("unknown response type %0").arg(type));
}
}