Implement digest auth for gen2 rpc
parent
387b8362f4
commit
70e0f2c7ce
|
|
@ -426,7 +426,6 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info)
|
||||||
{shelly25Channel1ActionTypeId, 1},
|
{shelly25Channel1ActionTypeId, 1},
|
||||||
{shelly25Channel2ActionTypeId, 2}
|
{shelly25Channel2ActionTypeId, 2}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (channelParamTypeMap.contains(thing->thingClassId())) {
|
if (channelParamTypeMap.contains(thing->thingClassId())) {
|
||||||
relay = thing->paramValue(channelParamTypeMap.value(thing->thingClassId())).toInt();
|
relay = thing->paramValue(channelParamTypeMap.value(thing->thingClassId())).toInt();
|
||||||
} else if (actionChannelMap.contains(action.actionTypeId())) {
|
} else if (actionChannelMap.contains(action.actionTypeId())) {
|
||||||
|
|
@ -435,16 +434,27 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info)
|
||||||
|
|
||||||
ParamTypeId powerParamTypeId = powerActionParamTypesMap.value(action.actionTypeId());
|
ParamTypeId powerParamTypeId = powerActionParamTypesMap.value(action.actionTypeId());
|
||||||
bool on = action.param(powerParamTypeId).value().toBool();
|
bool on = action.param(powerParamTypeId).value().toBool();
|
||||||
url.setPath(QString("/relay/%1").arg(relay - 1));
|
|
||||||
QUrlQuery query;
|
if (shellyId.contains("Plus")) {
|
||||||
query.addQueryItem("turn", on ? "on" : "off");
|
QVariantMap params;
|
||||||
url.setQuery(query);
|
params.insert("id", relay - 1);
|
||||||
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
|
params.insert("on", on);
|
||||||
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
|
ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Switch.Set", params);
|
||||||
connect(reply, &QNetworkReply::finished, info, [info, reply, on](){
|
connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){
|
||||||
info->thing()->setStateValue("power", on);
|
info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
|
||||||
info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
|
});
|
||||||
});
|
} else {
|
||||||
|
url.setPath(QString("/relay/%1").arg(relay - 1));
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("turn", on ? "on" : "off");
|
||||||
|
url.setQuery(query);
|
||||||
|
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
|
||||||
|
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, info, [info, reply, on](){
|
||||||
|
info->thing()->setStateValue("power", on);
|
||||||
|
info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1321,6 +1331,7 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info)
|
||||||
{
|
{
|
||||||
Thing *thing = info->thing();
|
Thing *thing = info->thing();
|
||||||
QHostAddress address = getIP(thing);
|
QHostAddress address = getIP(thing);
|
||||||
|
QString shellyId = info->thing()->paramValue("id").toString();
|
||||||
|
|
||||||
if (address.isNull()) {
|
if (address.isNull()) {
|
||||||
qCWarning(dcShelly()) << "Unable to determine Shelly's network address. Failed to set up device.";
|
qCWarning(dcShelly()) << "Unable to determine Shelly's network address. Failed to set up device.";
|
||||||
|
|
@ -1328,11 +1339,15 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString password = info->thing()->paramValue("password").toString();
|
||||||
|
|
||||||
ShellyJsonRpcClient *client = new ShellyJsonRpcClient(info->thing());
|
ShellyJsonRpcClient *client = new ShellyJsonRpcClient(info->thing());
|
||||||
client->open(address);
|
client->open(address, "admin", password, shellyId);
|
||||||
connect(client, &ShellyJsonRpcClient::stateChanged, info, [info, client, this](QAbstractSocket::SocketState state) {
|
connect(client, &ShellyJsonRpcClient::stateChanged, info, [info, client, this](QAbstractSocket::SocketState state) {
|
||||||
qCDebug(dcShelly()) << "Websocket state changed:" << state;
|
qCDebug(dcShelly()) << "Websocket state changed:" << state;
|
||||||
ShellyRpcReply *reply = client->sendRequest("Shelly.GetDeviceInfo");
|
// GetDeviceInfo wouldn't require authentication if enabled, so if the setup is changed to fetch some info from GetDeviceInfo,
|
||||||
|
// make sure to not just replace the GetStatus call, or authentication verification won't work any more.
|
||||||
|
ShellyRpcReply *reply = client->sendRequest("Shelly.GetStatus");
|
||||||
connect(reply, &ShellyRpcReply::finished, info, [info, client, this](ShellyRpcReply::Status status, const QVariantMap &response){
|
connect(reply, &ShellyRpcReply::finished, info, [info, client, this](ShellyRpcReply::Status status, const QVariantMap &response){
|
||||||
if (status != ShellyRpcReply::StatusSuccess) {
|
if (status != ShellyRpcReply::StatusSuccess) {
|
||||||
qCWarning(dcShelly) << "Error during shelly setup";
|
qCWarning(dcShelly) << "Error during shelly setup";
|
||||||
|
|
@ -1360,7 +1375,7 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == QAbstractSocket::UnconnectedState) {
|
if (state == QAbstractSocket::UnconnectedState) {
|
||||||
client->open(getIP(thing));
|
client->open(getIP(thing), "admin", thing->paramValue("password").toString(), thing->paramValue("id").toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(client, &ShellyJsonRpcClient::notificationReceived, thing, [thing, this](const QVariantMap ¬ification){
|
connect(client, &ShellyJsonRpcClient::notificationReceived, thing, [thing, this](const QVariantMap ¬ification){
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
Q_DECLARE_LOGGING_CATEGORY(dcShelly)
|
Q_DECLARE_LOGGING_CATEGORY(dcShelly)
|
||||||
|
|
||||||
ShellyRpcReply::ShellyRpcReply(int id, QObject *parent):
|
ShellyRpcReply::ShellyRpcReply(const QVariantMap &requestBody, QObject *parent):
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_id(id)
|
m_requestBody(requestBody)
|
||||||
{
|
{
|
||||||
QTimer::singleShot(10000, this, [this]{finished(StatusTimeout, QVariantMap());});
|
QTimer::singleShot(10000, this, [this]{finished(StatusTimeout, QVariantMap());});
|
||||||
connect(this, &ShellyRpcReply::finished, this, &ShellyRpcReply::deleteLater);
|
connect(this, &ShellyRpcReply::finished, this, &ShellyRpcReply::deleteLater);
|
||||||
|
|
@ -15,7 +15,12 @@ ShellyRpcReply::ShellyRpcReply(int id, QObject *parent):
|
||||||
|
|
||||||
int ShellyRpcReply::id() const
|
int ShellyRpcReply::id() const
|
||||||
{
|
{
|
||||||
return m_id;
|
return m_requestBody.value("id").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap ShellyRpcReply::requestBody() const
|
||||||
|
{
|
||||||
|
return m_requestBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShellyJsonRpcClient::ShellyJsonRpcClient(QObject *parent)
|
ShellyJsonRpcClient::ShellyJsonRpcClient(QObject *parent)
|
||||||
|
|
@ -27,8 +32,12 @@ ShellyJsonRpcClient::ShellyJsonRpcClient(QObject *parent)
|
||||||
connect(m_socket, &QWebSocket::textMessageReceived, this, &ShellyJsonRpcClient::onTextMessageReceived);
|
connect(m_socket, &QWebSocket::textMessageReceived, this, &ShellyJsonRpcClient::onTextMessageReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShellyJsonRpcClient::open(const QHostAddress &address)
|
void ShellyJsonRpcClient::open(const QHostAddress &address, const QString &user, const QString &password, const QString &shellyId)
|
||||||
{
|
{
|
||||||
|
m_password = password;
|
||||||
|
m_user = user;
|
||||||
|
m_shellyId = shellyId;
|
||||||
|
|
||||||
QUrl url;
|
QUrl url;
|
||||||
url.setScheme("ws");
|
url.setScheme("ws");
|
||||||
url.setHost(address.toString());
|
url.setHost(address.toString());
|
||||||
|
|
@ -36,7 +45,7 @@ void ShellyJsonRpcClient::open(const QHostAddress &address)
|
||||||
m_socket->open(url);
|
m_socket->open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method)
|
ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method, const QVariantMap ¶ms)
|
||||||
{
|
{
|
||||||
int id = m_currentId++;
|
int id = m_currentId++;
|
||||||
|
|
||||||
|
|
@ -44,8 +53,15 @@ ShellyRpcReply *ShellyJsonRpcClient::sendRequest(const QString &method)
|
||||||
data.insert("id", id);
|
data.insert("id", id);
|
||||||
data.insert("src", "nymea");
|
data.insert("src", "nymea");
|
||||||
data.insert("method", method);
|
data.insert("method", method);
|
||||||
|
if (!params.isEmpty()) {
|
||||||
|
data.insert("params", params);
|
||||||
|
}
|
||||||
|
|
||||||
ShellyRpcReply *reply = new ShellyRpcReply(id, this);
|
if (!m_password.isEmpty() && m_nonce != 0) {
|
||||||
|
data.insert("auth", createAuthMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
ShellyRpcReply *reply = new ShellyRpcReply(data, this);
|
||||||
connect(reply, &ShellyRpcReply::finished, this, [this, id]{
|
connect(reply, &ShellyRpcReply::finished, this, [this, id]{
|
||||||
m_pendingReplies.remove(id);
|
m_pendingReplies.remove(id);
|
||||||
});
|
});
|
||||||
|
|
@ -75,12 +91,62 @@ void ShellyJsonRpcClient::onTextMessageReceived(const QString &message)
|
||||||
}
|
}
|
||||||
|
|
||||||
int id = data.value("id").toInt();
|
int id = data.value("id").toInt();
|
||||||
ShellyRpcReply *reply = m_pendingReplies.take(id);
|
ShellyRpcReply *reply = m_pendingReplies.value(id);
|
||||||
|
|
||||||
if (!reply) {
|
if (!reply) {
|
||||||
qCDebug(dcShelly()) << "Received a message which is neither a notification nor a reply to a request:" << message;
|
qCDebug(dcShelly()) << "Received a message which is neither a notification nor a reply to a request:" << message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reply->finished(ShellyRpcReply::StatusSuccess, data.value("result").toMap());
|
ShellyRpcReply::Status status = ShellyRpcReply::StatusSuccess;
|
||||||
|
if (data.contains("error")) {
|
||||||
|
QVariantMap errorMap = data.value("error").toMap();
|
||||||
|
qCWarning(dcShelly()) << "Error in shelly command:" << errorMap.value("code").toInt() << errorMap.value("message");
|
||||||
|
status = static_cast<ShellyRpcReply::Status>(errorMap.value("code").toInt());
|
||||||
|
|
||||||
|
if (status == ShellyRpcReply::StatusAuthenticationRequired) {
|
||||||
|
if (m_nonce == 0) {
|
||||||
|
qCInfo(dcShelly) << "Authentication required. Initializing nonce and retrying...";
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QVariantMap authInfo = QJsonDocument::fromJson(errorMap.value("message").toByteArray(), &error).toVariant().toMap();
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcShelly()) << "Unable to parse auth error message. Authentication will not work.";
|
||||||
|
emit reply->finished(status, QVariantMap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_nonce = authInfo.value("nonce").toInt();
|
||||||
|
m_nc = authInfo.value("nc").toInt();
|
||||||
|
QVariantMap newBody = reply->requestBody();
|
||||||
|
newBody.insert("auth", createAuthMap());
|
||||||
|
qCDebug(dcShelly) << "Sending request with auth" << qUtf8Printable(QJsonDocument::fromVariant(newBody).toJson());
|
||||||
|
m_socket->sendTextMessage(QJsonDocument::fromVariant(newBody).toJson(QJsonDocument::Compact));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
qCWarning(dcShelly()) << "Username and password seem to be wrong.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit reply->finished(status, data.value("result").toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap ShellyJsonRpcClient::createAuthMap() const
|
||||||
|
{
|
||||||
|
int cnonce = qrand();
|
||||||
|
QByteArray ha1 = QString("%1:%2:%3").arg(m_user).arg(m_shellyId.toLower()).arg(m_password).toUtf8();
|
||||||
|
ha1 = QCryptographicHash::hash(ha1, QCryptographicHash::Sha256).toHex();
|
||||||
|
QByteArray ha2 = QByteArrayLiteral("dummy_method:dummy_uri");
|
||||||
|
ha2 = QCryptographicHash::hash(ha2, QCryptographicHash::Sha256).toHex();
|
||||||
|
QByteArray response = QString("%1:%2:%3:%4:auth:%5").arg(QString::fromUtf8(ha1)).arg(m_nonce).arg(m_nc).arg(cnonce).arg(QString::fromUtf8(ha2)).toUtf8();
|
||||||
|
response = QCryptographicHash::hash(response, QCryptographicHash::Sha256).toHex();
|
||||||
|
|
||||||
|
QVariantMap auth;
|
||||||
|
auth.insert("realm", m_shellyId.toLower());
|
||||||
|
auth.insert("username", m_user);
|
||||||
|
auth.insert("nonce", m_nonce);
|
||||||
|
auth.insert("cnonce", cnonce);
|
||||||
|
auth.insert("response", response);
|
||||||
|
auth.insert("algorithm", "SHA-256");
|
||||||
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,35 @@ class ShellyRpcReply: public QObject
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum Status {
|
enum Status {
|
||||||
StatusSuccess,
|
StatusSuccess = 0,
|
||||||
StatusTimeout
|
|
||||||
|
// Shelly common error codes
|
||||||
|
StatusInvalidArgument = -103,
|
||||||
|
StatusDeadlineExceeded = -104,
|
||||||
|
StatusResourceExhausted = -108,
|
||||||
|
StatusFailedPrecondition = -109,
|
||||||
|
StatusUnavailable = -114,
|
||||||
|
|
||||||
|
StatusCodeBadRequest = 400,
|
||||||
|
StatusAuthenticationRequired = 401,
|
||||||
|
|
||||||
|
// Our own
|
||||||
|
StatusTimeout = -1
|
||||||
|
|
||||||
};
|
};
|
||||||
Q_ENUM(Status)
|
Q_ENUM(Status)
|
||||||
|
|
||||||
explicit ShellyRpcReply(int id, QObject *parent = nullptr);
|
explicit ShellyRpcReply(const QVariantMap &requestBody, QObject *parent = nullptr);
|
||||||
|
|
||||||
int id() const;
|
int id() const;
|
||||||
|
QString method() const;
|
||||||
|
QVariantMap requestBody() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void finished(Status status, const QVariantMap &response);
|
void finished(Status status, const QVariantMap &response);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_id = 0;
|
QVariantMap m_requestBody;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ShellyJsonRpcClient : public QObject
|
class ShellyJsonRpcClient : public QObject
|
||||||
|
|
@ -31,9 +46,9 @@ class ShellyJsonRpcClient : public QObject
|
||||||
public:
|
public:
|
||||||
explicit ShellyJsonRpcClient(QObject *parent = nullptr);
|
explicit ShellyJsonRpcClient(QObject *parent = nullptr);
|
||||||
|
|
||||||
void open(const QHostAddress &address);
|
void open(const QHostAddress &address, const QString &user, const QString &password, const QString &shellyId);
|
||||||
|
|
||||||
ShellyRpcReply* sendRequest(const QString &method);
|
ShellyRpcReply* sendRequest(const QString &method, const QVariantMap ¶ms = QVariantMap());
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stateChanged(QAbstractSocket::SocketState state);
|
void stateChanged(QAbstractSocket::SocketState state);
|
||||||
|
|
@ -43,10 +58,19 @@ private slots:
|
||||||
void onTextMessageReceived(const QString &message);
|
void onTextMessageReceived(const QString &message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QVariantMap createAuthMap() const;
|
||||||
|
|
||||||
QWebSocket *m_socket = nullptr;
|
QWebSocket *m_socket = nullptr;
|
||||||
QHash<int, ShellyRpcReply*> m_pendingReplies;
|
QHash<int, ShellyRpcReply*> m_pendingReplies;
|
||||||
|
|
||||||
int m_currentId = 1;
|
int m_currentId = 1;
|
||||||
|
|
||||||
|
// Needed (only) for authentication
|
||||||
|
QString m_user;
|
||||||
|
QString m_password;
|
||||||
|
QString m_shellyId;
|
||||||
|
qulonglong m_nonce = 0;
|
||||||
|
int m_nc = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SHELLYJSONRPCCLIENT_H
|
#endif // SHELLYJSONRPCCLIENT_H
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue