diff --git a/data/dbus-1/io.guh.nymead.conf b/data/dbus-1/io.guh.nymead.conf new file mode 100644 index 00000000..4106861d --- /dev/null +++ b/data/dbus-1/io.guh.nymead.conf @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/debian/control b/debian/control index ab3bebec..a3203962 100644 --- a/debian/control +++ b/debian/control @@ -25,6 +25,7 @@ Build-Depends: debhelper (>= 9.0.0), libssl-dev, libmbedtls-dev, libaws-iot-device-sdk-cpp, + dbus-test-runner, Package: guh Architecture: any diff --git a/debian/guhd.install.in b/debian/guhd.install.in index 80a3b6c1..d6e71ad5 100644 --- a/debian/guhd.install.in +++ b/debian/guhd.install.in @@ -5,3 +5,4 @@ usr/lib/@DEB_HOST_MULTIARCH@/libguh-core.so.1.0 usr/lib/@DEB_HOST_MULTIARCH@/libguh-core.so.1.0.0 data/systemd/guhd.service /lib/systemd/system/ data/logrotate/guhd /etc/logrotate.d/ +data/dbus-1/io.guh.nymead.conf /etc/dbus-1/system.d/ diff --git a/debian/rules b/debian/rules index 7bc240e6..19de33ef 100755 --- a/debian/rules +++ b/debian/rules @@ -43,7 +43,7 @@ override_dh_install: $(PREPROCESS_FILES:.in=) cp -a $(CURDIR)/doc/html $(CURDIR)/debian/guh-doc/usr/share/doc/guh/ || true override_dh_auto_test: - dh_auto_test -- -k TESTARGS="-o -,txt -o test-results.xml,xunitxml" + dh_auto_test -- -k TESTARGS="-m 120 -p -o -p -,txt -p -o -p test-results.xml,xunitxml" TESTRUNNER="dbus-test-runner --bus-type=system --task" override_dh_strip: dh_strip --dbg-package=guh-dbg diff --git a/guh.pri b/guh.pri index dfbcd205..06b0afb0 100644 --- a/guh.pri +++ b/guh.pri @@ -6,7 +6,7 @@ GUH_PLUGINS_PATH=/usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH')/gu # define protocol versions JSON_PROTOCOL_VERSION_MAJOR=0 -JSON_PROTOCOL_VERSION_MINOR=53 +JSON_PROTOCOL_VERSION_MINOR=54 REST_API_VERSION=1 DEFINES += GUH_VERSION_STRING=\\\"$${GUH_VERSION_STRING}\\\" \ diff --git a/libguh-core/guhcore.cpp b/libguh-core/guhcore.cpp index 32ad385d..94d5ee0d 100644 --- a/libguh-core/guhcore.cpp +++ b/libguh-core/guhcore.cpp @@ -438,6 +438,9 @@ void GuhCore::init() { qCDebug(dcApplication) << "Creating Rule Engine"; m_ruleEngine = new RuleEngine(this); + qCDebug(dcApplication()) << "Creating User Manager"; + m_userManager = new UserManager(this); + qCDebug(dcApplication) << "Creating Server Manager"; m_serverManager = new ServerManager(m_configuration, this); @@ -445,9 +448,6 @@ void GuhCore::init() { qCDebug(dcApplication) << "Creating Network Manager"; m_networkManager = new NetworkManager(this); - qCDebug(dcApplication) << "Creating User Manager"; - m_userManager = new UserManager(this); - qCDebug(dcApplication) << "Creating Cloud Manager"; m_cloudManager = new CloudManager(m_networkManager, this); m_cloudManager->setDeviceId(m_configuration->serverUuid()); diff --git a/libguh-core/guhdbusservice.cpp b/libguh-core/guhdbusservice.cpp new file mode 100644 index 00000000..969b24ae --- /dev/null +++ b/libguh-core/guhdbusservice.cpp @@ -0,0 +1,61 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "guhdbusservice.h" + +namespace guhserver { + +QDBusConnection GuhDBusService::s_connection = QDBusConnection::systemBus(); + +GuhDBusService::GuhDBusService(const QString &objectPath, UserManager *parent) : QObject(parent) +{ + bool status = s_connection.registerService("io.guh.nymead"); + if (!status) { + qCWarning(dcApplication) << "Failed to register D-Bus service."; + return; + } + status = s_connection.registerObject(objectPath, this, QDBusConnection::ExportScriptableContents); + if (!status) { + qCWarning(dcApplication()) << "Failed to register D-Bus object."; + return; + } + m_isValid = true; +} + +bool GuhDBusService::isValid() +{ + return m_isValid; +} + +QDBusConnection GuhDBusService::connection() const +{ + return s_connection; +} + +void GuhDBusService::setBusType(QDBusConnection::BusType busType) +{ + if (busType == QDBusConnection::SessionBus) { + s_connection = QDBusConnection::sessionBus(); + } else { + s_connection = QDBusConnection::systemBus(); + } +} + +} diff --git a/libguh-core/guhdbusservice.h b/libguh-core/guhdbusservice.h new file mode 100644 index 00000000..e5717050 --- /dev/null +++ b/libguh-core/guhdbusservice.h @@ -0,0 +1,53 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef GUHDBUSSERVICE_H +#define GUHDBUSSERVICE_H + +#include +#include +#include "guhcore.h" + +namespace guhserver { + +class GuhDBusService : public QObject, public QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "io.guh.nymead") + +public: + explicit GuhDBusService(const QString &objectPath, UserManager *parent = nullptr); + + static void setBusType(QDBusConnection::BusType busType); + + bool isValid(); + +protected: + QDBusConnection connection() const; + +private: + static QDBusConnection s_connection; + bool m_isValid = false; + +}; + +} + +#endif // GUHDBUSSERVICE_H diff --git a/libguh-core/jsonrpc/jsonhandler.cpp b/libguh-core/jsonrpc/jsonhandler.cpp index 9bb16296..736b986e 100644 --- a/libguh-core/jsonrpc/jsonhandler.cpp +++ b/libguh-core/jsonrpc/jsonhandler.cpp @@ -328,7 +328,7 @@ void JsonReply::setCommandId(int commandId) /*! Start the timeout timer for this \l{JsonReply}. The default timeout is 15 seconds. */ void JsonReply::startWait() { - m_timeout.start(15000); + m_timeout.start(30000); } void JsonReply::timeout() diff --git a/libguh-core/jsonrpc/jsonrpcserver.cpp b/libguh-core/jsonrpc/jsonrpcserver.cpp index 24814487..5cea0ec3 100644 --- a/libguh-core/jsonrpc/jsonrpcserver.cpp +++ b/libguh-core/jsonrpc/jsonrpcserver.cpp @@ -86,6 +86,7 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject returns.insert("protocol version", JsonTypes::basicTypeToString(JsonTypes::String)); returns.insert("initialSetupRequired", JsonTypes::basicTypeToString(JsonTypes::Bool)); returns.insert("authenticationRequired", JsonTypes::basicTypeToString(JsonTypes::Bool)); + returns.insert("pushButtonAuthAvailable", JsonTypes::basicTypeToString(JsonTypes::Bool)); setReturns("Hello", returns); params.clear(); returns.clear(); @@ -118,17 +119,40 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject setReturns("CreateUser", returns); params.clear(); returns.clear(); - setDescription("Authenticate", "Authenticate a client to the api. This will return a new token to be used to authorize a client at the API."); + setDescription("Authenticate", "Authenticate a client to the api via user & password challenge. Provide " + "a device name which allows the user to identify the client and revoke the token in case " + "the device is lost or stolen. This will return a new token to be used to authorize a " + "client at the API."); params.insert("username", JsonTypes::basicTypeToString(JsonTypes::String)); params.insert("password", JsonTypes::basicTypeToString(JsonTypes::String)); params.insert("deviceName", JsonTypes::basicTypeToString(JsonTypes::String)); setParams("Authenticate", params); - returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::Bool)); returns.insert("o:token", JsonTypes::basicTypeToString(JsonTypes::String)); setReturns("Authenticate", returns); params.clear(); returns.clear(); - setDescription("Tokens", "Return a list of if TokenInfo objects all the tokens for the current user."); + setDescription("RequestPushButtonAuth", "Authenticate a client to the api via Push Button method. " + "Provide a device name which allows the user to identify the client and revoke the " + "token in case the device is lost or stolen. If push button hardware is available, " + "this will return with success and start listening for push button presses. When the " + "push button is pressed, the PushButtonAuthFinished notification will be sent to the " + "requesting client. The procedure will be cancelled when the connection is interrupted. " + "If another client requests push button authentication while a procedure is still going " + "on, the second call will take over and the first one will be notified by the " + "PushButtonAuthFinished signal about the error. The application should make it clear " + "to the user to not press the button when the procedure fails as this can happen for 2 " + "reasons: a) a second user is trying to auth at the same time and only the currently " + "active user should press the button or b) it might indicate an attacker trying to take " + "over and snooping in for tokens."); + params.insert("deviceName", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("RequestPushButtonAuth", params); + returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::Bool)); + returns.insert("transactionId", JsonTypes::basicTypeToString(JsonTypes::Int)); + setReturns("RequestPushButtonAuth", returns); + + params.clear(); returns.clear(); + setDescription("Tokens", "Return a list of TokenInfo objects of all the tokens for the current user."); setParams("Tokens", params); returns.insert("tokenInfoList", QVariantList() << JsonTypes::tokenInfoRef()); setReturns("Tokens", returns); @@ -168,7 +192,16 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject params.insert("connected", JsonTypes::basicTypeToString(JsonTypes::Bool)); setParams("CloudConnectedChanged", params); + params.clear(); + setDescription("PushButtonAuthFinished", "Emitted when a push button authentication reaches final state. NOTE: This notification is special. It will only be emitted to connections that did actively request a push button authentication, but also it will be emitted regardless of the notification settings. "); + params.insert("status", JsonTypes::userErrorRef()); + params.insert("transactionId", JsonTypes::basicTypeToString(JsonTypes::Int)); + params.insert("o:token", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("PushButtonAuthFinished", params); + QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); + + connect(GuhCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServer::onPushButtonAuthFinished); } /*! Returns the \e namespace of \l{JsonHandler}. */ @@ -218,9 +251,9 @@ JsonReply* JsonRPCServer::Version(const QVariantMap ¶ms) const JsonReply* JsonRPCServer::SetNotificationStatus(const QVariantMap ¶ms) { QUuid clientId = this->property("clientId").toUuid(); - m_clients[clientId] = params.value("enabled").toBool(); + m_clientNotifications[clientId] = params.value("enabled").toBool(); QVariantMap returns; - returns.insert("enabled", m_clients[clientId]); + returns.insert("enabled", m_clientNotifications[clientId]); return createReply(returns); } @@ -251,6 +284,21 @@ JsonReply *JsonRPCServer::Authenticate(const QVariantMap ¶ms) return createReply(ret); } +JsonReply *JsonRPCServer::RequestPushButtonAuth(const QVariantMap ¶ms) +{ + QString deviceName = params.value("deviceName").toString(); + QUuid clientId = this->property("clientId").toUuid(); + + int transactionId = GuhCore::instance()->userManager()->requestPushButtonAuth(deviceName); + m_pushButtonTransactions.insert(transactionId, clientId); + + QVariantMap data; + data.insert("transactionId", transactionId); + // TODO: return false if pushbutton auth is disabled in settings + data.insert("success", true); + return createReply(data); +} + JsonReply *JsonRPCServer::Tokens(const QVariantMap ¶ms) const { Q_UNUSED(params) @@ -390,6 +438,7 @@ QVariantMap JsonRPCServer::createWelcomeMessage(TransportInterface *interface) c handshake.insert("protocol version", JSON_PROTOCOL_VERSION); handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? GuhCore::instance()->userManager()->users().isEmpty() : false)); handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); + handshake.insert("pushButtonAuthAvailable", GuhCore::instance()->userManager()->pushButtonAuthAvailable()); return handshake; } @@ -444,16 +493,18 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) // check if authentication is required for this transport if (m_interfaces.value(interface)) { - // if there is no user in the system yet, let's fail unless this is a CreateUser, Introspect or Hello call + QByteArray token = message.value("token").toByteArray(); + QStringList authExemptMethodsNoUser = {"Introspect", "Hello", "CreateUser", "RequestPushButtonAuth"}; + QStringList authExemptMethodsWithUser = {"Introspect", "Hello", "Authenticate", "RequestPushButtonAuth"}; + // if there is no user in the system yet, let's fail unless this is special method for authentication itself if (GuhCore::instance()->userManager()->users().isEmpty()) { - if (!(targetNamespace == "JSONRPC" && (method == "CreateUser" || method == "Introspect" || method == "Hello"))) { + if (!(targetNamespace == "JSONRPC" && authExemptMethodsNoUser.contains(method)) && (token.isEmpty() || !GuhCore::instance()->userManager()->verifyToken(token))) { sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call CreateUser first."); return; } } else { - // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect or Hello call - QByteArray token = message.value("token").toByteArray(); - if (!(targetNamespace == "JSONRPC" && (method == "Authenticate" || method == "Introspect" || method == "Hello")) && !GuhCore::instance()->userManager()->verifyToken(token)) { + // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect Hello call + if (!(targetNamespace == "JSONRPC" && authExemptMethodsWithUser.contains(method)) && (token.isEmpty() || !GuhCore::instance()->userManager()->verifyToken(token))) { sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); return; } @@ -522,8 +573,8 @@ void JsonRPCServer::sendNotification(const QVariantMap ¶ms) notification.insert("notification", handler->name() + "." + method.name()); notification.insert("params", params); - foreach (TransportInterface *interface, m_interfaces.keys()) { - interface->sendData(m_clients.keys(true), QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact)); + foreach (const QUuid &clientId, m_clientNotifications.keys(true)) { + m_clientTransports.value(clientId)->sendData(clientId, QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact)); } } @@ -567,6 +618,35 @@ void JsonRPCServer::onCloudConnectedChanged(bool connected) emit CloudConnectedChanged(params); } +void JsonRPCServer::onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token) +{ + QUuid clientId = m_pushButtonTransactions.take(transactionId); + if (clientId.isNull()) { + qCDebug(dcJsonRpc()) << "Received a PushButton reply but wasn't expecting it."; + return; + } + + TransportInterface *transport = m_clientTransports.value(clientId); + if (!transport) { + qCWarning(dcJsonRpc()) << "No transport for given clientId"; + return; + } + + QVariantMap params; + params.insert("transactionId", transactionId); + params.insert("success", success); + if (success) { + params.insert("token", token); + } + + QVariantMap notification; + notification.insert("id", transactionId); + notification.insert("notification", "JSONRPC.PushButtonAuthFinished"); + notification.insert("params", params); + + transport->sendData(clientId, QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact)); +} + void JsonRPCServer::registerHandler(JsonHandler *handler) { m_handlers.insert(handler->name(), handler); @@ -582,15 +662,21 @@ void JsonRPCServer::clientConnected(const QUuid &clientId) { TransportInterface *interface = qobject_cast(sender()); + m_clientTransports.insert(clientId, interface); + // If authentication is required, notifications are disabled by default. Clients must enable them with a valid token - m_clients.insert(clientId, !interface->configuration().authenticationEnabled); + m_clientNotifications.insert(clientId, !interface->configuration().authenticationEnabled); interface->sendData(clientId, QJsonDocument::fromVariant(createWelcomeMessage(interface)).toJson(QJsonDocument::Compact)); } void JsonRPCServer::clientDisconnected(const QUuid &clientId) { - m_clients.remove(clientId); + m_clientTransports.remove(clientId); + m_clientNotifications.remove(clientId); + if (m_pushButtonTransactions.values().contains(clientId)) { + GuhCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId)); + } } } diff --git a/libguh-core/jsonrpc/jsonrpcserver.h b/libguh-core/jsonrpc/jsonrpcserver.h index 52ae53ad..25a00908 100644 --- a/libguh-core/jsonrpc/jsonrpcserver.h +++ b/libguh-core/jsonrpc/jsonrpcserver.h @@ -54,6 +54,7 @@ public: Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap ¶ms); @@ -62,8 +63,9 @@ public: signals: void CloudConnectedChanged(const QVariantMap &map); + void PushButtonAuthFinished(const QVariantMap ¶ms); - // Internal + // Server API public: void registerTransportInterface(TransportInterface *interface, bool authenticationRequired); void unregisterTransportInterface(TransportInterface *interface); @@ -90,14 +92,16 @@ private slots: void pairingFinished(QString cognitoUserId, int status, const QString &message); void onCloudConnectedChanged(bool connected); + void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); private: - QMap m_interfaces; + QMap m_interfaces; // Interface, authenticationRequired QHash m_handlers; QHash m_asyncReplies; - // clientId, notificationsEnabled - QHash m_clients; + QHash m_clientTransports; + QHash m_clientNotifications; + QHash m_pushButtonTransactions; QHash m_pairingRequests; diff --git a/libguh-core/libguh-core.pro b/libguh-core/libguh-core.pro index ac432317..a9d2371d 100644 --- a/libguh-core/libguh-core.pro +++ b/libguh-core/libguh-core.pro @@ -75,6 +75,9 @@ HEADERS += guhcore.h \ cloudmanager.h \ MbedTLS/MbedTLSConnection.hpp \ janusconnector.h \ + pushbuttondbusservice.h \ + guhdbusservice.h + SOURCES += guhcore.cpp \ tcpserver.cpp \ @@ -132,3 +135,5 @@ SOURCES += guhcore.cpp \ cloudmanager.cpp \ MbedTLS/MbedTLSConnection.cpp \ janusconnector.cpp \ + pushbuttondbusservice.cpp \ + guhdbusservice.cpp diff --git a/libguh-core/pushbuttondbusservice.cpp b/libguh-core/pushbuttondbusservice.cpp new file mode 100644 index 00000000..0214710d --- /dev/null +++ b/libguh-core/pushbuttondbusservice.cpp @@ -0,0 +1,87 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "pushbuttondbusservice.h" +#include "loggingcategories.h" + +#include +#include +#include + +namespace guhserver { + +PushButtonDBusService::PushButtonDBusService(const QString &objectPath, UserManager *parent) : GuhDBusService(objectPath, parent), + m_userManager(parent) +{ + if (!isValid()) { + qCWarning(dcUserManager()) << "Failed to register PushButton D-Bus service."; + return; + } + qCDebug(dcUserManager()) << "PushButton D-Bus service set up."; +} + +bool PushButtonDBusService::agentAvailable() const +{ + return m_registeredAgents.count() > 0; +} + +void PushButtonDBusService::RegisterButtonAgent(const QDBusObjectPath &agentPath) +{ + QDBusMessage msg = message(); + connection().connect(QString(), QString(agentPath.path()), QString(), "PushButtonPressed", this, SIGNAL(pushButtonPressed())); + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(msg.service(), connection(), QDBusServiceWatcher::WatchForUnregistration, this); + connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, &PushButtonDBusService::serviceUnregistered); + qCDebug(dcUserManager()) << "PushButton handler" << agentPath.path() << msg.service() << "registered."; + m_registeredAgents.append(msg.service()); +} + +void PushButtonDBusService::UnregisterButtonAgent() +{ + QDBusMessage msg = message(); + serviceUnregistered(msg.service()); +} + +QByteArray PushButtonDBusService::GenerateAuthToken(const QString &deviceName) +{ + int transactionId = m_userManager->requestPushButtonAuth(deviceName); + bool success = false; + QByteArray token; + QMetaObject::Connection c = connect(m_userManager, &UserManager::pushButtonAuthFinished, this, [&] (int i, bool s, const QByteArray &t) { + if (transactionId == i) { + success = s; + token = t; + } + }); + emit pushButtonPressed(); + disconnect(c); + return token; +} + +void PushButtonDBusService::serviceUnregistered(const QString &serviceName) +{ + if (!m_registeredAgents.contains(serviceName)) { + qCWarning(dcUserManager()) << "PushButton agent" << serviceName << "not known. Cannot unregister."; + return; + } + m_registeredAgents.removeAll(serviceName); + qCDebug(dcUserManager()) << "PushButton agent" << serviceName << "unregistered"; +} + +} diff --git a/libguh-core/pushbuttondbusservice.h b/libguh-core/pushbuttondbusservice.h new file mode 100644 index 00000000..40d83e0a --- /dev/null +++ b/libguh-core/pushbuttondbusservice.h @@ -0,0 +1,56 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PUSHBUTTONDBUSSERVICE_H +#define PUSHBUTTONDBUSSERVICE_H + +#include +#include "usermanager.h" +#include "guhdbusservice.h" + +namespace guhserver { + +class PushButtonDBusService : public GuhDBusService +{ + Q_OBJECT +public: + explicit PushButtonDBusService(const QString &objectPath, UserManager *parent); + + bool agentAvailable() const; + + Q_SCRIPTABLE void RegisterButtonAgent(const QDBusObjectPath &agentPath); + Q_SCRIPTABLE void UnregisterButtonAgent(); + + Q_SCRIPTABLE QByteArray GenerateAuthToken(const QString &deviceName); + +signals: + void pushButtonPressed(); + +private slots: + void serviceUnregistered(const QString &serviceName); + +private: + UserManager *m_userManager; + QStringList m_registeredAgents; +}; + +} + +#endif // PUSHBUTTONDBUSSERVICE_H diff --git a/libguh-core/tcpserver.cpp b/libguh-core/tcpserver.cpp index 35c70475..2c90b768 100644 --- a/libguh-core/tcpserver.cpp +++ b/libguh-core/tcpserver.cpp @@ -81,6 +81,8 @@ void TcpServer::sendData(const QUuid &clientId, const QByteArray &data) client = m_clientList.value(clientId); if (client) { client->write(data + '\n'); + } else { + qWarning(dcTcpServer()) << "Client" << clientId << "unknown to this transport"; } } diff --git a/libguh-core/usermanager.cpp b/libguh-core/usermanager.cpp index 5b1932fd..3de327fb 100644 --- a/libguh-core/usermanager.cpp +++ b/libguh-core/usermanager.cpp @@ -22,6 +22,7 @@ #include "guhsettings.h" #include "loggingcategories.h" #include "guhcore.h" +#include "pushbuttondbusservice.h" #include #include @@ -44,6 +45,10 @@ UserManager::UserManager(QObject *parent) : QObject(parent) return; } initDB(); + + m_pushButtonDBusService = new PushButtonDBusService("/io/guh/nymead/UserManager", this); + connect(m_pushButtonDBusService, &PushButtonDBusService::pushButtonPressed, this, &UserManager::onPushButtonPressed); + m_pushButtonTransaction = qMakePair(-1, QString()); } QStringList UserManager::users() const @@ -105,6 +110,11 @@ UserManager::UserError UserManager::removeUser(const QString &username) return UserErrorNoError; } +bool UserManager::pushButtonAuthAvailable() const +{ + return m_pushButtonDBusService->agentAvailable(); +} + QByteArray UserManager::authenticate(const QString &username, const QString &password, const QString &deviceName) { if (!validateUsername(username)) { @@ -142,6 +152,35 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas return token; } +int UserManager::requestPushButtonAuth(const QString &deviceName) +{ + if (m_pushButtonTransaction.first != -1) { + qCWarning(dcUserManager()) << "PushButton authentication already in progress for device" << m_pushButtonTransaction.second << ". Cancelling..."; + cancelPushButtonAuth(m_pushButtonTransaction.first); + } + + qCDebug(dcUserManager()) << "Starting PushButton authentication for device" << deviceName; + int transactionId = ++m_pushButtonTransactionIdCounter; + m_pushButtonTransaction = qMakePair(transactionId, deviceName); + return transactionId; +} + +void UserManager::cancelPushButtonAuth(int transactionId) +{ + if (m_pushButtonTransaction.first == -1) { + qCWarning(dcUserManager()) << "No PushButton transaction in progress. Nothing to cancel."; + return; + } + if (m_pushButtonTransaction.first != transactionId) { + qCWarning(dcUserManager()) << "PushButton transaction" << transactionId << "not in progress. Cannot cancel."; + return; + } + qCDebug(dcUserManager()) << "Cancelling PushButton transaction for device:" << m_pushButtonTransaction.second; + emit pushButtonAuthFinished(m_pushButtonTransaction.first, false, QByteArray()); + m_pushButtonTransaction.first = -1; + +} + QString UserManager::userForToken(const QByteArray &token) const { if (!validateToken(token)) { @@ -220,7 +259,7 @@ bool UserManager::verifyToken(const QByteArray &token) qCDebug(dcUserManager) << "Authorisation failed for token" << token; return false; } - qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString(); + //qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString(); return true; } @@ -246,4 +285,30 @@ bool UserManager::validateToken(const QByteArray &token) const return validator.exactMatch(token); } +void UserManager::onPushButtonPressed() +{ + if (m_pushButtonTransaction.first == -1) { + qCDebug(dcUserManager()) << "PushButton pressed but don't have a transaction waiting for it."; + return; + } + + QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); + QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");") + .arg(QUuid::createUuid().toString()) + .arg("") + .arg(QString::fromUtf8(token)) + .arg(GuhCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(m_pushButtonTransaction.second); + + m_db.exec(storeTokenQuery); + if (m_db.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Error storing token in DB:" << m_db.lastError().databaseText() << m_db.lastError().driverText(); + emit pushButtonAuthFinished(m_pushButtonTransaction.first, false, QByteArray()); + } + qCDebug(dcUserManager()) << "PushButton Auth succeeded"; + emit pushButtonAuthFinished(m_pushButtonTransaction.first, true, token); + + m_pushButtonTransaction.first = -1; +} + } diff --git a/libguh-core/usermanager.h b/libguh-core/usermanager.h index a282b0d9..01a9f452 100644 --- a/libguh-core/usermanager.h +++ b/libguh-core/usermanager.h @@ -28,6 +28,8 @@ namespace guhserver { +class PushButtonDBusService; + class UserManager : public QObject { Q_OBJECT @@ -50,20 +52,33 @@ public: UserError createUser(const QString &username, const QString &password); UserError removeUser(const QString &username); + bool pushButtonAuthAvailable() const; + QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName); + int requestPushButtonAuth(const QString &deviceName); + void cancelPushButtonAuth(int transactionId); QString userForToken(const QByteArray &token) const; QList tokens(const QString &username) const; guhserver::UserManager::UserError removeToken(const QUuid &tokenId); bool verifyToken(const QByteArray &token); +signals: + void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); + private: void initDB(); bool validateUsername(const QString &username) const; bool validateToken(const QByteArray &token) const; +private slots: + void onPushButtonPressed(); + private: QSqlDatabase m_db; + PushButtonDBusService *m_pushButtonDBusService = nullptr; + int m_pushButtonTransactionIdCounter = 0; + QPair m_pushButtonTransaction; }; diff --git a/libguh-core/websocketserver.cpp b/libguh-core/websocketserver.cpp index 0be0e6a4..cdf5c4ce 100644 --- a/libguh-core/websocketserver.cpp +++ b/libguh-core/websocketserver.cpp @@ -93,6 +93,8 @@ void WebSocketServer::sendData(const QUuid &clientId, const QByteArray &data) if (client) { qCDebug(dcWebSocketServerTraffic()) << "Sending data to client" << data; client->sendTextMessage(data + '\n'); + } else { + qCWarning(dcWebSocketServer()) << "Client" << clientId << "unknown to this transport"; } } diff --git a/plugins/mock/translations/de_DE.ts b/plugins/mock/translations/de_DE.ts index ed8f7955..19d60bc3 100644 --- a/plugins/mock/translations/de_DE.ts +++ b/plugins/mock/translations/de_DE.ts @@ -12,230 +12,230 @@ MockDevice - + guh The name of the vendor (2062d64d-3232-433c-88bc-0d33c0ba2ba6) guh - + Mock Device The name of the DeviceClass (753f0d32-0468-4d08-82ed-1964aab03298) Mock Gerät - + http port The name of the paramType (d4f06047-125e-4479-9810-b54c189917f5) of Mock Device HTTP Port - + Mock Action 3 (async) The name of the ActionType fbae06d3-7666-483e-a39e-ec50fe89054e of deviceClass Mock Device Mock Aktion 3 (async) - + Mock Action 4 (broken) The name of the ActionType df3cf33d-26d5-4577-9132-9823bd33fad0 of deviceClass Mock Device Mock Aktion 4 (kaputt) - + Mock Action 5 (async, broken) The name of the ActionType bfe89a1d-3497-4121-8318-e77c37537219 of deviceClass Mock Device Mock Aktion 5 (async, kaputt) - + Mock Device (Auto created) The name of the DeviceClass (ab4257b3-7548-47ee-9bd4-7dc3004fd197) Mock Gerät (Auto erstellt) - + Mock Device (Push Button) The name of the DeviceClass (9e03144c-e436-4eea-82d9-ccb33ef778db) Mock Gerät (Drückknopf) - + Wait 3 second before you continue, the push button will be pressed automatically. The pairing info of deviceClass Mock Device (Push Button) Warte 3 Sekunden bevor du fortfährst, the Knopf wird automatisch gerückt. - + configParamInt The name of the paramType (e1f72121-a426-45e2-b475-8262b5cdf103) of Mock Devices configParamInt - + configParamBool The name of the paramType (c75723b6-ea4f-4982-9751-6c5e39c88145) of Mock Devices configParamBool - + async The name of the paramType (f2977061-4dd0-4ef5-85aa-3b7134743be3) of Mock Device async - + broken The name of the paramType (ae8f8901-f2c1-42a5-8111-6d2fc8e4c1e4) of Mock Device kaputt - + resultCount The name of the paramType (d222adb4-2f9c-4c3f-8655-76400d0fb6ce) of Mock Device Resultat Anzahl - + Dummy int state changed The name of the autocreated EventType (80baec19-54de-4948-ac46-31eabfaceb83) Int Zustand verändert - + Dummy int state The name of the ParamType of StateType (80baec19-54de-4948-ac46-31eabfaceb83) of DeviceClass Mock Device Dummy Int Zustand - + Dummy bool state changed The name of the autocreated EventType (9dd6a97c-dfd1-43dc-acbd-367932742310) Bool Zustand verändert - + Dummy bool state The name of the ParamType of StateType (9dd6a97c-dfd1-43dc-acbd-367932742310) of DeviceClass Mock Device Dummy Bool Zustand - + Mock Action 1 (with params) The name of the ActionType dea0f4e1-65e3-4981-8eaa-2701c53a9185 of deviceClass Mock Device Mock Aktion 1 (Mit Parametern) - + mockActionParam1 The name of the paramType (a2d3a256-a551-4712-a65b-ecd5a436a1cb) of Mock Device mockActionParam1 - + mockActionParam2 The name of the paramType (304a4899-18be-4e3b-94f4-d03be52f3233) of Mock Device mockActionParam2 - + Mock Action 2 (without params) The name of the ActionType defd3ed6-1a0d-400b-8879-a0202cf39935 of deviceClass Mock Device Mock Aktion (ohne Parameter - + mockParamInt The name of the paramType (0550e16d-60b9-4ba5-83f4-4d3cee656121) of Mock Device mockParamInt - + Mock Event 1 The name of the EventType 45bf3752-0fc6-46b9-89fd-ffd878b5b22b of deviceClass Mock Device (Auto created) Mock Event 1 - + Mock Event 2 The name of the EventType 863d5920-b1cf-4eb9-88bd-8f7b8583b1cf of deviceClass Mock Device (Auto created) Mock Event 2 - + color changed The name of the autocreated EventType (20dc7c22-c50e-42db-837c-2bbced939f8e) Farbe geändert - + color The name of the ParamType of StateType (20dc7c22-c50e-42db-837c-2bbced939f8e) of DeviceClass Mock Device (Push Button) Farbe - + Set color The name of the autocreated ActionType (20dc7c22-c50e-42db-837c-2bbced939f8e) Setze Farbe - + percentage changed The name of the autocreated EventType (72981c04-267a-4ba0-a59e-9921d2f3af9c) Prozent gändert - + percentage The name of the ParamType of StateType (72981c04-267a-4ba0-a59e-9921d2f3af9c) of DeviceClass Mock Device (Push Button) Prozent - + Set percentage The name of the autocreated ActionType (72981c04-267a-4ba0-a59e-9921d2f3af9c) Setze Prozentwert - + allowed values changed The name of the autocreated EventType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) Erlaubter Wert geändert - + allowed values The name of the ParamType of StateType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) of DeviceClass Mock Device (Push Button) Erlaubte Werte - + Set allowed values The name of the autocreated ActionType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) Setze erlaubten Wert - + double value changed The name of the autocreated EventType (53cd7c55-49b7-441b-b970-9048f20f0e2c) Double Wert geändert - + double value The name of the ParamType of StateType (53cd7c55-49b7-441b-b970-9048f20f0e2c) of DeviceClass Mock Device (Push Button) Double Wert - + Set double value The name of the autocreated ActionType (53cd7c55-49b7-441b-b970-9048f20f0e2c) Setze double Wert - - + + Set bool value The name of the autocreated ActionType (e680f7a4-b39e-46da-be41-fa3170fe3768) ---------- @@ -243,20 +243,20 @@ The name of the autocreated ActionType (d24ede5f-4064-4898-bb84-cfb533b1fbc0)Setze boll Wert - + Timeout action The name of the ActionType 54646e7c-bc54-4895-81a2-590d72d120f9 of deviceClass Mock Device (Push Button) Timeout Aktion - + Mock Device (Display Pin) The name of the DeviceClass (296f1fd4-e893-46b2-8a42-50d1bceb8730) Mock Gerät (Pin anzeigen) - - + + bool value changed The name of the autocreated EventType (e680f7a4-b39e-46da-be41-fa3170fe3768) ---------- @@ -264,8 +264,8 @@ The name of the autocreated EventType (d24ede5f-4064-4898-bb84-cfb533b1fbc0)Bool Wert geändert - - + + bool value The name of the ParamType of StateType (e680f7a4-b39e-46da-be41-fa3170fe3768) of DeviceClass Mock Device (Push Button) ---------- @@ -273,97 +273,97 @@ The name of the ParamType of StateType (d24ede5f-4064-4898-bb84-cfb533b1fbc0) of Bool Wert - + Text line The name of the paramType (e6acf0c7-4b8e-4296-ac62-855d20deb816) of Mock Device (InputTypes) Textzeile - + Text area The name of the paramType (716f0994-bc01-42b0-b64d-59236f7320d2) of Mock Device (InputTypes) Textfeld - + Password text The name of the paramType (e5c0d14b-c9f1-4aca-a56e-85bfa6977150) of Mock Device (InputTypes) Passwort Text - + Search text The name of the paramType (22add8c9-ee4f-43ad-8931-58e999313ac3) of Mock Device (InputTypes) Suchtext - + Mail address The name of the paramType (a8494faf-3a0f-4cf3-84b7-4b39148a838d) of Mock Device (InputTypes) Mail Adresse - + IPv4 address The name of the paramType (9e5f86a0-4bb3-4892-bff8-3fc4032af6e2) of Mock Device (InputTypes) IPv4 Adresse - + IPv6 address The name of the paramType (43bf3832-dd48-4090-a836-656e8b60216e) of Mock Device (InputTypes) IPv6 Adresse - + URL The name of the paramType (fa67229f-fcef-496f-b671-59a4b48f3ab5) of Mock Device (InputTypes) URL - + Mac address The name of the paramType (e93db587-7919-48f3-8c88-1651de63c765) of Mock Device (InputTypes) Mac Adresse - + Please enter the secret which normaly will be displayed on the device. For the mockdevice the pin is 243681. The pairing info of deviceClass Mock Device (Display Pin) Bitte geben sie den Pincode ein der normalerweise auf dem Gerät angezeit werden würde. In diesem fall lautet der Pincode 243681. - + Mock Devices The name of the plugin Mock Devices (727a4a9a-c187-446f-aadf-f1b2220607d1) Mock Gerät - + pin The name of the paramType (da820e07-22dc-4173-9c07-2f49a4e265f9) of Mock Device (Display Pin) Pin - + Mock Device (Parent) The name of the DeviceClass (a71fbde9-9a38-4bf8-beab-c8aade2608ba) Mock Gerät (Elternteil) - + Mock Device (Child) The name of the DeviceClass (40893c9f-bc47-40c1-8bf7-b390c7c1b4fc) Mock Gerät (Kind) - + parent uuid The name of the paramType (104b5288-404e-42d3-bf38-e40682e75681) of Mock Device (Child) Elternteil Uuid - + Mock Device (InputTypes) The name of the DeviceClass (515ffdf1-55e5-498d-9abc-4e2fe768f3a9) Mock Gerät (Eingabemethoden) diff --git a/plugins/mock/translations/en_US.ts b/plugins/mock/translations/en_US.ts index 72bacc14..adf3e165 100644 --- a/plugins/mock/translations/en_US.ts +++ b/plugins/mock/translations/en_US.ts @@ -12,230 +12,230 @@ MockDevice - + guh The name of the vendor (2062d64d-3232-433c-88bc-0d33c0ba2ba6) - + Mock Device The name of the DeviceClass (753f0d32-0468-4d08-82ed-1964aab03298) - + http port The name of the paramType (d4f06047-125e-4479-9810-b54c189917f5) of Mock Device - + Mock Action 3 (async) The name of the ActionType fbae06d3-7666-483e-a39e-ec50fe89054e of deviceClass Mock Device - + Mock Action 4 (broken) The name of the ActionType df3cf33d-26d5-4577-9132-9823bd33fad0 of deviceClass Mock Device - + Mock Action 5 (async, broken) The name of the ActionType bfe89a1d-3497-4121-8318-e77c37537219 of deviceClass Mock Device - + Mock Device (Auto created) The name of the DeviceClass (ab4257b3-7548-47ee-9bd4-7dc3004fd197) - + Mock Device (Push Button) The name of the DeviceClass (9e03144c-e436-4eea-82d9-ccb33ef778db) - + Wait 3 second before you continue, the push button will be pressed automatically. The pairing info of deviceClass Mock Device (Push Button) - + configParamInt The name of the paramType (e1f72121-a426-45e2-b475-8262b5cdf103) of Mock Devices - + configParamBool The name of the paramType (c75723b6-ea4f-4982-9751-6c5e39c88145) of Mock Devices - + async The name of the paramType (f2977061-4dd0-4ef5-85aa-3b7134743be3) of Mock Device - + broken The name of the paramType (ae8f8901-f2c1-42a5-8111-6d2fc8e4c1e4) of Mock Device - + resultCount The name of the paramType (d222adb4-2f9c-4c3f-8655-76400d0fb6ce) of Mock Device - + Dummy int state changed The name of the autocreated EventType (80baec19-54de-4948-ac46-31eabfaceb83) - + Dummy int state The name of the ParamType of StateType (80baec19-54de-4948-ac46-31eabfaceb83) of DeviceClass Mock Device - + Dummy bool state changed The name of the autocreated EventType (9dd6a97c-dfd1-43dc-acbd-367932742310) - + Dummy bool state The name of the ParamType of StateType (9dd6a97c-dfd1-43dc-acbd-367932742310) of DeviceClass Mock Device - + Mock Action 1 (with params) The name of the ActionType dea0f4e1-65e3-4981-8eaa-2701c53a9185 of deviceClass Mock Device - + mockActionParam1 The name of the paramType (a2d3a256-a551-4712-a65b-ecd5a436a1cb) of Mock Device - + mockActionParam2 The name of the paramType (304a4899-18be-4e3b-94f4-d03be52f3233) of Mock Device - + Mock Action 2 (without params) The name of the ActionType defd3ed6-1a0d-400b-8879-a0202cf39935 of deviceClass Mock Device - + mockParamInt The name of the paramType (0550e16d-60b9-4ba5-83f4-4d3cee656121) of Mock Device - + Mock Event 1 The name of the EventType 45bf3752-0fc6-46b9-89fd-ffd878b5b22b of deviceClass Mock Device (Auto created) - + Mock Event 2 The name of the EventType 863d5920-b1cf-4eb9-88bd-8f7b8583b1cf of deviceClass Mock Device (Auto created) - + color changed The name of the autocreated EventType (20dc7c22-c50e-42db-837c-2bbced939f8e) - + color The name of the ParamType of StateType (20dc7c22-c50e-42db-837c-2bbced939f8e) of DeviceClass Mock Device (Push Button) - + Set color The name of the autocreated ActionType (20dc7c22-c50e-42db-837c-2bbced939f8e) - + percentage changed The name of the autocreated EventType (72981c04-267a-4ba0-a59e-9921d2f3af9c) - + percentage The name of the ParamType of StateType (72981c04-267a-4ba0-a59e-9921d2f3af9c) of DeviceClass Mock Device (Push Button) - + Set percentage The name of the autocreated ActionType (72981c04-267a-4ba0-a59e-9921d2f3af9c) - + allowed values changed The name of the autocreated EventType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) - + allowed values The name of the ParamType of StateType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) of DeviceClass Mock Device (Push Button) - + Set allowed values The name of the autocreated ActionType (05f63f9c-f61e-4dcf-ad55-3f13fde2765b) - + double value changed The name of the autocreated EventType (53cd7c55-49b7-441b-b970-9048f20f0e2c) - + double value The name of the ParamType of StateType (53cd7c55-49b7-441b-b970-9048f20f0e2c) of DeviceClass Mock Device (Push Button) - + Set double value The name of the autocreated ActionType (53cd7c55-49b7-441b-b970-9048f20f0e2c) - - + + Set bool value The name of the autocreated ActionType (e680f7a4-b39e-46da-be41-fa3170fe3768) ---------- @@ -243,20 +243,20 @@ The name of the autocreated ActionType (d24ede5f-4064-4898-bb84-cfb533b1fbc0) - + Timeout action The name of the ActionType 54646e7c-bc54-4895-81a2-590d72d120f9 of deviceClass Mock Device (Push Button) - + Mock Device (Display Pin) The name of the DeviceClass (296f1fd4-e893-46b2-8a42-50d1bceb8730) - - + + bool value changed The name of the autocreated EventType (e680f7a4-b39e-46da-be41-fa3170fe3768) ---------- @@ -264,8 +264,8 @@ The name of the autocreated EventType (d24ede5f-4064-4898-bb84-cfb533b1fbc0) - - + + bool value The name of the ParamType of StateType (e680f7a4-b39e-46da-be41-fa3170fe3768) of DeviceClass Mock Device (Push Button) ---------- @@ -273,97 +273,97 @@ The name of the ParamType of StateType (d24ede5f-4064-4898-bb84-cfb533b1fbc0) of - + Text line The name of the paramType (e6acf0c7-4b8e-4296-ac62-855d20deb816) of Mock Device (InputTypes) - + Text area The name of the paramType (716f0994-bc01-42b0-b64d-59236f7320d2) of Mock Device (InputTypes) - + Password text The name of the paramType (e5c0d14b-c9f1-4aca-a56e-85bfa6977150) of Mock Device (InputTypes) - + Search text The name of the paramType (22add8c9-ee4f-43ad-8931-58e999313ac3) of Mock Device (InputTypes) - + Mail address The name of the paramType (a8494faf-3a0f-4cf3-84b7-4b39148a838d) of Mock Device (InputTypes) - + IPv4 address The name of the paramType (9e5f86a0-4bb3-4892-bff8-3fc4032af6e2) of Mock Device (InputTypes) - + IPv6 address The name of the paramType (43bf3832-dd48-4090-a836-656e8b60216e) of Mock Device (InputTypes) - + URL The name of the paramType (fa67229f-fcef-496f-b671-59a4b48f3ab5) of Mock Device (InputTypes) - + Mac address The name of the paramType (e93db587-7919-48f3-8c88-1651de63c765) of Mock Device (InputTypes) - + Please enter the secret which normaly will be displayed on the device. For the mockdevice the pin is 243681. The pairing info of deviceClass Mock Device (Display Pin) - + Mock Devices The name of the plugin Mock Devices (727a4a9a-c187-446f-aadf-f1b2220607d1) - + pin The name of the paramType (da820e07-22dc-4173-9c07-2f49a4e265f9) of Mock Device (Display Pin) - + Mock Device (Parent) The name of the DeviceClass (a71fbde9-9a38-4bf8-beab-c8aade2608ba) - + Mock Device (Child) The name of the DeviceClass (40893c9f-bc47-40c1-8bf7-b390c7c1b4fc) - + parent uuid The name of the paramType (104b5288-404e-42d3-bf38-e40682e75681) of Mock Device (Child) - + Mock Device (InputTypes) The name of the DeviceClass (515ffdf1-55e5-498d-9abc-4e2fe768f3a9) diff --git a/server/main.cpp b/server/main.cpp index 0ef69274..561e5b16 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -38,6 +38,7 @@ #include "guhcore.h" #include "guhservice.h" #include "guhsettings.h" +#include "guhdbusservice.h" #include "guhapplication.h" #include "loggingcategories.h" @@ -195,6 +196,9 @@ int main(int argc, char *argv[]) QCommandLineOption logOption({"l", "log"}, QCoreApplication::translate("main", "Specify a log file to write to, If this option is not specified, logs will be printed to the standard output."), "logfile", "/var/log/guhd.log"); parser.addOption(logOption); + QCommandLineOption dbusOption(QStringList() << "session", QCoreApplication::translate("main", "If specified, all D-Bus interfaces will be bound to the session bus instead of the system bus.")); + parser.addOption(dbusOption); + parser.process(application); // Open the logfile, if any specified @@ -234,6 +238,10 @@ int main(int argc, char *argv[]) } QLoggingCategory::installFilter(loggingCategoryFilter); + if (parser.isSet(dbusOption)) { + GuhDBusService::setBusType(QDBusConnection::SessionBus); + } + bool startForeground = parser.isSet(foregroundOption); if (startForeground) { // inform about userid diff --git a/tests/auto/api.json b/tests/auto/api.json index a850211a..7552ac96 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -1,4 +1,4 @@ -0.53 +0.54 { "methods": { "Actions.ExecuteAction": { @@ -403,7 +403,7 @@ } }, "JSONRPC.Authenticate": { - "description": "Authenticate a client to the api. This will return a new token to be used to authorize a client at the API.", + "description": "Authenticate a client to the api via user & password challenge. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. This will return a new token to be used to authorize a client at the API.", "params": { "deviceName": "String", "password": "String", @@ -411,7 +411,7 @@ }, "returns": { "o:token": "String", - "success": "String" + "success": "Bool" } }, "JSONRPC.CreateUser": { @@ -435,6 +435,7 @@ "language": "String", "name": "String", "protocol version": "String", + "pushButtonAuthAvailable": "Bool", "server": "String", "uuid": "Uuid", "version": "String" @@ -475,6 +476,16 @@ "error": "$ref:UserError" } }, + "JSONRPC.RequestPushButtonAuth": { + "description": "Authenticate a client to the api via Push Button method. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. If push button hardware is available, this will return with success and start listening for push button presses. When the push button is pressed, the PushButtonAuthFinished notification will be sent to the requesting client. The procedure will be cancelled when the connection is interrupted. If another client requests push button authentication while a procedure is still going on, the second call will take over and the first one will be notified by the PushButtonAuthFinished signal about the error. The application should make it clear to the user to not press the button when the procedure fails as this can happen for 2 reasons: a) a second user is trying to auth at the same time and only the currently active user should press the button or b) it might indicate an attacker trying to take over and snooping in for tokens.", + "params": { + "deviceName": "String" + }, + "returns": { + "success": "Bool", + "transactionId": "Int" + } + }, "JSONRPC.SetNotificationStatus": { "description": "Enable/Disable notifications for this connections.", "params": { @@ -496,7 +507,7 @@ } }, "JSONRPC.Tokens": { - "description": "Return a list of if TokenInfo objects all the tokens for the current user.", + "description": "Return a list of TokenInfo objects of all the tokens for the current user.", "params": { }, "returns": { @@ -850,6 +861,14 @@ "connected": "Bool" } }, + "JSONRPC.PushButtonAuthFinished": { + "description": "Emitted when a push button authentication reaches final state. NOTE: This notification is special. It will only be emitted to connections that did actively request a push button authentication, but also it will be emitted regardless of the notification settings. ", + "params": { + "o:token": "String", + "status": "$ref:UserError", + "transactionId": "Int" + } + }, "Logging.LogDatabaseUpdated": { "description": "Emitted whenever the database was updated. The database will be updated when a log entry was deleted. A log entry will be deleted when the corresponding device or a rule will be removed, or when the oldest entry of the database was deleted to keep to database in the size limits.", "params": { diff --git a/tests/auto/guhtestbase.cpp b/tests/auto/guhtestbase.cpp index 7ca064ba..e57c529d 100644 --- a/tests/auto/guhtestbase.cpp +++ b/tests/auto/guhtestbase.cpp @@ -199,6 +199,7 @@ void GuhTestBase::initTestCase() // Add the mockdevice m_mockTcpServer = MockTcpServer::servers().first(); m_clientId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(m_clientId); createMockDevice(); @@ -223,7 +224,7 @@ void GuhTestBase::cleanup() } } -QVariant GuhTestBase::injectAndWait(const QString &method, const QVariantMap ¶ms) +QVariant GuhTestBase::injectAndWait(const QString &method, const QVariantMap ¶ms, const QUuid &clientId) { QVariantMap call; call.insert("id", m_commandId); @@ -234,7 +235,7 @@ QVariant GuhTestBase::injectAndWait(const QString &method, const QVariantMap &pa QJsonDocument jsonDoc = QJsonDocument::fromVariant(call); QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); - m_mockTcpServer->injectData(m_clientId, jsonDoc.toJson(QJsonDocument::Compact)); + m_mockTcpServer->injectData(clientId.isNull() ? m_clientId : clientId, jsonDoc.toJson(QJsonDocument::Compact)); if (spy.count() == 0) { spy.wait(); @@ -499,6 +500,7 @@ void GuhTestBase::restartServer() QSignalSpy spy(GuhCore::instance()->deviceManager(), SIGNAL(loaded())); spy.wait(); m_mockTcpServer = MockTcpServer::servers().first(); + m_mockTcpServer->clientConnected(m_clientId); } void GuhTestBase::clearLoggingDatabase() diff --git a/tests/auto/guhtestbase.h b/tests/auto/guhtestbase.h index fdae7a27..4f379b8b 100644 --- a/tests/auto/guhtestbase.h +++ b/tests/auto/guhtestbase.h @@ -104,7 +104,7 @@ protected slots: void cleanup(); protected: - QVariant injectAndWait(const QString &method, const QVariantMap ¶ms = QVariantMap()); + QVariant injectAndWait(const QString &method, const QVariantMap ¶ms = QVariantMap(), const QUuid &clientId = QUuid()); QVariant checkNotification(const QSignalSpy &spy, const QString ¬ification); QVariantList checkNotifications(const QSignalSpy &spy, const QString ¬ification); diff --git a/tests/auto/jsonrpc/jsonrpc.pro b/tests/auto/jsonrpc/jsonrpc.pro index a93e7d16..b239ef31 100644 --- a/tests/auto/jsonrpc/jsonrpc.pro +++ b/tests/auto/jsonrpc/jsonrpc.pro @@ -2,4 +2,7 @@ include(../../../guh.pri) include(../autotests.pri) TARGET = testjsonrpc -SOURCES += testjsonrpc.cpp +SOURCES += testjsonrpc.cpp \ + ../../utils/pushbuttonagent.cpp + +HEADERS += ../../utils/pushbuttonagent.h diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index ef9a6ded..eecdefc1 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -23,6 +23,7 @@ #include "guhcore.h" #include "devicemanager.h" #include "mocktcpserver.h" +#include "../../utils/pushbuttonagent.h" #include #include @@ -65,6 +66,39 @@ private slots: void pluginConfigChangeEmitsNotification(); + /* + Cases for push button auth: + + Case 1: regular pushbutton + - alice sends JSONRPC.RequestPushButtonAuth, gets "OK" back (if push button hardware is available) + - alice pushes the hardware button and gets a notification on jsonrpc containing the token for local auth + */ + void testPushButtonAuth(); + + /* + Case 2: if we have an attacker in the network, he could try to call requestPushButtonAuth and + hope someone would eventually press the button and give him a token. In order to prevent this + any previous attempt for a push button auth needs to be cancelled when a new request comes in: + + * Mallory does RequestPushButtonAuth, gets OK back + * Alice does RequestPushButtonAuth, + * Mallory receives a "PushButtonFailed" notification + * Alice receives OK + * alice presses the hardware button + * Alice reveices a notification with token, mallory receives nothing + + Case 3: Mallory tries to hijack it back again + + * Mallory does RequestPushButtonAuth, gets OK back + * Alice does RequestPusButtonAuth, + * Alice gets ok reply, Mallory gets failed notification + * Mallory quickly does RequestPushButtonAuth again to win the fight + * Alice gets failed notification and can instruct the user to _not_ press the button now until procedure is restarted + */ + void testPushButtonAuthInterrupt(); + + void testPushButtonAuthConnectionDrop(); + private: QStringList extractRefs(const QVariant &variant); @@ -109,12 +143,22 @@ void TestJSONRPC::testHandshake() QString guhVersionString(GUH_VERSION_STRING); QVERIFY2(handShake.value("version").toString() == guhVersionString, "Handshake version doesn't match Guh version."); + // Check whether pushButtonAuth is disabled + QCOMPARE(handShake.value("pushButtonAuthAvailable").toBool(), false); + + // Now register push button agent + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(); + // And now check if it is sent again when calling JSONRPC.Hello handShake = injectAndWait("JSONRPC.Hello").toMap(); QCOMPARE(handShake.value("params").toMap().value("version").toString(), guhVersionString); m_mockTcpServer->clientDisconnected(newClientId); + // Check whether pushButtonAuth is now + QCOMPARE(handShake.value("params").toMap().value("pushButtonAuthAvailable").toBool(), true); + // And now check if it is sent again when calling JSONRPC.Hello handShake = injectAndWait("JSONRPC.Hello").toMap(); QCOMPARE(handShake.value("params").toMap().value("version").toString(), guhVersionString); @@ -876,6 +920,188 @@ void TestJSONRPC::pluginConfigChangeEmitsNotification() QCOMPARE(notificationData.first().toMap().value("notification").toString() == "Devices.PluginConfigurationChanged", true); } +void TestJSONRPC::testPushButtonAuth() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(); + + QVariantMap params; + params.insert("deviceName", "pbtestdevice"); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Setup connection to mock client + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + pushButtonAgent.sendButtonPressed(); + + if (clientSpy.count() == 0) { + clientSpy.wait(); + } + QVariantMap rsp = checkNotification(clientSpy, "JSONRPC.PushButtonAuthFinished").toMap(); + + QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId); + QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification"); +} + + + +void TestJSONRPC::testPushButtonAuthInterrupt() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(); + + // m_clientId is registered in gutTestbase already, just using it here to improve readability of the test + QUuid aliceId = m_clientId; + + // Create a new clientId for mallory and connect it to the server + QUuid malloryId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(malloryId); + + // Snoop in on everything the TCP server sends to its clients. + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + // request push button auth for client 1 (alice) and check for OK reply + QVariantMap params; + params.insert("deviceName", "alice"); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId1 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + + // Request push button auth for client 2 (mallory) + clientSpy.clear(); + params.clear(); + params.insert("deviceName", "mallory"); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Both clients should receive something. Wait for it + if (clientSpy.count() < 2) { + clientSpy.wait(); + } + + // spy.at(0) should be the failed notification for alice + // spy.at(1) shpuld be the OK reply for mallory + + + // alice should have received a failed notification. She knows something's wrong. + QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), aliceId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId1); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); + + // Mallory instead should have received an OK + QVariantMap reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.at(1).first().toUuid(), malloryId); + QCOMPARE(reply.value("params").toMap().value("success").toBool(), true); + + + // Alice tries once more + clientSpy.clear(); + params.clear(); + params.insert("deviceName", "alice"); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId3 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Both clients should receive something. Wait for it + if (clientSpy.count() < 2) { + clientSpy.wait(); + } + + // spy.at(0) should be the failed notification for mallory + // spy.at(1) shpuld be the OK reply for alice + + // mallory should have received a failed notification. She knows something's wrong. + notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), malloryId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId2); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); + + // Alice instead should have received an OK + reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.at(1).first().toUuid(), aliceId); + QCOMPARE(reply.value("params").toMap().value("success").toBool(), true); + + clientSpy.clear(); + + // do the button press + pushButtonAgent.sendButtonPressed(); + + // Wait for things to happen + if (clientSpy.count() == 0) { + clientSpy.wait(); + } + + // There should have been only exactly one message sent, the token for alice + // Mallory should not have received anything + QCOMPARE(clientSpy.count(), 1); + notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), aliceId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); + QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); +} + +void TestJSONRPC::testPushButtonAuthConnectionDrop() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(); + + // Snoop in on everything the TCP server sends to its clients. + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + // Create a new clientId for alice and connect it to the server + QUuid aliceId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(aliceId); + + // request push button auth for client 1 (alice) and check for OK reply + QVariantMap params; + params.insert("deviceName", "alice"); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + + // Disconnect alice + m_mockTcpServer->clientDisconnected(aliceId); + + // Now try with bob + // Create a new clientId for bob and connect it to the server + QUuid bobId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(bobId); + + // request push button auth for client 1 (alice) and check for OK reply + params.clear(); + params.insert("deviceName", "bob"); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); + + clientSpy.clear(); + + pushButtonAgent.sendButtonPressed(); + + // Wait for things to happen + if (clientSpy.count() == 0) { + clientSpy.wait(); + } + + // There should have been only exactly one message sent, the token for bob + QCOMPARE(clientSpy.count(), 1); + QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), bobId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); + QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); + +} + #include "testjsonrpc.moc" QTEST_MAIN(TestJSONRPC) diff --git a/tests/libguh-core/libguh-core.pro b/tests/libguh-core/libguh-core.pro deleted file mode 100644 index 43223ff4..00000000 --- a/tests/libguh-core/libguh-core.pro +++ /dev/null @@ -1,19 +0,0 @@ -include(../../guh.pri) -include(../../server/server.pri) - -TARGET = guh-core -TEMPLATE = lib - -QT += sql - -DEFINES += TESTING_ENABLED - -INCLUDEPATH += $$top_srcdir/server/ \ - $$top_srcdir/server/jsonrpc \ - $$top_srcdir/libguh \ - $$top_srcdir/tests/auto/ - -LIBS += -L$$top_builddir/libguh/ -lguh -lssl -lcrypto -laws-iot-sdk-cpp -lmbedtls -lmbedx509 -lmbedcrypto - -target.path = /usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH') -INSTALLS += target diff --git a/tests/scripts/getconfig.sh b/tests/scripts/getconfig.sh new file mode 100755 index 00000000..82634ffe --- /dev/null +++ b/tests/scripts/getconfig.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $1 ]; then + echo "usage $0 host" +else + (echo '{"id":1, "method":"Configuration.GetConfigurations"}'; sleep 1) | nc $1 2222 +fi diff --git a/tests/scripts/hello.sh b/tests/scripts/hello.sh new file mode 100755 index 00000000..6e7f2155 --- /dev/null +++ b/tests/scripts/hello.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $1 ]; then + echo "usage: $0 host" +else + (echo '{"id":1, "method": "JSONRPC.Hello"}'; sleep 1) | nc $1 2222 +fi diff --git a/tests/scripts/pushbuttonauthenticate.sh b/tests/scripts/pushbuttonauthenticate.sh new file mode 100755 index 00000000..bc63d546 --- /dev/null +++ b/tests/scripts/pushbuttonauthenticate.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $2 ]; then + echo "usage: $0 host devicename" +else + (echo '{"id":1, "method": "JSONRPC.RequestPushButtonAuth", "params": { "deviceName": "'$2'"}}'; sleep 1) | nc $1 2222 +fi diff --git a/tests/tests.pro b/tests/tests.pro index 2ede5161..06c1e4b4 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,3 +1,3 @@ TEMPLATE = subdirs -SUBDIRS = auto +SUBDIRS = auto tools/simplepushbuttonhandler diff --git a/tests/tools/simplepushbuttonhandler/inputwatcher.cpp b/tests/tools/simplepushbuttonhandler/inputwatcher.cpp new file mode 100644 index 00000000..24bcfbda --- /dev/null +++ b/tests/tools/simplepushbuttonhandler/inputwatcher.cpp @@ -0,0 +1,40 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "inputwatcher.h" + +#include +#include +#include + +#include + +InputWatcher::InputWatcher(QObject *parent) : QObject(parent) +{ + m_notifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, this); + connect(m_notifier, SIGNAL(activated(int)), this, SLOT(readyRead())); +} + +void InputWatcher::readyRead() +{ + QTextStream qin(stdin); + qin.readLine(); + emit enterPressed(); +} diff --git a/tests/tools/simplepushbuttonhandler/inputwatcher.h b/tests/tools/simplepushbuttonhandler/inputwatcher.h new file mode 100644 index 00000000..62321fac --- /dev/null +++ b/tests/tools/simplepushbuttonhandler/inputwatcher.h @@ -0,0 +1,44 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INPUTWATCHER_H +#define INPUTWATCHER_H + +#include + +class QSocketNotifier; + +class InputWatcher : public QObject +{ + Q_OBJECT +public: + explicit InputWatcher(QObject *parent = nullptr); + +private slots: + void readyRead(); + +signals: + void enterPressed(); + +private: + QSocketNotifier *m_notifier; +}; + +#endif // INPUTWATCHER_H diff --git a/tests/tools/simplepushbuttonhandler/main.cpp b/tests/tools/simplepushbuttonhandler/main.cpp new file mode 100644 index 00000000..7172d238 --- /dev/null +++ b/tests/tools/simplepushbuttonhandler/main.cpp @@ -0,0 +1,47 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 +#include +#include +#include + +#include "../../utils/pushbuttonagent.h" +#include "inputwatcher.h" + +int main(int argc, char *argv[]) +{ + QCommandLineParser parser; + parser.addHelpOption(); + QCommandLineOption dbusOption(QStringList() << "session", QCoreApplication::translate("main", "If specified, all D-Bus interfaces will be bound to the session bus instead of the system bus.")); + parser.addOption(dbusOption); + + QCoreApplication a(argc, argv); + + PushButtonAgent agent; + if (!agent.init(parser.isSet(dbusOption) ? QDBusConnection::SessionBus : QDBusConnection::SystemBus)) { + return -1; + } + InputWatcher inputWatcher; + QObject::connect(&inputWatcher, &InputWatcher::enterPressed, &agent, &PushButtonAgent::sendButtonPressed); + + qDebug() << "Use the Enter key as push button."; + return a.exec(); +} diff --git a/tests/tools/simplepushbuttonhandler/simplepushbuttonhandler.pro b/tests/tools/simplepushbuttonhandler/simplepushbuttonhandler.pro new file mode 100644 index 00000000..0ac2c7e6 --- /dev/null +++ b/tests/tools/simplepushbuttonhandler/simplepushbuttonhandler.pro @@ -0,0 +1,24 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += main.cpp \ + ../../utils/pushbuttonagent.cpp \ + inputwatcher.cpp + +HEADERS += \ + ../../utils/pushbuttonagent.h \ + inputwatcher.h diff --git a/tests/utils/pushbuttonagent.cpp b/tests/utils/pushbuttonagent.cpp new file mode 100644 index 00000000..3c7b2eb5 --- /dev/null +++ b/tests/utils/pushbuttonagent.cpp @@ -0,0 +1,57 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "pushbuttonagent.h" + +#include +#include +#include +#include + +PushButtonAgent::PushButtonAgent(QObject *parent) : QObject(parent) +{ +} + +bool PushButtonAgent::init(QDBusConnection::BusType busType) +{ + QDBusConnection bus = busType == QDBusConnection::SessionBus ? QDBusConnection::sessionBus() : QDBusConnection::systemBus(); + + bool result = bus.registerObject("/guh/pushbuttonhandler", this, QDBusConnection::ExportScriptableContents); + if (!result) { + qDebug() << "Error registering PushButton agent on D-Bus."; + return false; + } + + QDBusMessage message = QDBusMessage::createMethodCall("io.guh.nymead", "/io/guh/nymead/UserManager", QString(), "RegisterButtonAgent"); + message << qVariantFromValue(QDBusObjectPath("/guh/pushbuttonhandler")); + QDBusMessage reply = bus.call(message); + if (!reply.errorName().isEmpty()) { + qDebug() << "Error registering PushButton agent:" << reply.errorMessage(); + return false; + } + qDebug() << "PushButton agent registered."; + return true; +} + +void PushButtonAgent::sendButtonPressed() +{ + qDebug() << "Sending button pressed event."; + emit PushButtonPressed(); +} diff --git a/tests/utils/pushbuttonagent.h b/tests/utils/pushbuttonagent.h new file mode 100644 index 00000000..9e411f22 --- /dev/null +++ b/tests/utils/pushbuttonagent.h @@ -0,0 +1,42 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PUSHBUTTONAGENT_H +#define PUSHBUTTONAGENT_H + +#include +#include + +class PushButtonAgent : public QObject +{ + Q_OBJECT +public: + explicit PushButtonAgent(QObject *parent = nullptr); + + bool init(QDBusConnection::BusType busType = QDBusConnection::SystemBus); + +signals: + Q_SCRIPTABLE void PushButtonPressed(); + +public slots: + void sendButtonPressed(); +}; + +#endif // PUSHBUTTONAGENT_H diff --git a/translations/guhd-de_DE.ts b/translations/guhd-de_DE.ts index c7671883..5b6e744a 100644 --- a/translations/guhd-de_DE.ts +++ b/translations/guhd-de_DE.ts @@ -4,7 +4,7 @@ main - + guh ( /[guːh]/ ) is an open source IoT (Internet of Things) server, which allows to control a lot of different devices from many different @@ -23,12 +23,12 @@ Szenen undVerhaltensweisen des Systems festzulegen. - + Run guhd in the foreground, not as daemon. Starte guhd im Vordergrund, nicht als Service. - + Debug categories to enable. Prefix with "No" to disable. Warnings from all categories will be printed unless explicitly muted with "NoWarnings". Categories are: @@ -36,17 +36,22 @@ Categories are: Es gibt folgende Kategorien: - + Enables all debug categories. This parameter overrides all debug category parameters. Aktiviere alle Debug-Kategorien. Dieser Parameter überschreibt alle anderen Debug-Kategorien Parameter. - + Specify a log file to write to, If this option is not specified, logs will be printed to the standard output. - + + If specified, all D-Bus interfaces will be bound to the session bus instead of the system bus. + + + + No such debug category: Diese Debug-Kategorie existiert nicht: diff --git a/translations/guhd-en_US.ts b/translations/guhd-en_US.ts index 287005b4..863daa0c 100644 --- a/translations/guhd-en_US.ts +++ b/translations/guhd-en_US.ts @@ -4,7 +4,7 @@ main - + guh ( /[guːh]/ ) is an open source IoT (Internet of Things) server, which allows to control a lot of different devices from many different @@ -23,12 +23,12 @@ for your environment. - + Run guhd in the foreground, not as daemon. Run guhd in the foreground, not as daemon. - + Debug categories to enable. Prefix with "No" to disable. Warnings from all categories will be printed unless explicitly muted with "NoWarnings". Categories are: @@ -37,17 +37,22 @@ Categories are: Categories are: - + Enables all debug categories. This parameter overrides all debug category parameters. Enables all debug categories. This parameter overrides all debug category parameters. - + Specify a log file to write to, If this option is not specified, logs will be printed to the standard output. - + + If specified, all D-Bus interfaces will be bound to the session bus instead of the system bus. + + + + No such debug category: No such debug category: