From e73212b8be98612aaabb20168a6de906c84e75ae Mon Sep 17 00:00:00 2001 From: Manuel Pietschmann Date: Sun, 11 Sep 2016 21:45:41 +0200 Subject: [PATCH] Add more details to cross gateway communication REST API (wip) --- de_web_plugin_private.h | 4 +- gateway.cpp | 282 ++++++++++++++++++++++++++++++++++++---- gateway.h | 21 +++ gateway_scanner.cpp | 47 +++++-- rest_gateways.cpp | 137 ++++++++++++++++--- 5 files changed, 435 insertions(+), 56 deletions(-) diff --git a/de_web_plugin_private.h b/de_web_plugin_private.h index f5804817fc..8a761f53e9 100644 --- a/de_web_plugin_private.h +++ b/de_web_plugin_private.h @@ -501,7 +501,9 @@ class DeRestPluginPrivate : public QObject // REST API gateways int handleGatewaysApi(const ApiRequest &req, ApiResponse &rsp); - int getGateways(const ApiRequest &req, ApiResponse &rsp); + int getAllGateways(const ApiRequest &req, ApiResponse &rsp); + int getGatewayState(const ApiRequest &req, ApiResponse &rsp); + void gatewayToMap(const ApiRequest &req, const Gateway *gw, QVariantMap &map); // REST API configuration int handleConfigurationApi(const ApiRequest &req, ApiResponse &rsp); diff --git a/gateway.cpp b/gateway.cpp index e8885f27d6..5d675a8068 100644 --- a/gateway.cpp +++ b/gateway.cpp @@ -1,16 +1,11 @@ +#include #include #include #include #include #include #include "gateway.h" - -enum GW_State -{ - GW_StateOffline, - GW_StateNotAuthorized, - GW_StateConnected -}; +#include "json.h" enum GW_Event { @@ -27,8 +22,11 @@ class GatewayPrivate void handleEventStateOffline(GW_Event event); void handleEventStateNotAuthorized(GW_Event event); void handleEventStateConnected(GW_Event event); + void checkConfigResponse(const QByteArray &data); + void checkGroupsResponse(const QByteArray &data); - GW_State state; + Gateway::State state; + bool pairingEnabled; QString apikey; QString name; QString uuid; @@ -37,8 +35,9 @@ class GatewayPrivate QTimer *timer; GW_Event timerAction; QNetworkAccessManager *manager; + QBuffer *reqBuffer; QNetworkReply *reply; - QTime tLastReply; + std::vector groups; }; Gateway::Gateway(QObject *parent) : @@ -46,15 +45,16 @@ Gateway::Gateway(QObject *parent) : d_ptr(new GatewayPrivate) { Q_D(Gateway); - d->state = GW_StateOffline; + d->state = Gateway::StateOffline; + d->pairingEnabled = true; d->reply = 0; d->manager = new QNetworkAccessManager(this); + connect(d->manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*))); d->timer = new QTimer(this); d->timer->setSingleShot(true); + d->reqBuffer = new QBuffer(this); connect(d->timer, SIGNAL(timeout()), this, SLOT(timerFired())); - d->apikey = "adasd90892msd"; - d->startTimer(5000, ActionProcess); } @@ -129,6 +129,42 @@ void Gateway::setPort(quint16 port) } } +void Gateway::setApiKey(const QString &apiKey) +{ + Q_D(Gateway); + if (d->apikey != apiKey) + { + d->apikey = apiKey; + } +} + +bool Gateway::pairingEnabled() const +{ + Q_D(const Gateway); + return d->pairingEnabled; +} + +void Gateway::setPairingEnabled(bool pairingEnabled) +{ + Q_D(Gateway); + if (d->pairingEnabled != pairingEnabled) + { + d->pairingEnabled = pairingEnabled; + } +} + +Gateway::State Gateway::state() const +{ + Q_D(const Gateway); + return d->state; +} + +const std::vector &Gateway::groups() const +{ + Q_D(const Gateway); + return d->groups; +} + void Gateway::timerFired() { Q_D(Gateway); @@ -162,9 +198,9 @@ void GatewayPrivate::startTimer(int msec, GW_Event event) void GatewayPrivate::handleEvent(GW_Event event) { - if (state == GW_StateOffline) { handleEventStateOffline(event); } - else if (state == GW_StateNotAuthorized) { handleEventStateNotAuthorized(event); } - else if (state == GW_StateConnected) { handleEventStateConnected(event); } + if (state == Gateway::StateOffline) { handleEventStateOffline(event); } + else if (state == Gateway::StateNotAuthorized) { handleEventStateNotAuthorized(event); } + else if (state == Gateway::StateConnected) { handleEventStateConnected(event); } else { Q_ASSERT(0); @@ -190,41 +226,165 @@ void GatewayPrivate::handleEventStateOffline(GW_Event event) QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + startTimer(2000, EventTimeout); + } + else if (event == EventResponse) + { + QNetworkReply *r = reply; + if (reply) + { + timer->stop(); + reply = 0; + int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + r->deleteLater(); - startTimer(100, EventTimeout); + if (code == 403) + { + state = Gateway::StateNotAuthorized; + startTimer(1000, ActionProcess); + } + else if (code == 200) + { + checkConfigResponse(r->readAll()); + state = Gateway::StateConnected; + startTimer(500, ActionProcess); + } + else + { + DBG_Printf(DBG_INFO, "unhandled http status code in offline state %d\n", code); + startTimer(10000, EventTimeout); + } + } + } + else if (event == EventTimeout) + { + if (reply) + { + QNetworkReply *r = reply; + reply = 0; + if (r->isRunning()) + { + r->abort(); + } + r->deleteLater(); + } + startTimer(10000, ActionProcess); + } +} + +void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) +{ + if (event == ActionProcess) + { + if (!pairingEnabled) + { + startTimer(5000, ActionProcess); + return; + } + + // try to create user account + QString url; + url.sprintf("http://%s:%u/api/", qPrintable(address.toString()), port); + + QVariantMap map; + map[QLatin1String("devicetype")] = QLatin1String("x-gw"); + map[QLatin1String("username")] = apikey; + + QString json = deCONZ::jsonStringFromMap(map); + + reqBuffer->close(); + reqBuffer->setData(json.toUtf8()); + reqBuffer->open(QBuffer::ReadOnly); + + QNetworkRequest req(url); + reply = manager->sendCustomRequest(req, "POST", reqBuffer); + + QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + + startTimer(5000, EventTimeout); } else if (event == EventResponse) { - timer->stop(); - tLastReply.start(); QNetworkReply *r = reply; if (reply) { + timer->stop(); reply = 0; int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (code == 403 || code == 200) // not authorized or ok { - QNetworkRequest req = r->request(); QByteArray data = r->readAll(); - DBG_Printf(DBG_INFO, "GW reply code %d from %s\n%s\n", code, qPrintable(req.url().toString()), qPrintable(data)); + DBG_Printf(DBG_INFO, "GW create user reply %d: %s\n", code, qPrintable(data)); } + r->deleteLater(); if (code == 403) { - state = GW_StateNotAuthorized; - startTimer(10, ActionProcess); + // retry + startTimer(10000, ActionProcess); } else if (code == 200) { - state = GW_StateConnected; - startTimer(10, ActionProcess); + // go to state offline first to query config + state = Gateway::StateOffline; + startTimer(100, ActionProcess); } } } else if (event == EventTimeout) + { + state = Gateway::StateOffline; + startTimer(5000, ActionProcess); + } +} + +void GatewayPrivate::handleEventStateConnected(GW_Event event) +{ + if (event == ActionProcess) + { + QString url; + url.sprintf("http://%s:%u/api/%s/groups", + qPrintable(address.toString()), port, qPrintable(apikey)); + + reply = manager->get(QNetworkRequest(url)); + QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + + + startTimer(1000, EventTimeout); + } + else if (event == EventResponse) + { + QNetworkReply *r = reply; + if (reply) + { + timer->stop(); + reply = 0; + int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + + if (code == 200) + { + //state = Gateway::StateConnected; + // ok check again later + checkGroupsResponse(r->readAll()); + startTimer(15000, ActionProcess); + } + else + { + DBG_Printf(DBG_INFO, "unhandled http status code in connected state %d switch to offline state\n", code); + state = Gateway::StateOffline; + startTimer(5000, ActionProcess); + } + + r->deleteLater(); + } + } + else if (event == EventTimeout) { if (reply) { @@ -236,16 +396,82 @@ void GatewayPrivate::handleEventStateOffline(GW_Event event) } r->deleteLater(); } - startTimer(10000, ActionProcess); + DBG_Printf(DBG_INFO, "request timeout in connected state switch to offline state\n"); + state = Gateway::StateOffline; + startTimer(5000, ActionProcess); } } -void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) +void GatewayPrivate::checkConfigResponse(const QByteArray &data) { + bool ok; + QVariant var = Json::parse(data, ok); + + if (var.type() != QVariant::Map) + return; + QVariantMap map = var.toMap(); + + if (!ok || map.isEmpty()) + { + return; + } + + if (map.contains(QLatin1String("name"))) + { + name = map[QLatin1String("name")].toString(); + } } -void GatewayPrivate::handleEventStateConnected(GW_Event event) +void GatewayPrivate::checkGroupsResponse(const QByteArray &data) { + bool ok; + QVariant var = Json::parse(data, ok); + + if (var.type() != QVariant::Map) + return; + + QVariantMap map = var.toMap(); + if (!ok || map.isEmpty()) + { + return; + } + + QStringList groupIds = map.keys(); + + QStringList::iterator i = groupIds.begin(); + QStringList::iterator end = groupIds.end(); + + if (groups.size() != (size_t)groupIds.size()) + { + groups.clear(); + } + + for (size_t j = 0; i != end; ++i, j++) + { + + QVariantMap g = map[*i].toMap(); + QString name = g["name"].toString(); + + if (j == groups.size()) + { + Gateway::Group group; + group.name = name; + group.id = *i; + groups.push_back(group); + DBG_Printf(DBG_INFO, "\tgroup %s: %s\n", qPrintable(group.id), qPrintable(group.name)); + } + else if (j < groups.size()) + { + Gateway::Group &group = groups[j]; + if (group.name != name || group.id != *i) + { + // update + group.name = name; + group.id = *i; + DBG_Printf(DBG_INFO, "\tgroup %s: %s\n", qPrintable(group.id), qPrintable(group.name)); + } + } + } } diff --git a/gateway.h b/gateway.h index 82f7b7f099..8f0bef9a30 100644 --- a/gateway.h +++ b/gateway.h @@ -6,10 +6,25 @@ #include class GatewayPrivate; + class Gateway : public QObject { Q_OBJECT public: + class Group + { + public: + QString id; + QString name; + }; + + enum State + { + StateOffline, + StateNotAuthorized, + StateConnected + }; + explicit Gateway(QObject *parent = 0); ~Gateway(); @@ -21,6 +36,12 @@ class Gateway : public QObject void setAddress(const QHostAddress &address); quint16 port() const; void setPort(quint16 port); + void setApiKey(const QString &apiKey); + bool pairingEnabled() const; + void setPairingEnabled(bool pairingEnabled); + State state() const; + + const std::vector &groups() const; signals: diff --git a/gateway_scanner.cpp b/gateway_scanner.cpp index 75c681ed7c..66a1cca200 100644 --- a/gateway_scanner.cpp +++ b/gateway_scanner.cpp @@ -63,6 +63,7 @@ GatewayScanner::GatewayScanner(QObject *parent) : d->scanIteration = 0; d->state = StateInit; d->manager = new QNetworkAccessManager(this); + connect(d->manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestFinished(QNetworkReply*))); d->timer = new QTimer(this); d->timer->setSingleShot(true); connect(d->timer, SIGNAL(timeout()), this, SLOT(scanTimerFired())); @@ -210,15 +211,42 @@ void GatewayScannerPrivate::handleEvent(ScanEvent event) reply = 0; int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (code == 403 || code == 200) // not authorized or ok + if (code == 200) // not authorized or ok { QNetworkRequest req = r->request(); - QByteArray data = r->readAll(); - DBG_Printf(DBG_INFO, "reply code %d from %s\n%s\n", code, qPrintable(req.url().toString()), qPrintable(data)); - - QString uuid; - QString name; - q->foundGateway(scanIp, scanPort, uuid, name); + DBG_Printf(DBG_INFO, "reply code %d from %s\n", code, qPrintable(req.url().toString())); + + bool isGateway = false; + char buf[256]; + qint64 n = 0; + while ((n = r->readLine(buf, sizeof(buf))) > 0) + { + if (n < 10 || (size_t)n >= sizeof(buf)) + continue; + + buf[n] = '\0'; + + if (!isGateway) + { + if (strstr(buf, ">dresden elektronik<")) + { + isGateway = true; + } + } + else + { + const char *uuid = strstr(buf, "uuid:"); + const char *end = uuid ? strstr(uuid, "") : 0; + if (uuid && end) + { + uuid += strlen("uuid:"); + buf[end - buf] = '\0'; + QString name; + q->foundGateway(scanIp, scanPort, uuid, name); + break; + } + } + } } r->deleteLater(); @@ -267,12 +295,11 @@ void GatewayScannerPrivate::queryNextIp() scanIp = interfaces.back(); scanPort = 80; QString url; - QString apikey = "adasd"; - url.sprintf("http://%u.%u.%u.%u:%u/api/%s/config", + url.sprintf("http://%u.%u.%u.%u:%u/description.xml", ((scanIp >> 24) & 0xff), ((scanIp >> 16) & 0xff), ((scanIp >> 8) & 0xff), - host & 0xff, scanPort, qPrintable(apikey)); + host & 0xff, scanPort); scanIp &= 0xffffff00ull; scanIp |= host & 0xff; diff --git a/rest_gateways.cpp b/rest_gateways.cpp index 40dc490213..2ee0147fe6 100644 --- a/rest_gateways.cpp +++ b/rest_gateways.cpp @@ -33,62 +33,165 @@ int DeRestPluginPrivate::handleGatewaysApi(const ApiRequest &req, ApiResponse &r return REQ_NOT_HANDLED; } + if(!checkApikeyAuthentification(req, rsp)) + { + return REQ_READY_SEND; + } + // GET /api//gateways if ((req.path.size() == 3) && (req.hdr.method() == QLatin1String("GET"))) { - return getGateways(req, rsp); + return getAllGateways(req, rsp); + } + // GET /api//gateways/ + else if ((req.path.size() == 4) && (req.hdr.method() == QLatin1String("GET"))) + { + return getGatewayState(req, rsp); } return REQ_NOT_HANDLED; } -int DeRestPluginPrivate::getGateways(const ApiRequest &req, ApiResponse &rsp) +int DeRestPluginPrivate::getAllGateways(const ApiRequest &req, ApiResponse &rsp) { Q_UNUSED(req); rsp.httpStatus = HttpStatusOk; - rsp.str = "{}"; for (size_t i = 0; i < gateways.size(); i++) { - Gateway *gw = gateways[i]; - Q_ASSERT(gw); - QVariantMap g; - if (!gw->uuid().isEmpty()) + QVariantMap map; + gatewayToMap(req, gateways[i], map); + if (!map.isEmpty()) { - g[QLatin1String("uuid")] = gw->uuid(); + rsp.map[QString::number(i + 1)] = map; } - if (!gw->name().isEmpty()) + } + + if (rsp.map.isEmpty()) + { + rsp.str = "{}"; + return REQ_READY_SEND; + } + + return REQ_READY_SEND; +} + +/*! GET /api//gateways/ + \return 0 - on success + -1 - on error + */ +int DeRestPluginPrivate::getGatewayState(const ApiRequest &req, ApiResponse &rsp) +{ + DBG_Assert(req.path.size() == 4); + + if (req.path.size() != 4) + { + return REQ_NOT_HANDLED; + } + + rsp.httpStatus = HttpStatusOk; + + bool ok; + size_t idx = req.path[3].toUInt(&ok); + + if (!ok || idx == 0 || (idx - 1) >= gateways.size()) + { + rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/gateways/%1").arg(req.path[3]), QString("resource, /gateways/%1, not available").arg(req.path[3]))); + rsp.httpStatus = HttpStatusNotFound; + return REQ_READY_SEND; + } + + idx -= 1; + + gatewayToMap(req, gateways[idx], rsp.map); + + if (rsp.map.isEmpty()) + { + rsp.str = "{}"; + } + + return REQ_READY_SEND; +} + +/*! Puts all parameters in a map for later JSON serialization. + */ +void DeRestPluginPrivate::gatewayToMap(const ApiRequest &req, const Gateway *gw, QVariantMap &map) +{ + Q_UNUSED(req); + + if (!gw) + { + return; + } + + if (!gw->uuid().isEmpty()) + { + map[QLatin1String("uuid")] = gw->uuid(); + } + if (!gw->name().isEmpty()) + { + map[QLatin1String("name")] = gw->name(); + } + map[QLatin1String("ip")] = gw->address().toString(); + map[QLatin1String("port")] = (double)gw->port(); + map[QLatin1String("pairing")] = gw->pairingEnabled(); + + if (!gw->groups().empty()) + { + QVariantMap groups; + + for (size_t i = 0; i < gw->groups().size(); i++) { - g[QLatin1String("name")] = gw->name(); + const Gateway::Group &g = gw->groups()[i]; + groups[g.id] = g.name; } - g[QLatin1String("ip")] = gw->address().toString(); - g[QLatin1String("port")] = (double)gw->port(); - rsp.list.push_back(g); + + map[QLatin1String("groups")] = groups; } - return REQ_READY_SEND; + switch (gw->state()) + { + case Gateway::StateConnected: { map[QLatin1String("state")] = QLatin1String("connected"); } break; + case Gateway::StateNotAuthorized: { map[QLatin1String("state")] = QLatin1String("not authorized"); } break; + case Gateway::StateOffline: { map[QLatin1String("state")] = QLatin1String("offline"); } break; + default: { map[QLatin1String("state")] = QLatin1String("unknown"); } + break; + } } void DeRestPluginPrivate::foundGateway(quint32 ip, quint16 port, const QString &uuid, const QString &name) { + if (uuid.isEmpty()) + { + return; + } for (size_t i = 0; i < gateways.size(); i++) { Gateway *gw = gateways[i]; Q_ASSERT(gw); - if (gw->address().toIPv4Address() == ip && gw->port() == port) - return; // already known + if (gw && gw->uuid() == uuid) + { + if (gw->address().toIPv4Address() != ip || gw->port() != port) + { + gw->setAddress(QHostAddress(ip)); + gw->setPort(port); + } - if (!uuid.isEmpty() && gw->uuid() == uuid) return; // already known + } } + Q_ASSERT(gwUuid.length() >= 10); + QString gwApikey = gwUuid.left(10); + Gateway *gw = new Gateway(this); gw->setAddress(QHostAddress(ip)); gw->setPort(port); gw->setUuid(uuid); gw->setName(name); + gw->setApiKey(gwApikey); DBG_Printf(DBG_INFO, "found gateway %s:%u\n", qPrintable(gw->address().toString()), port); gateways.push_back(gw); }