From 538cb3d799eb0b870121a102fa2d692529ce5909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Sat, 4 Aug 2018 13:54:30 +0200 Subject: [PATCH] Start implementing JsonRpcServer --- README.md | 2 + .../authentication/authenticationreply.cpp | 46 +++ .../authentication/authenticationreply.h | 49 ++++ .../authentication/authenticator.cpp | 13 + .../authentication/authenticator.h | 29 ++ .../authentication/awsauthenticator.cpp | 17 ++ .../authentication/awsauthenticator.h | 21 ++ libnymea-remoteproxy/engine.cpp | 71 ++++- libnymea-remoteproxy/engine.h | 17 +- .../jsonrpc/authenticationhandler.cpp | 51 ++++ .../jsonrpc/authenticationhandler.h | 29 ++ libnymea-remoteproxy/jsonrpc/jsonhandler.cpp | 117 ++++++++ libnymea-remoteproxy/jsonrpc/jsonhandler.h | 43 +++ libnymea-remoteproxy/jsonrpc/jsonreply.cpp | 82 ++++++ libnymea-remoteproxy/jsonrpc/jsonreply.h | 60 ++++ libnymea-remoteproxy/jsonrpc/jsontypes.cpp | 261 ++++++++++++++++++ libnymea-remoteproxy/jsonrpc/jsontypes.h | 90 ++++++ libnymea-remoteproxy/jsonrpcserver.cpp | 157 +++++++++++ libnymea-remoteproxy/jsonrpcserver.h | 54 ++++ libnymea-remoteproxy/libnymea-remoteproxy.pro | 26 +- libnymea-remoteproxy/loggingcategories.cpp | 4 +- libnymea-remoteproxy/loggingcategories.h | 4 +- libnymea-remoteproxy/proxyclient.cpp | 23 ++ libnymea-remoteproxy/proxyclient.h | 26 ++ libnymea-remoteproxy/proxyconfiguration.cpp | 6 + libnymea-remoteproxy/proxyconfiguration.h | 18 ++ libnymea-remoteproxy/proxyserver.cpp | 62 +++++ libnymea-remoteproxy/proxyserver.h | 33 +++ libnymea-remoteproxy/transportinterface.cpp | 5 + libnymea-remoteproxy/transportinterface.h | 5 + libnymea-remoteproxy/websocketserver.cpp | 3 +- .../libnymea-remoteproxyclient.pro | 6 +- .../remoteproxyconnector.cpp | 91 +++--- .../remoteproxyconnector.h | 20 +- .../websocketconnector.cpp | 6 + .../websocketconnector.h | 17 ++ nymea-remoteproxy.pri | 5 + server/main.cpp | 12 +- tests/mockauthenticator.cpp | 25 ++ tests/mockauthenticator.h | 30 ++ tests/nymea-remoteproxy-tests.cpp | 79 ++++-- tests/nymea-remoteproxy-tests.h | 4 + tests/tests.pro | 6 +- 43 files changed, 1629 insertions(+), 96 deletions(-) create mode 100644 libnymea-remoteproxy/authentication/authenticationreply.cpp create mode 100644 libnymea-remoteproxy/authentication/authenticationreply.h create mode 100644 libnymea-remoteproxy/authentication/authenticator.cpp create mode 100644 libnymea-remoteproxy/authentication/authenticator.h create mode 100644 libnymea-remoteproxy/authentication/awsauthenticator.cpp create mode 100644 libnymea-remoteproxy/authentication/awsauthenticator.h create mode 100644 libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp create mode 100644 libnymea-remoteproxy/jsonrpc/authenticationhandler.h create mode 100644 libnymea-remoteproxy/jsonrpc/jsonhandler.cpp create mode 100644 libnymea-remoteproxy/jsonrpc/jsonhandler.h create mode 100644 libnymea-remoteproxy/jsonrpc/jsonreply.cpp create mode 100644 libnymea-remoteproxy/jsonrpc/jsonreply.h create mode 100644 libnymea-remoteproxy/jsonrpc/jsontypes.cpp create mode 100644 libnymea-remoteproxy/jsonrpc/jsontypes.h create mode 100644 libnymea-remoteproxy/jsonrpcserver.cpp create mode 100644 libnymea-remoteproxy/jsonrpcserver.h create mode 100644 libnymea-remoteproxy/proxyclient.cpp create mode 100644 libnymea-remoteproxy/proxyclient.h create mode 100644 libnymea-remoteproxy/proxyconfiguration.cpp create mode 100644 libnymea-remoteproxy/proxyconfiguration.h create mode 100644 libnymea-remoteproxy/proxyserver.cpp create mode 100644 libnymea-remoteproxy/proxyserver.h create mode 100644 libnymea-remoteproxyclient/websocketconnector.cpp create mode 100644 libnymea-remoteproxyclient/websocketconnector.h create mode 100644 tests/mockauthenticator.cpp create mode 100644 tests/mockauthenticator.h diff --git a/README.md b/README.md index 271dd99..8a7034b 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ The first data a client **must** send to the proxy server is the authentication "id": 0, "method": "Authentication.Authenticate" "params": { + "name": "string", + "id": "uuid", "token": "tokenstring" } } diff --git a/libnymea-remoteproxy/authentication/authenticationreply.cpp b/libnymea-remoteproxy/authentication/authenticationreply.cpp new file mode 100644 index 0000000..78949e9 --- /dev/null +++ b/libnymea-remoteproxy/authentication/authenticationreply.cpp @@ -0,0 +1,46 @@ +#include "authenticationreply.h" +#include "authentication/authenticator.h" + +AuthenticationReply::AuthenticationReply(const QUuid clientId, const QString &token, QObject *parent) : + QObject(parent), + m_clientId(clientId), + m_token(token) +{ + +} + +QUuid AuthenticationReply::clientId() const +{ + return m_clientId; +} + +QString AuthenticationReply::token() const +{ + return m_token; +} + +bool AuthenticationReply::isTimedOut() const +{ + return m_timedOut; +} + +bool AuthenticationReply::isFinished() const +{ + return m_finished; +} + +void AuthenticationReply::setError(Authenticator::AuthenticationError error) +{ + m_error = error; +} + +void AuthenticationReply::onTimeout() +{ + + +} + +void AuthenticationReply::abort() +{ + +} diff --git a/libnymea-remoteproxy/authentication/authenticationreply.h b/libnymea-remoteproxy/authentication/authenticationreply.h new file mode 100644 index 0000000..369ed8f --- /dev/null +++ b/libnymea-remoteproxy/authentication/authenticationreply.h @@ -0,0 +1,49 @@ +#ifndef AUTHENTICATIONREPLY_H +#define AUTHENTICATIONREPLY_H + +#include +#include +#include +#include + +#include "authenticator.h" + +class AuthenticationReply : public QObject +{ + Q_OBJECT +public: + friend class Authenticator; + + explicit AuthenticationReply(const QUuid clientId, const QString &token, QObject *parent = nullptr); + + QUuid clientId() const; + QString token() const; + + bool isTimedOut() const; + bool isFinished() const; + + Authenticator::AuthenticationError error() const; + +private: + QUuid m_clientId; + QString m_token; + QTimer m_timer; + + bool m_timedOut = false; + bool m_finished = false; + Authenticator::AuthenticationError m_error; + + void setError(Authenticator::AuthenticationError error); + +signals: + void finished(); + +private slots: + void onTimeout(); + +public slots: + void abort(); + +}; + +#endif // AUTHENTICATIONREPLY_H diff --git a/libnymea-remoteproxy/authentication/authenticator.cpp b/libnymea-remoteproxy/authentication/authenticator.cpp new file mode 100644 index 0000000..42e4da5 --- /dev/null +++ b/libnymea-remoteproxy/authentication/authenticator.cpp @@ -0,0 +1,13 @@ +#include "authenticator.h" +#include "authenticationreply.h" + +Authenticator::Authenticator(QObject *parent) : + QObject(parent) +{ + +} + +Authenticator::~Authenticator() +{ + +} diff --git a/libnymea-remoteproxy/authentication/authenticator.h b/libnymea-remoteproxy/authentication/authenticator.h new file mode 100644 index 0000000..ae29626 --- /dev/null +++ b/libnymea-remoteproxy/authentication/authenticator.h @@ -0,0 +1,29 @@ +#ifndef AUTHENTICATOR_H +#define AUTHENTICATOR_H + +#include +#include + +class AuthenticationReply; + +class Authenticator : public QObject +{ + Q_OBJECT +public: + enum AuthenticationError { + AuthenticationErrorNoError, + AuthenticationErrorTimeout, + AuthenticationErrorAborted, + AuthenticationErrorAuthenticationFailed, + AuthenticationErrorAuthenticationServerNotResponding + }; + Q_ENUM(AuthenticationError) + + explicit Authenticator(QObject *parent = nullptr); + virtual ~Authenticator() = 0; + +public slots: + virtual AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) = 0; +}; + +#endif // AUTHENTICATOR_H diff --git a/libnymea-remoteproxy/authentication/awsauthenticator.cpp b/libnymea-remoteproxy/authentication/awsauthenticator.cpp new file mode 100644 index 0000000..c485a47 --- /dev/null +++ b/libnymea-remoteproxy/authentication/awsauthenticator.cpp @@ -0,0 +1,17 @@ +#include "awsauthenticator.h" +#include "loggingcategories.h" + +AwsAuthenticator::AwsAuthenticator(QObject *parent) : + Authenticator(parent) +{ + +} + +AuthenticationReply *AwsAuthenticator::authenticate(const QUuid &clientId, const QString &token) +{ + qCDebug(dcAuthenticator()) << "Start authenticating" << clientId << "using token" << token; + + AuthenticationReply *reply = new AuthenticationReply(clientId, token, this); + + return reply; +} diff --git a/libnymea-remoteproxy/authentication/awsauthenticator.h b/libnymea-remoteproxy/authentication/awsauthenticator.h new file mode 100644 index 0000000..766014f --- /dev/null +++ b/libnymea-remoteproxy/authentication/awsauthenticator.h @@ -0,0 +1,21 @@ +#ifndef AWSAUTHENTICATOR_H +#define AWSAUTHENTICATOR_H + +#include + +#include "authenticator.h" +#include "authenticationreply.h" + +class AwsAuthenticator : public Authenticator +{ + Q_OBJECT +public: + explicit AwsAuthenticator(QObject *parent = nullptr); + ~AwsAuthenticator() override = default; + +public slots: + AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) override; + +}; + +#endif // AWSAUTHENTICATOR_H diff --git a/libnymea-remoteproxy/engine.cpp b/libnymea-remoteproxy/engine.cpp index 2465ca2..0bf796a 100644 --- a/libnymea-remoteproxy/engine.cpp +++ b/libnymea-remoteproxy/engine.cpp @@ -13,11 +13,6 @@ Engine *Engine::instance() return s_instance; } -bool Engine::exists() -{ - return s_instance != nullptr; -} - void Engine::destroy() { qCDebug(dcEngine()) << "Destroy server engine"; @@ -28,12 +23,24 @@ void Engine::destroy() s_instance = nullptr; } +bool Engine::exists() +{ + return s_instance != nullptr; +} + void Engine::start() { if (!m_running) qCDebug(dcEngine()) << "Start server engine"; - qCDebug(dcEngine()) << "Starting websocket server"; + // Init proxy server + if (m_proxyServer) { + delete m_proxyServer; + m_proxyServer = nullptr; + } + + m_proxyServer = new ProxyServer(this); + // Init WebSocketServer if (m_webSocketServer) { delete m_webSocketServer; @@ -47,7 +54,14 @@ void Engine::start() m_webSocketServer = new WebSocketServer(m_sslConfiguration, this); m_webSocketServer->setServerUrl(websocketServerUrl); - m_webSocketServer->startServer(); + + m_proxyServer->registerTransportInterface(m_webSocketServer); + + // Make sure an authenticator was registered + Q_ASSERT_X(m_authenticator != nullptr, "Engine", "There is no authenticator registerd."); + + qCDebug(dcEngine()) << "Starting proxy server"; + m_proxyServer->startServer(); setRunning(true); } @@ -57,8 +71,13 @@ void Engine::stop() if (m_running) qCDebug(dcEngine()) << "Stop server engine"; + if (m_proxyServer) { + m_proxyServer->stopServer(); + m_proxyServer->deleteLater(); + m_proxyServer = nullptr; + } + if (m_webSocketServer) { - m_webSocketServer->stopServer(); m_webSocketServer->deleteLater(); m_webSocketServer = nullptr; } @@ -71,6 +90,16 @@ bool Engine::running() const return m_running; } +QString Engine::serverName() const +{ + return m_serverName; +} + +void Engine::setServerName(const QString &serverName) +{ + m_serverName = serverName; +} + void Engine::setWebSocketServerHostAddress(const QHostAddress &hostAddress) { qCDebug(dcEngine()) << "Websocket server host address:" << hostAddress; @@ -103,6 +132,32 @@ void Engine::setAuthenticationServerUrl(const QUrl &url) m_authenticationServerUrl = url; } +void Engine::setAuthenticator(Authenticator *authenticator) +{ + if (m_authenticator) { + qCDebug(dcEngine()) << "There is already an authenticator registered. Unregister default authenticator"; + m_authenticator->deleteLater(); + m_authenticator = nullptr; + + // FIXME: do unconnect + } + + m_authenticator = authenticator; + + + +} + +Authenticator *Engine::authenticator() const +{ + return m_authenticator; +} + +ProxyServer *Engine::proxyServer() const +{ + return m_proxyServer; +} + WebSocketServer *Engine::webSocketServer() const { return m_webSocketServer; diff --git a/libnymea-remoteproxy/engine.h b/libnymea-remoteproxy/engine.h index f309298..355edab 100644 --- a/libnymea-remoteproxy/engine.h +++ b/libnymea-remoteproxy/engine.h @@ -6,27 +6,36 @@ #include #include +#include "proxyserver.h" #include "websocketserver.h" +#include "authentication/authenticator.h" class Engine : public QObject { Q_OBJECT public: static Engine *instance(); - static bool exists(); void destroy(); + static bool exists(); + void start(); void stop(); bool running() const; + QString serverName() const; + void setServerName(const QString &serverName); + void setWebSocketServerHostAddress(const QHostAddress &hostAddress); void setWebSocketServerPort(const quint16 &port); - void setSslConfiguration(const QSslConfiguration &configuration); void setAuthenticationServerUrl(const QUrl &url); + void setAuthenticator(Authenticator *authenticator); + + Authenticator *authenticator() const; + ProxyServer *proxyServer() const; WebSocketServer *webSocketServer() const; private: @@ -35,13 +44,15 @@ private: static Engine *s_instance; bool m_running = false; + QString m_serverName; quint16 m_webSocketServerPort = 1212; QHostAddress m_webSocketServerHostAddress = QHostAddress::LocalHost; - QSslConfiguration m_sslConfiguration; QUrl m_authenticationServerUrl; + Authenticator *m_authenticator = nullptr; + ProxyServer *m_proxyServer = nullptr; WebSocketServer *m_webSocketServer = nullptr; void setRunning(bool running); diff --git a/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp new file mode 100644 index 0000000..3dfb573 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp @@ -0,0 +1,51 @@ +#include "jsontypes.h" +#include "loggingcategories.h" +#include "authenticationhandler.h" + +#include "engine.h" + +AuthenticationHandler::AuthenticationHandler(QObject *parent) : + JsonHandler(parent) +{ + // Methods + QVariantMap params; QVariantMap returns; + + setDescription("Authenticate", "Authenticate this connection. This should always be the first request to the server. The given id is the unique server/client uuid (i.e. the uuid of server/client)."); + params.insert("id", JsonTypes::basicTypeToString(JsonTypes::Uuid)); + params.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("token", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("Authenticate", params); + returns.insert("authenticationError", JsonTypes::authenticationErrorRef()); + setReturns("Authenticate", returns); + } + +QString AuthenticationHandler::name() const +{ + return "Authentication"; +} + +JsonReply *AuthenticationHandler::Authenticate(const QVariantMap ¶ms, const QUuid &clientId) +{ + qCDebug(dcJsonRpc()) << "Authenticate:" << clientId.toString(); + + QString clientName = params.value("name").toString(); + QString clientToken = params.value("token").toString(); + QUuid clientDeviceId = QUuid(params.value("id").toString()); + + Q_UNUSED(clientDeviceId); + + JsonReply *jsonReply = createAsyncReply("Authenticate"); + AuthenticationReply *authReply = Engine::instance()->authenticator()->authenticate(clientId, clientToken); + connect(authReply, &AuthenticationReply::finished, this, &AuthenticationHandler::onAuthenticationFinished); + + m_runningAuthentications.insert(authReply, jsonReply); + + return jsonReply; +} + +void AuthenticationHandler::onAuthenticationFinished() +{ + AuthenticationReply *authReply = static_cast(sender()); + JsonReply *jsonReply = m_runningAuthentications.value(authReply); + +} diff --git a/libnymea-remoteproxy/jsonrpc/authenticationhandler.h b/libnymea-remoteproxy/jsonrpc/authenticationhandler.h new file mode 100644 index 0000000..d829c12 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/authenticationhandler.h @@ -0,0 +1,29 @@ +#ifndef AUTHENTICATIONHANDLER_H +#define AUTHENTICATIONHANDLER_H + +#include + +#include "jsonhandler.h" +#include "authentication/authenticationreply.h" + +class AuthenticationHandler : public JsonHandler +{ + Q_OBJECT +public: + explicit AuthenticationHandler(QObject *parent = nullptr); + ~AuthenticationHandler() override = default; + + QString name() const override; + + Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms, const QUuid &clientId); + +private: + QHash m_runningAuthentications; + +private slots: + void onAuthenticationFinished(); + + +}; + +#endif // AUTHENTICATIONHANDLER_H diff --git a/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp b/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp new file mode 100644 index 0000000..091036e --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp @@ -0,0 +1,117 @@ +#include "jsonhandler.h" +#include +#include +#include + +#include "jsonreply.h" +#include "jsontypes.h" +#include "loggingcategories.h" + +JsonHandler::JsonHandler(QObject *parent): + QObject(parent) +{ + +} + +QVariantMap JsonHandler::introspect(const QMetaMethod::MethodType &type) +{ + QVariantMap data; + for (int i = 0; i < metaObject()->methodCount(); ++i) { + QMetaMethod method = metaObject()->method(i); + + if (method.methodType() != type) + continue; + + switch (method.methodType()) { + case QMetaMethod::Method: { + if (!m_descriptions.contains(method.name()) || !m_params.contains(method.name()) || !m_returns.contains(method.name())) { + continue; + } + QVariantMap methodData; + methodData.insert("description", m_descriptions.value(method.name())); + methodData.insert("params", m_params.value(method.name())); + methodData.insert("returns", m_returns.value(method.name())); + data.insert(name() + "." + method.name(), methodData); + break; + } + case QMetaMethod::Signal: { + if (!m_descriptions.contains(method.name()) || !m_params.contains(method.name())) { + continue; + } + if (QString(method.name()).contains(QRegExp("^[A-Z]"))) { + QVariantMap methodData; + methodData.insert("description", m_descriptions.value(method.name())); + methodData.insert("params", m_params.value(method.name())); + data.insert(name() + "." + method.name(), methodData); + } + break; + default: + ;;// Nothing to do for slots + } + } + } + return data; +} + +bool JsonHandler::hasMethod(const QString &methodName) +{ + return m_descriptions.contains(methodName) && m_params.contains(methodName) && m_returns.contains(methodName); +} + +QPair JsonHandler::validateParams(const QString &methodName, const QVariantMap ¶ms) +{ + QVariantMap paramTemplate = m_params.value(methodName); + return JsonTypes::validateMap(paramTemplate, params); +} + +QPair JsonHandler::validateReturns(const QString &methodName, const QVariantMap &returns) +{ + QVariantMap returnsTemplate = m_returns.value(methodName); + return JsonTypes::validateMap(returnsTemplate, returns); +} + +void JsonHandler::setDescription(const QString &methodName, const QString &description) +{ + for(int i = 0; i < metaObject()->methodCount(); ++i) { + QMetaMethod method = metaObject()->method(i); + if (method.name() == methodName) { + m_descriptions.insert(methodName, description); + return; + } + } + qCWarning(dcJsonRpc()) << "Cannot set description. No such method:" << methodName; +} + +void JsonHandler::setParams(const QString &methodName, const QVariantMap ¶ms) +{ + for(int i = 0; i < metaObject()->methodCount(); ++i) { + QMetaMethod method = metaObject()->method(i); + if (method.name() == methodName) { + m_params.insert(methodName, params); + return; + } + } + qCWarning(dcJsonRpc()) << "Cannot set params. No such method:" << methodName; +} + +void JsonHandler::setReturns(const QString &methodName, const QVariantMap &returns) +{ + for(int i = 0; i < metaObject()->methodCount(); ++i) { + QMetaMethod method = metaObject()->method(i); + if (method.name() == methodName) { + m_returns.insert(methodName, returns); + return; + } + } + qCWarning(dcJsonRpc()) << "Cannot set returns. No such method:" << methodName; +} + +JsonReply *JsonHandler::createReply(const QVariantMap &data) const +{ + return JsonReply::createReply(const_cast(this), data); +} + +JsonReply *JsonHandler::createAsyncReply(const QString &method) const +{ + return JsonReply::createAsyncReply(const_cast(this), method); +} diff --git a/libnymea-remoteproxy/jsonrpc/jsonhandler.h b/libnymea-remoteproxy/jsonrpc/jsonhandler.h new file mode 100644 index 0000000..90432e5 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsonhandler.h @@ -0,0 +1,43 @@ +#ifndef JSONHANDLER_H +#define JSONHANDLER_H + +#include +#include +#include +#include +#include + +class JsonReply; + +class JsonHandler : public QObject +{ + Q_OBJECT +public: + explicit JsonHandler(QObject *parent = nullptr); + + virtual QString name() const = 0; + + QVariantMap introspect(const QMetaMethod::MethodType &type); + + bool hasMethod(const QString &methodName); + QPair validateParams(const QString &methodName, const QVariantMap ¶ms); + QPair validateReturns(const QString &methodName, const QVariantMap &returns); + +private: + QHash m_descriptions; + QHash m_params; + QHash m_returns; + +signals: + void asyncReply(int id, const QVariantMap ¶ms); + +protected: + void setDescription(const QString &methodName, const QString &description); + void setParams(const QString &methodName, const QVariantMap ¶ms); + void setReturns(const QString &methodName, const QVariantMap &returns); + + JsonReply *createReply(const QVariantMap &data) const; + JsonReply *createAsyncReply(const QString &method) const; + +}; +#endif // JSONHANDLER_H diff --git a/libnymea-remoteproxy/jsonrpc/jsonreply.cpp b/libnymea-remoteproxy/jsonrpc/jsonreply.cpp new file mode 100644 index 0000000..0acefce --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsonreply.cpp @@ -0,0 +1,82 @@ +#include "jsonreply.h" + +JsonReply *JsonReply::createReply(JsonHandler *handler, const QVariantMap &data) +{ + return new JsonReply(TypeSync, handler, QString(), data); +} + +JsonReply *JsonReply::createAsyncReply(JsonHandler *handler, const QString &method) +{ + return new JsonReply(TypeAsync, handler, method); +} + +JsonReply::Type JsonReply::type() const +{ + return m_type; +} + +QVariantMap JsonReply::data() const +{ + return m_data; +} + +void JsonReply::setData(const QVariantMap &data) +{ + m_data = data; +} + +JsonHandler *JsonReply::handler() const +{ + return m_handler; +} + +QString JsonReply::method() const +{ + return m_method; +} + +QUuid JsonReply::connectionId() const +{ + return m_connectionId; +} + +void JsonReply::setClientId(const QUuid &connectionId) +{ + m_connectionId = connectionId; +} + +int JsonReply::commandId() const +{ + return m_commandId; +} + +void JsonReply::setCommandId(int commandId) +{ + m_commandId = commandId; +} + +bool JsonReply::timedOut() const +{ + return m_timedOut; +} + +void JsonReply::startWait() +{ + m_timeout.start(5000); +} + +void JsonReply::timeout() +{ + m_timedOut = true; + emit finished(); +} + +JsonReply::JsonReply(JsonReply::Type type, JsonHandler *handler, const QString &method, const QVariantMap &data): + m_type(type), + m_data(data), + m_handler(handler), + m_method(method), + m_timedOut(false) +{ + connect(&m_timeout, &QTimer::timeout, this, &JsonReply::timeout); +} diff --git a/libnymea-remoteproxy/jsonrpc/jsonreply.h b/libnymea-remoteproxy/jsonrpc/jsonreply.h new file mode 100644 index 0000000..afab2a5 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsonreply.h @@ -0,0 +1,60 @@ +#ifndef JSONRPCREPLY_H +#define JSONRPCREPLY_H + +#include +#include +#include + +#include "jsonhandler.h" + +class JsonReply: public QObject +{ + Q_OBJECT +public: + enum Type { + TypeSync, + TypeAsync + }; + + static JsonReply *createReply(JsonHandler *handler, const QVariantMap &data); + static JsonReply *createAsyncReply(JsonHandler *handler, const QString &method); + + Type type() const; + QVariantMap data() const; + void setData(const QVariantMap &data); + + JsonHandler *handler() const; + QString method() const; + + QUuid connectionId() const; + void setClientId(const QUuid &connectionId); + + int commandId() const; + void setCommandId(int commandId); + + bool timedOut() const; + +public slots: + void startWait(); + +signals: + void finished(); + +private slots: + void timeout(); + +private: + JsonReply(Type type, JsonHandler *handler, const QString &method, const QVariantMap &data = QVariantMap()); + Type m_type; + QVariantMap m_data; + + JsonHandler *m_handler; + QString m_method; + QUuid m_connectionId; + int m_commandId; + bool m_timedOut; + + QTimer m_timeout; +}; + +#endif // JSONRPCREPLY_H diff --git a/libnymea-remoteproxy/jsonrpc/jsontypes.cpp b/libnymea-remoteproxy/jsonrpc/jsontypes.cpp new file mode 100644 index 0000000..e8d8f48 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsontypes.cpp @@ -0,0 +1,261 @@ +#include "jsontypes.h" +#include +#include +#include + +#include "loggingcategories.h" + +bool JsonTypes::s_initialized = false; +QString JsonTypes::s_lastError; + +// Types +QVariantList JsonTypes::s_basicType; +QVariantList JsonTypes::s_authenticationError; + +// Objects + + + +QVariantMap JsonTypes::allTypes() +{ + QVariantMap allTypes; + + // Enums + allTypes.insert("BasicType", basicType()); + allTypes.insert("AuthenticationError", authenticationError()); + + // Types + + return allTypes; +} + +void JsonTypes::init() +{ + // Declare types + s_basicType = enumToStrings(JsonTypes::staticMetaObject, "BasicType"); + s_authenticationError = enumToStrings(Authenticator::staticMetaObject, "AuthenticationError"); + + s_initialized = true; +} + + +QPair JsonTypes::validateMap(const QVariantMap &templateMap, const QVariantMap &map) +{ + s_lastError.clear(); + + // Make sure all values defined in the template are around + foreach (const QString &key, templateMap.keys()) { + QString strippedKey = key; + strippedKey.remove(QRegExp("^o:")); + if (!key.startsWith("o:") && !map.contains(strippedKey)) { + qCWarning(dcJsonRpc()) << "*** missing key" << key; + qCWarning(dcJsonRpc()) << "Expected: " << templateMap; + qCWarning(dcJsonRpc()) << "Got: " << map; + QJsonDocument jsonDoc = QJsonDocument::fromVariant(map); + return report(false, QString("Missing key %1 in %2").arg(key).arg(QString(jsonDoc.toJson()))); + } + if (map.contains(strippedKey)) { + QPair result = validateVariant(templateMap.value(key), map.value(strippedKey)); + if (!result.first) { + qCWarning(dcJsonRpc()) << "Object not matching template" << templateMap.value(key) << map.value(strippedKey); + return result; + } + } + } + + // Make sure there aren't any other parameters than the allowed ones + foreach (const QString &key, map.keys()) { + QString optKey = "o:" + key; + + if (!templateMap.contains(key) && !templateMap.contains(optKey)) { + qCWarning(dcJsonRpc()) << "Forbidden param" << key << "in params"; + QJsonDocument jsonDoc = QJsonDocument::fromVariant(map); + return report(false, QString("Forbidden key \"%1\" in %2").arg(key).arg(QString(jsonDoc.toJson()))); + } + } + + return report(true, ""); +} + +QPair JsonTypes::validateVariant(const QVariant &templateVariant, const QVariant &variant) +{ + switch(templateVariant.type()) { + case QVariant::String: + if (templateVariant.toString().startsWith("$ref:")) { + QString refName = templateVariant.toString(); + if (refName == basicTypeRef()) { + QPair result = validateBasicType(variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(basicTypeRef()); + return result; + } + } else if (refName == authenticationErrorRef()) { + QPair result = validateEnum(s_authenticationError, variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(authenticationErrorRef()); + return result; + } + } else { + Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data()); + return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName)); + } + } else { + QPair result = JsonTypes::validateProperty(templateVariant, variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << "Property not matching:" << templateVariant << "!=" << variant; + return result; + } + } + break; + case QVariant::Map: { + QPair result = validateMap(templateVariant.toMap(), variant.toMap()); + if (!result.first) { + return result; + } + break; + } + case QVariant::List: { + QPair result = validateList(templateVariant.toList(), variant.toList()); + if (!result.first) { + return result; + } + break; + } + default: + qCWarning(dcJsonRpc()) << "Unhandled value" << templateVariant; + return report(false, QString("Unhandled value %1.").arg(templateVariant.toString())); + } + return report(true, ""); + +} + +QPair JsonTypes::validateEnum(const QVariantList &enumList, const QVariant &value) +{ + QStringList enumStrings; + foreach (const QVariant &variant, enumList) { + enumStrings.append(variant.toString()); + } + + bool valid = enumStrings.contains(value.toString()); + QString errorMessage = QString("Value %1 not allowed in %2").arg(value.toString()).arg(enumStrings.join(", ")); + if (!valid) + qCWarning(dcJsonRpc()) << errorMessage; + + return report(valid, errorMessage); +} + +QPair JsonTypes::validateProperty(const QVariant &templateValue, const QVariant &value) +{ + QString strippedTemplateValue = templateValue.toString(); + + if (strippedTemplateValue == JsonTypes::basicTypeToString(JsonTypes::Variant)) { + return report(true, ""); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(JsonTypes::Object)){ + return report(true, ""); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(QVariant::Uuid)) { + QString errorString = QString("Param %1 is not a uuid.").arg(value.toString()); + return report(value.canConvert(QVariant::Uuid), errorString); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(QVariant::String)) { + QString errorString = QString("Param %1 is not a string.").arg(value.toString()); + return report(value.canConvert(QVariant::String), errorString); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(QVariant::Bool)) { + QString errorString = QString("Param %1 is not a bool.").arg(value.toString()); + return report(value.canConvert(QVariant::Bool), errorString); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(QVariant::Int)) { + QString errorString = QString("Param %1 is not a int.").arg(value.toString()); + return report(value.canConvert(QVariant::Int), errorString); + } + if (strippedTemplateValue == JsonTypes::basicTypeToString(QVariant::UInt)) { + QString errorString = QString("Param %1 is not a int.").arg(value.toString()); + return report(value.canConvert(QVariant::UInt), errorString); + } + qCWarning(dcJsonRpc()) << QString("Unhandled property type: %1 (expected: %2)").arg(value.toString()).arg(strippedTemplateValue); + QString errorString = QString("Unhandled property type: %1 (expected: %2)").arg(value.toString()).arg(strippedTemplateValue); + return report(false, errorString); +} + +QPair JsonTypes::validateList(const QVariantList &templateList, const QVariantList &list) +{ + Q_ASSERT(templateList.count() == 1); + QVariant entryTemplate = templateList.first(); + + for (int i = 0; i < list.count(); ++i) { + QVariant listEntry = list.at(i); + QPair result = validateVariant(entryTemplate, listEntry); + if (!result.first) { + qCWarning(dcJsonRpc()) << "List entry not matching template"; + return result; + } + } + return report(true, ""); +} + +QPair JsonTypes::validateBasicType(const QVariant &variant) +{ + if (variant.canConvert(QVariant::Uuid) && QVariant(variant).convert(QVariant::Uuid)) { + return report(true, ""); + } + if (variant.canConvert(QVariant::String) && QVariant(variant).convert(QVariant::String)) { + return report(true, ""); + } + if (variant.canConvert(QVariant::Int) && QVariant(variant).convert(QVariant::Int)) { + return report(true, ""); + } + if (variant.canConvert(QVariant::Double) && QVariant(variant).convert(QVariant::Double)) { + return report(true, ""); + } + if (variant.canConvert(QVariant::Bool && QVariant(variant).convert(QVariant::Bool))) { + return report(true, ""); + } + return report(false, QString("Error validating basic type %1.").arg(variant.toString())); +} + +QString JsonTypes::basicTypeToString(const QVariant::Type &type) +{ + switch (type) { + case QVariant::Uuid: + return "Uuid"; + break; + case QVariant::String: + return "String"; + break; + case QVariant::Int: + return "Int"; + break; + case QVariant::UInt: + return "UInt"; + break; + case QVariant::Double: + return "Double"; + break; + case QVariant::Bool: + return "Bool"; + break; + default: + return QVariant::typeToName(static_cast(type)); + break; + } +} + +QPair JsonTypes::report(bool status, const QString &message) +{ + return qMakePair(status, message); +} + +QVariantList JsonTypes::enumToStrings(const QMetaObject &metaObject, const QString &enumName) +{ + int enumIndex = metaObject.indexOfEnumerator(enumName.toLatin1().data()); + Q_ASSERT_X(enumIndex >= 0, "JsonTypes", QString("Enumerator %1 not found in %2").arg(enumName).arg(metaObject.className()).toLocal8Bit()); + QMetaEnum metaEnum = metaObject.enumerator(enumIndex); + + QVariantList enumStrings; + for (int i = 0; i < metaEnum.keyCount(); i++) { + enumStrings << metaEnum.valueToKey(metaEnum.value(i)); + } + return enumStrings; +} diff --git a/libnymea-remoteproxy/jsonrpc/jsontypes.h b/libnymea-remoteproxy/jsonrpc/jsontypes.h new file mode 100644 index 0000000..a814d0c --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/jsontypes.h @@ -0,0 +1,90 @@ +#ifndef JSONTYPES_H +#define JSONTYPES_H + +#include +#include +#include +#include + +#include "authentication/authenticator.h" + +#define DECLARE_OBJECT(typeName, jsonName) \ + public: \ + static QString typeName##Ref() { return QStringLiteral("$ref:") + QStringLiteral(jsonName); if (!s_initialized) { init(); } } \ + static QVariantMap typeName##Description() { \ + if (!s_initialized) { init(); } \ + return s_##typeName; \ + } \ + private: \ + static QVariantMap s_##typeName; \ + public: + +#define DECLARE_TYPE(typeName, enumString, className, enumName) \ + public: \ + static QString typeName##Ref() { return QStringLiteral("$ref:") + QStringLiteral(enumString); if (!s_initialized) { init(); } } \ + static QVariantList typeName() { \ + if (!s_initialized) { init(); } \ + return s_##typeName; \ + } \ + static QString typeName##ToString(className::enumName value) { \ + if (!s_initialized) { init(); } \ + QMetaObject metaObject = className::staticMetaObject; \ + int enumIndex = metaObject.indexOfEnumerator(enumString); \ + QMetaEnum metaEnum = metaObject.enumerator(enumIndex); \ + return metaEnum.valueToKey(metaEnum.value(value)); \ + } \ + private: \ + static QVariantList s_##typeName; \ + public: + + +class JsonTypes +{ + Q_GADGET + Q_ENUMS(BasicType) + +public: + enum BasicType { + Uuid, + String, + Int, + UInt, + Double, + Bool, + Variant, + Object + }; + Q_ENUM(BasicType) + + static QVariantMap allTypes(); + + // Declare types + DECLARE_TYPE(basicType, "BasicType", JsonTypes, BasicType) + DECLARE_TYPE(authenticationError, "AuthenticationError", Authenticator, AuthenticationError) + + // Declare objects + + // Pack methods + + // Validation methods + static QPair validateMap(const QVariantMap &templateMap, const QVariantMap &map); + static QPair validateVariant(const QVariant &templateVariant, const QVariant &variant); + static QPair validateEnum(const QVariantList &enumList, const QVariant &value); + static QPair validateProperty(const QVariant &templateValue, const QVariant &value); + static QPair validateList(const QVariantList &templateList, const QVariantList &list); + static QPair validateBasicType(const QVariant &variant); + + // Converter + static QString basicTypeToString(const QVariant::Type &type); + +private: + static bool s_initialized; + static QString s_lastError; + + static void init(); + + static QPair report(bool status, const QString &message); + static QVariantList enumToStrings(const QMetaObject &metaObject, const QString &enumName); +}; + +#endif // JSONTYPES_H diff --git a/libnymea-remoteproxy/jsonrpcserver.cpp b/libnymea-remoteproxy/jsonrpcserver.cpp new file mode 100644 index 0000000..a25360e --- /dev/null +++ b/libnymea-remoteproxy/jsonrpcserver.cpp @@ -0,0 +1,157 @@ +#include "engine.h" +#include "jsonrpcserver.h" +#include "loggingcategories.h" +#include "jsonrpc/jsontypes.h" + +#include +#include + +JsonRpcServer::JsonRpcServer(QObject *parent) : + JsonHandler(parent) +{ + // Methods + QVariantMap params; QVariantMap returns; + + params.clear(); returns.clear(); + setDescription("Hello", "Once connected to this server, a client can get information about the server by saying Hello. The response informs the client about the server."); + setParams("Hello", params); + returns.insert("server", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("version", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("apiVersion", JsonTypes::basicTypeToString(JsonTypes::String)); + setReturns("Hello", returns); + + params.clear(); returns.clear(); + setDescription("Introspect", "Introspect this API."); + setParams("Introspect", params); + returns.insert("methods", JsonTypes::basicTypeToString(JsonTypes::Object)); + returns.insert("types", JsonTypes::basicTypeToString(JsonTypes::Object)); + returns.insert("notifications", JsonTypes::basicTypeToString(JsonTypes::Object)); + setReturns("Introspect", returns); + + QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); +} + +JsonRpcServer::~JsonRpcServer() +{ + qCDebug(dcJsonRpc()) << "Shutting down Json RPC server"; +} + +QString JsonRpcServer::name() const +{ + return "RemoteProxy"; +} + +QHash JsonRpcServer::handlers() const +{ + return m_handlers; +} + +void JsonRpcServer::registerTransportInterface(TransportInterface *interface) +{ + if (m_interfaces.contains(interface)) + return; + + connect(interface, &TransportInterface::clientConnected, this, &JsonRpcServer::clientConnected); + connect(interface, &TransportInterface::clientDisconnected, this, &JsonRpcServer::clientDisconnected); + connect(interface, &TransportInterface::dataAvailable, this, &JsonRpcServer::processData); + m_interfaces.append(interface); +} + +void JsonRpcServer::unregisterTransportInterface(TransportInterface *interface) +{ + disconnect(interface, &TransportInterface::clientConnected, this, &JsonRpcServer::clientConnected); + disconnect(interface, &TransportInterface::clientDisconnected, this, &JsonRpcServer::clientDisconnected); + disconnect(interface, &TransportInterface::dataAvailable, this, &JsonRpcServer::processData); + m_interfaces.removeOne(interface); +} + +JsonReply *JsonRpcServer::Hello(const QVariantMap ¶ms) const +{ + Q_UNUSED(params) + + QVariantMap data; + data.insert("server", QCoreApplication::applicationName()); + data.insert("name", Engine::instance()->serverName()); + data.insert("version", QCoreApplication::applicationVersion()); + data.insert("apiVersion", API_VERSION_STRING); + + return createReply(data); +} + +JsonReply *JsonRpcServer::Introspect(const QVariantMap ¶ms) const +{ + Q_UNUSED(params) + + QVariantMap data; + data.insert("types", JsonTypes::allTypes()); + + QVariantMap methods; + foreach (JsonHandler *handler, m_handlers) { + methods.unite(handler->introspect(QMetaMethod::Method)); + } + + data.insert("methods", methods); + + QVariantMap signalsMap; + foreach (JsonHandler *handler, m_handlers) { + signalsMap.unite(handler->introspect(QMetaMethod::Signal)); + } + + data.insert("notifications", signalsMap); + + return createReply(data); +} + +void JsonRpcServer::sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap ¶ms) +{ + QVariantMap response; + response.insert("id", commandId); + response.insert("status", "success"); + response.insert("params", params); + + QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact); + qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data; + interface->sendData(clientId, data); +} + +void JsonRpcServer::sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error) +{ + QVariantMap errorResponse; + errorResponse.insert("id", commandId); + errorResponse.insert("status", "error"); + errorResponse.insert("error", error); + + QByteArray data = QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact); + qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data; + interface->sendData(clientId, data); +} + +QString JsonRpcServer::formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const +{ + QJsonDocument doc = QJsonDocument::fromVariant(handler->introspect(QMetaMethod::Method).value(targetNamespace + "." + method)); + QJsonDocument doc2 = QJsonDocument::fromVariant(data); + return QString("\nMethod: %1\nTemplate: %2\nValue: %3") + .arg(targetNamespace + "." + method) + .arg(QString(doc.toJson(QJsonDocument::Indented))) + .arg(QString(doc2.toJson(QJsonDocument::Indented))); +} + +void JsonRpcServer::registerHandler(JsonHandler *handler) +{ + m_handlers.insert(handler->name(), handler); + qCDebug(dcJsonRpc()) << "Register handler" << handler->name(); + for (int i = 0; i < handler->metaObject()->methodCount(); ++i) { + QMetaMethod method = handler->metaObject()->method(i); + if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegExp("^[A-Z]"))) { + QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)"))); + } + } +} + +void JsonRpcServer::setup() +{ + registerHandler(this); + registerHandler(new AuthenticationHandler(this)); +} + diff --git a/libnymea-remoteproxy/jsonrpcserver.h b/libnymea-remoteproxy/jsonrpcserver.h new file mode 100644 index 0000000..ea487a4 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpcserver.h @@ -0,0 +1,54 @@ +#ifndef JSONRPCSERVER_H +#define JSONRPCSERVER_H + +#include + +#include "transportinterface.h" +#include "jsonrpc/jsonhandler.h" +#include "jsonrpc/authenticationhandler.h" + +class JsonRpcServer : public JsonHandler +{ + Q_OBJECT +public: + explicit JsonRpcServer(QObject *parent = nullptr); + ~JsonRpcServer() override; + + QString name() const override; + + QHash handlers() const; + + void registerTransportInterface(TransportInterface *interface); + void unregisterTransportInterface(TransportInterface *interface); + + Q_INVOKABLE JsonReply *Hello(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *Introspect(const QVariantMap ¶ms) const; + +private: + QList m_interfaces; + QHash m_handlers; + QHash m_asyncReplies; + QHash m_clientTransports; + QHash m_pairingRequests; + + int m_notificationId; + + void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap()); + void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); + QString formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const; + + void registerHandler(JsonHandler *handler); + + + +private slots: + void setup(); + void clientConnected(const QUuid &clientId); + void clientDisconnected(const QUuid &clientId); + void processData(const QUuid &clientId, const QByteArray &data); + void sendNotification(const QVariantMap ¶ms); + void asyncReplyFinished(); + +}; + +#endif // JSONRPCSERVER_H diff --git a/libnymea-remoteproxy/libnymea-remoteproxy.pro b/libnymea-remoteproxy/libnymea-remoteproxy.pro index 078a69e..ebd0205 100644 --- a/libnymea-remoteproxy/libnymea-remoteproxy.pro +++ b/libnymea-remoteproxy/libnymea-remoteproxy.pro @@ -7,13 +7,35 @@ HEADERS += \ engine.h \ loggingcategories.h \ transportinterface.h \ - websocketserver.h + websocketserver.h \ + proxyclient.h \ + proxyserver.h \ + jsonrpcserver.h \ + jsonrpc/jsonhandler.h \ + jsonrpc/jsonreply.h \ + jsonrpc/jsontypes.h \ + jsonrpc/authenticationhandler.h \ + authentication/authenticator.h \ + authentication/awsauthenticator.h \ + authentication/authenticationreply.h \ + proxyconfiguration.h SOURCES += \ engine.cpp \ loggingcategories.cpp \ transportinterface.cpp \ - websocketserver.cpp + websocketserver.cpp \ + proxyclient.cpp \ + proxyserver.cpp \ + jsonrpcserver.cpp \ + jsonrpc/jsonhandler.cpp \ + jsonrpc/jsonreply.cpp \ + jsonrpc/jsontypes.cpp \ + jsonrpc/authenticationhandler.cpp \ + authentication/authenticator.cpp \ + authentication/awsauthenticator.cpp \ + authentication/authenticationreply.cpp \ + proxyconfiguration.cpp # install header file with relative subdirectory diff --git a/libnymea-remoteproxy/loggingcategories.cpp b/libnymea-remoteproxy/loggingcategories.cpp index 92f4931..1844aa9 100644 --- a/libnymea-remoteproxy/loggingcategories.cpp +++ b/libnymea-remoteproxy/loggingcategories.cpp @@ -3,9 +3,9 @@ Q_LOGGING_CATEGORY(dcApplication, "Application") Q_LOGGING_CATEGORY(dcEngine, "Engine") Q_LOGGING_CATEGORY(dcJsonRpc, "JsonRpc") +Q_LOGGING_CATEGORY(dcJsonRpcTraffic, "JsonRpcTraffic") Q_LOGGING_CATEGORY(dcWebSocketServer, "WebSocketServer") Q_LOGGING_CATEGORY(dcWebSocketServerTraffic, "WebSocketServerTraffic") Q_LOGGING_CATEGORY(dcAuthenticator, "Authenticator") -Q_LOGGING_CATEGORY(dcDebug, "Debug") -Q_LOGGING_CATEGORY(dcConnectionManager, "ConnectionManager") +Q_LOGGING_CATEGORY(dcProxyServer, "ProxyServer") diff --git a/libnymea-remoteproxy/loggingcategories.h b/libnymea-remoteproxy/loggingcategories.h index 070af87..f7a70eb 100644 --- a/libnymea-remoteproxy/loggingcategories.h +++ b/libnymea-remoteproxy/loggingcategories.h @@ -7,10 +7,10 @@ Q_DECLARE_LOGGING_CATEGORY(dcApplication) Q_DECLARE_LOGGING_CATEGORY(dcEngine) Q_DECLARE_LOGGING_CATEGORY(dcJsonRpc) +Q_DECLARE_LOGGING_CATEGORY(dcJsonRpcTraffic) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServer) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcAuthenticator) -Q_DECLARE_LOGGING_CATEGORY(dcConnectionManager) -Q_DECLARE_LOGGING_CATEGORY(dcDebug) +Q_DECLARE_LOGGING_CATEGORY(dcProxyServer) #endif // LOGGINGCATEGORIES_H diff --git a/libnymea-remoteproxy/proxyclient.cpp b/libnymea-remoteproxy/proxyclient.cpp new file mode 100644 index 0000000..28f750a --- /dev/null +++ b/libnymea-remoteproxy/proxyclient.cpp @@ -0,0 +1,23 @@ +#include "proxyclient.h" + +ProxyClient::ProxyClient(const QUuid &clientId, QObject *parent) : + QObject(parent), + m_clientId(clientId) +{ + +} + +QUuid ProxyClient::clientId() const +{ + return m_clientId; +} + +bool ProxyClient::authenticated() const +{ + return m_authenticated; +} + +bool ProxyClient::tunnelConnected() const +{ + return m_tunnelConnected; +} diff --git a/libnymea-remoteproxy/proxyclient.h b/libnymea-remoteproxy/proxyclient.h new file mode 100644 index 0000000..e2de8e0 --- /dev/null +++ b/libnymea-remoteproxy/proxyclient.h @@ -0,0 +1,26 @@ +#ifndef PROXYCLIENT_H +#define PROXYCLIENT_H + +#include +#include + +class ProxyClient : public QObject +{ + Q_OBJECT +public: + explicit ProxyClient(const QUuid &clientId, QObject *parent = nullptr); + + QUuid clientId() const; + + bool authenticated() const; + bool tunnelConnected() const; + +private: + QUuid m_clientId; + bool m_authenticated = false; + bool m_tunnelConnected = false; + + +}; + +#endif // PROXYCLIENT_H diff --git a/libnymea-remoteproxy/proxyconfiguration.cpp b/libnymea-remoteproxy/proxyconfiguration.cpp new file mode 100644 index 0000000..a04af30 --- /dev/null +++ b/libnymea-remoteproxy/proxyconfiguration.cpp @@ -0,0 +1,6 @@ +#include "proxyconfiguration.h" + +ProxyConfiguration::ProxyConfiguration(QObject *parent) : QObject(parent) +{ + +} diff --git a/libnymea-remoteproxy/proxyconfiguration.h b/libnymea-remoteproxy/proxyconfiguration.h new file mode 100644 index 0000000..4ec1df0 --- /dev/null +++ b/libnymea-remoteproxy/proxyconfiguration.h @@ -0,0 +1,18 @@ +#ifndef PROXYCONFIGURATION_H +#define PROXYCONFIGURATION_H + +#include + +class ProxyConfiguration : public QObject +{ + Q_OBJECT +public: + explicit ProxyConfiguration(QObject *parent = nullptr); + +signals: + +public slots: + +}; + +#endif // PROXYCONFIGURATION_H diff --git a/libnymea-remoteproxy/proxyserver.cpp b/libnymea-remoteproxy/proxyserver.cpp new file mode 100644 index 0000000..248dc73 --- /dev/null +++ b/libnymea-remoteproxy/proxyserver.cpp @@ -0,0 +1,62 @@ +#include "proxyserver.h" +#include "loggingcategories.h" + +ProxyServer::ProxyServer(QObject *parent) : QObject(parent) +{ + +} + +void ProxyServer::registerTransportInterface(TransportInterface *interface) +{ + qCDebug(dcProxyServer()) << "Register transport interface" << interface->serverName(); + + if (m_transportInterfaces.contains(interface)) { + qCWarning(dcProxyServer()) << "Transport interface already registerd."; + return; + } + + connect(interface, &TransportInterface::clientConnected, this, &ProxyServer::onClientConnected); + connect(interface, &TransportInterface::clientDisconnected, this, &ProxyServer::onClientDisconnected); + connect(interface, &TransportInterface::dataAvailable, this, &ProxyServer::onClientDataAvailable); + + m_transportInterfaces.append(interface); +} + +void ProxyServer::onClientConnected(const QUuid &clientId) +{ + TransportInterface *interface = static_cast(sender()); + qCDebug(dcProxyServer()) << "New client connected" << interface->serverName() << clientId.toString(); + + m_unauthenticatedClients.append(clientId); +} + +void ProxyServer::onClientDisconnected(const QUuid &clientId) +{ + TransportInterface *interface = static_cast(sender()); + qCDebug(dcProxyServer()) << "Client disconnected" << interface->serverName() << clientId.toString(); + + // Clean up this client since it does not exist any more + m_unauthenticatedClients.removeAll(clientId); +} + +void ProxyServer::onClientDataAvailable(const QUuid &clientId, const QByteArray &data) +{ + TransportInterface *interface = static_cast(sender()); + qCDebug(dcProxyServer()) << "Client data available" << interface->serverName() << clientId.toString() << qUtf8Printable(data); +} + +void ProxyServer::startServer() +{ + qCDebug(dcProxyServer()) << "Start proxy server."; + foreach (TransportInterface *interface, m_transportInterfaces) { + interface->startServer(); + } +} + +void ProxyServer::stopServer() +{ + qCDebug(dcProxyServer()) << "Stop proxy server."; + foreach (TransportInterface *interface, m_transportInterfaces) { + interface->stopServer(); + } +} diff --git a/libnymea-remoteproxy/proxyserver.h b/libnymea-remoteproxy/proxyserver.h new file mode 100644 index 0000000..870337e --- /dev/null +++ b/libnymea-remoteproxy/proxyserver.h @@ -0,0 +1,33 @@ +#ifndef PROXYSERVER_H +#define PROXYSERVER_H + +#include +#include + +#include "proxyclient.h" +#include "transportinterface.h" + +class ProxyServer : public QObject +{ + Q_OBJECT +public: + explicit ProxyServer(QObject *parent = nullptr); + + void registerTransportInterface(TransportInterface *interface); + +private: + QList m_transportInterfaces; + QList m_unauthenticatedClients; + +private slots: + void onClientConnected(const QUuid &clientId); + void onClientDisconnected(const QUuid &clientId); + void onClientDataAvailable(const QUuid &clientId, const QByteArray &data); + +public slots: + void startServer(); + void stopServer(); + +}; + +#endif // PROXYSERVER_H diff --git a/libnymea-remoteproxy/transportinterface.cpp b/libnymea-remoteproxy/transportinterface.cpp index 5aff4e6..d5b0c12 100644 --- a/libnymea-remoteproxy/transportinterface.cpp +++ b/libnymea-remoteproxy/transportinterface.cpp @@ -6,6 +6,11 @@ TransportInterface::TransportInterface(QObject *parent) : } +QString TransportInterface::serverName() const +{ + return m_serverName; +} + TransportInterface::~TransportInterface() { diff --git a/libnymea-remoteproxy/transportinterface.h b/libnymea-remoteproxy/transportinterface.h index b1a208e..8de7bc7 100644 --- a/libnymea-remoteproxy/transportinterface.h +++ b/libnymea-remoteproxy/transportinterface.h @@ -10,6 +10,8 @@ public: explicit TransportInterface(QObject *parent = nullptr); virtual ~TransportInterface() = 0; + QString serverName() const; + virtual void sendData(const QUuid &clientId, const QByteArray &data) = 0; virtual void sendData(const QList &clients, const QByteArray &data) = 0; @@ -18,6 +20,9 @@ signals: void clientDisconnected(const QUuid &clientId); void dataAvailable(const QUuid &clientId, const QByteArray &data); +protected: + QString m_serverName; + public slots: virtual bool startServer() = 0; virtual bool stopServer() = 0; diff --git a/libnymea-remoteproxy/websocketserver.cpp b/libnymea-remoteproxy/websocketserver.cpp index d698148..24db971 100644 --- a/libnymea-remoteproxy/websocketserver.cpp +++ b/libnymea-remoteproxy/websocketserver.cpp @@ -7,7 +7,7 @@ WebSocketServer::WebSocketServer(const QSslConfiguration &sslConfiguration, QObj TransportInterface(parent), m_sslConfiguration(sslConfiguration) { - + m_serverName = "Websocket server"; } WebSocketServer::~WebSocketServer() @@ -144,7 +144,6 @@ bool WebSocketServer::startServer() bool WebSocketServer::stopServer() { - // Clean up client connections foreach (QWebSocket *client, m_clientList.values()) { client->close(QWebSocketProtocol::CloseCodeNormal, "Stop server"); diff --git a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro index 8ad071f..667855d 100644 --- a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro +++ b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro @@ -4,11 +4,13 @@ TEMPLATE = lib TARGET = nymea-remoteproxyclient HEADERS += \ - remoteproxyconnector.h + remoteproxyconnector.h \ + websocketconnector.h SOURCES += \ - remoteproxyconnector.cpp + remoteproxyconnector.cpp \ + websocketconnector.cpp # install header file with relative subdirectory diff --git a/libnymea-remoteproxyclient/remoteproxyconnector.cpp b/libnymea-remoteproxyclient/remoteproxyconnector.cpp index 87f9249..af24c11 100644 --- a/libnymea-remoteproxyclient/remoteproxyconnector.cpp +++ b/libnymea-remoteproxyclient/remoteproxyconnector.cpp @@ -1,5 +1,7 @@ #include "remoteproxyconnector.h" +Q_LOGGING_CATEGORY(dcRemoteProxyConnector, "RemoteProxyConnector") + RemoteProxyConnector::RemoteProxyConnector(QObject *parent) : QObject(parent) { @@ -10,6 +12,11 @@ RemoteProxyConnector::~RemoteProxyConnector() disconnectServer(); } +RemoteProxyConnector::State RemoteProxyConnector::state() const +{ + return m_state; +} + RemoteProxyConnector::Error RemoteProxyConnector::error() const { return m_error; @@ -66,7 +73,12 @@ QUrl RemoteProxyConnector::serverUrl() const bool RemoteProxyConnector::isConnected() const { - return m_state == StateConnected; + return m_state == StateConnected || m_state == StateAuthenticating || m_state == StateWaitTunnel || m_state == StateTunnelEstablished; +} + +bool RemoteProxyConnector::tunnelEstablished() const +{ + return m_state == StateTunnelEstablished; } RemoteProxyConnector::ConnectionType RemoteProxyConnector::connectionType() const @@ -84,31 +96,32 @@ quint16 RemoteProxyConnector::serverPort() const return m_serverPort; } -QList RemoteProxyConnector::ignoreSslErrors() const +bool RemoteProxyConnector::insecureConnection() const { - return m_ignoreSslErrors; + return m_insecureConnection; } -void RemoteProxyConnector::setIgnoreSslErrors(const QList &errors) +void RemoteProxyConnector::setInsecureConnection(bool insecureConnection) { - m_ignoreSslErrors = errors; + m_insecureConnection = insecureConnection; } bool RemoteProxyConnector::sendData(const QByteArray &data) { - if (m_state != StateTunnelEstablished) { - qWarning() << "RemoteProxyClient: There is no established tunnel for" << serverUrl().toString() << "yet."; - return false; - } + // FIXME: reenable once the auth process is finished +// if (m_state != StateTunnelEstablished) { +// qWarning() << "RemoteProxyClient: There is no established tunnel for" << serverUrl().toString() << "yet."; +// return false; +// } if (!m_webSocket) { - qWarning() << "RemoteProxyClient: There is no websocket"; + qCWarning(dcRemoteProxyConnector()) << "There is no websocket"; return false; } qint64 dataSendCount = m_webSocket->sendTextMessage(QString::fromUtf8(data)); if (dataSendCount != data.count()) { - qWarning() << "RemoteProxyClient: Could not send all data to" << serverUrl().toString(); + qCWarning(dcRemoteProxyConnector()) << "Could not send all data to" << serverUrl().toString(); return false; } @@ -120,7 +133,7 @@ void RemoteProxyConnector::setState(RemoteProxyConnector::State state) if (m_state == state) return; - qDebug() << "RemoteProxyClient: State changed" << state; + qCDebug(dcRemoteProxyConnector()) << "State changed" << state; m_state = state; emit stateChanged(m_state); } @@ -130,7 +143,7 @@ void RemoteProxyConnector::setError(RemoteProxyConnector::Error error) if (m_error == error) return; - qDebug() << "RemoteProxyClient: Error occured" << error; + qCDebug(dcRemoteProxyConnector()) << "Error occured" << error; m_error = error; emit errorOccured(m_error); } @@ -154,7 +167,8 @@ void RemoteProxyConnector::setServerPort(quint16 serverPort) void RemoteProxyConnector::onSocketConnected() { setState(StateConnected); - qDebug() << "RemoteProxyClient: Connected to" << serverUrl().toString(); + qCDebug(dcRemoteProxyConnector()) << "Connected to" << serverUrl().toString(); + emit connected(); // TODO: start authentication process @@ -163,32 +177,34 @@ void RemoteProxyConnector::onSocketConnected() void RemoteProxyConnector::onSocketDisconnected() { - qDebug() << "RemoteProxyClient: Disconnected from" << serverUrl().toString(); + qCDebug(dcRemoteProxyConnector()) << "Disconnected from" << serverUrl().toString(); setState(StateDisconnected); + emit disconnected(); } void RemoteProxyConnector::onSocketError(QAbstractSocket::SocketError error) { - qWarning() << "RemoteProxyClient: Socket error occured" << error; + qCWarning(dcRemoteProxyConnector()) << "Socket error occured" << error; setError(ErrorSocketError); } void RemoteProxyConnector::onSocketSslError(const QList &errors) { - qWarning() << "RemoteProxyClient: Socket ssl errors occured" << errors; - foreach (const QSslError sslError, errors) { - qWarning() << "RemoteProxyClient: " << static_cast(sslError.error()) << sslError.errorString(); + if (m_insecureConnection) { + qCDebug(dcRemoteProxyConnector()) << "Ignore ssl errors because explicit allowed to use an insecure connection."; + m_webSocket->ignoreSslErrors(); + } else { + qCWarning(dcRemoteProxyConnector()) << "Socket ssl errors occured:"; + foreach (const QSslError sslError, errors) { + qCWarning(dcRemoteProxyConnector()) << " -->" << static_cast(sslError.error()) << sslError.errorString(); + } + setError(ErrorSslError); } - - qDebug() << m_ignoreSslErrors; - - m_webSocket->ignoreSslErrors(); - setError(ErrorSslError); } void RemoteProxyConnector::onSocketStateChanged(QAbstractSocket::SocketState state) { - qDebug() << "RemoteProxyClient: Socket state changed" << state; + qCDebug(dcRemoteProxyConnector()) << "Socket state changed" << state; switch (state) { case QAbstractSocket::ConnectingState: case QAbstractSocket::HostLookupState: @@ -206,7 +222,7 @@ void RemoteProxyConnector::onSocketStateChanged(QAbstractSocket::SocketState sta void RemoteProxyConnector::onTextMessageReceived(const QString &message) { // TODO: check if tunnel is established, if so, emit data received - qDebug() << "RemoteProxyClient: Data recived" << message; + qCDebug(dcRemoteProxyConnector()) << "Data received" << message; } void RemoteProxyConnector::onBinaryMessageReceived(const QByteArray &message) @@ -214,24 +230,20 @@ void RemoteProxyConnector::onBinaryMessageReceived(const QByteArray &message) Q_UNUSED(message); } -bool RemoteProxyConnector::connectServer(RemoteProxyConnector::ConnectionType type, const QHostAddress &serverAddress, quint16 port) +bool RemoteProxyConnector::connectServer(const QHostAddress &serverAddress, quint16 port) { - setConnectionType(type); setServerAddress(serverAddress); setServerPort(port); switch (m_connectionType) { // TODO: currently only websocket support case ConnectionTypeWebSocket: - if (m_webSocket) { - delete m_webSocket; - m_webSocket = nullptr; - } - - setState(StateDisconnected); + disconnectServer(); m_webSocket = new QWebSocket("libnymea-remoteproxyclient", QWebSocketProtocol::VersionLatest, this); - m_webSocket->ignoreSslErrors(m_ignoreSslErrors); + + if (m_insecureConnection) + m_webSocket->ignoreSslErrors(); connect(m_webSocket, &QWebSocket::connected, this, &RemoteProxyConnector::onSocketConnected); connect(m_webSocket, &QWebSocket::disconnected, this, &RemoteProxyConnector::onSocketDisconnected); @@ -244,7 +256,7 @@ bool RemoteProxyConnector::connectServer(RemoteProxyConnector::ConnectionType ty setState(StateConnecting); m_webSocket->open(serverUrl()); - qDebug() << "RemoteProxyClient: Start connecting to" << serverUrl().toString(); + qCDebug(dcRemoteProxyConnector()) << "Start connecting to" << serverUrl().toString(); return true; } @@ -253,5 +265,12 @@ bool RemoteProxyConnector::connectServer(RemoteProxyConnector::ConnectionType ty void RemoteProxyConnector::disconnectServer() { + if (!m_webSocket) + return; + qCDebug(dcRemoteProxyConnector()) << "Disconnect from server" << serverUrl().toString(); + m_webSocket->close(QWebSocketProtocol::CloseCodeNormal, "Bye bye"); + m_webSocket->deleteLater(); + m_webSocket = nullptr; + setState(StateDisconnected); } diff --git a/libnymea-remoteproxyclient/remoteproxyconnector.h b/libnymea-remoteproxyclient/remoteproxyconnector.h index 4dd444a..cf3641b 100644 --- a/libnymea-remoteproxyclient/remoteproxyconnector.h +++ b/libnymea-remoteproxyclient/remoteproxyconnector.h @@ -1,9 +1,13 @@ #ifndef REMOTEPROXYCONNECTOR_H #define REMOTEPROXYCONNECTOR_H +#include #include #include #include +#include + +Q_DECLARE_LOGGING_CATEGORY(dcRemoteProxyConnector) class RemoteProxyConnector : public QObject { @@ -53,8 +57,8 @@ public: QHostAddress serverAddress() const; quint16 serverPort() const; - QList ignoreSslErrors() const; - void setIgnoreSslErrors(const QList &errors); + bool insecureConnection() const; + void setInsecureConnection(bool insecureConnection); bool sendData(const QByteArray &data); @@ -64,7 +68,7 @@ private: quint16 m_serverPort = 443; State m_state = StateDisconnected; Error m_error = ErrorNoError; - QList m_ignoreSslErrors; + bool m_insecureConnection = false; bool m_tunnelEstablished = false; QWebSocket *m_webSocket = nullptr; @@ -79,8 +83,8 @@ signals: void connected(); void disconnected(); void tunnelEstablished(); - void stateChanged(State state); - void errorOccured(Error error); + void stateChanged(RemoteProxyConnector::State state); + void errorOccured(RemoteProxyConnector::Error error); void dataReady(const QByteArray &data); @@ -95,9 +99,13 @@ private slots: public slots: - bool connectServer(ConnectionType type, const QHostAddress &serverAddress, quint16 port); + bool connectServer(const QHostAddress &serverAddress, quint16 port); void disconnectServer(); }; +Q_DECLARE_METATYPE(RemoteProxyConnector::State); +Q_DECLARE_METATYPE(RemoteProxyConnector::Error); +Q_DECLARE_METATYPE(RemoteProxyConnector::ConnectionType); + #endif // REMOTEPROXYCONNECTOR_H diff --git a/libnymea-remoteproxyclient/websocketconnector.cpp b/libnymea-remoteproxyclient/websocketconnector.cpp new file mode 100644 index 0000000..d9f9cc2 --- /dev/null +++ b/libnymea-remoteproxyclient/websocketconnector.cpp @@ -0,0 +1,6 @@ +#include "websocketconnector.h" + +WebSocketConnector::WebSocketConnector(QObject *parent) : QObject(parent) +{ + +} diff --git a/libnymea-remoteproxyclient/websocketconnector.h b/libnymea-remoteproxyclient/websocketconnector.h new file mode 100644 index 0000000..11f84ab --- /dev/null +++ b/libnymea-remoteproxyclient/websocketconnector.h @@ -0,0 +1,17 @@ +#ifndef WEBSOCKETCONNECTOR_H +#define WEBSOCKETCONNECTOR_H + +#include + +class WebSocketConnector : public QObject +{ + Q_OBJECT +public: + explicit WebSocketConnector(QObject *parent = nullptr); + +signals: + +public slots: +}; + +#endif // WEBSOCKETCONNECTOR_H \ No newline at end of file diff --git a/nymea-remoteproxy.pri b/nymea-remoteproxy.pri index efcba55..47a39ca 100644 --- a/nymea-remoteproxy.pri +++ b/nymea-remoteproxy.pri @@ -1,6 +1,11 @@ QT *= network websockets QT -= gui +# define protocol versions +API_VERSION_MAJOR=0 +API_VERSION_MINOR=1 +DEFINES += API_VERSION_STRING=\\\"$${API_VERSION_MAJOR}.$${API_VERSION_MINOR}\\\" + CONFIG += c++11 console QMAKE_CXXFLAGS *= -Werror -std=c++11 -g diff --git a/server/main.cpp b/server/main.cpp index 1449eda..05379a2 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -19,6 +19,7 @@ #include "engine.h" #include "loggingcategories.h" +#include "authentication/awsauthenticator.h" static QHash s_loggingFilters; @@ -37,6 +38,7 @@ static void loggingCategoryFilter(QLoggingCategory *category) category->setEnabled(QtDebugMsg, debugEnabled); category->setEnabled(QtWarningMsg, debugEnabled || s_loggingFilters.value("Warnings")); } else { + // Enable default debug output category->setEnabled(QtDebugMsg, true); category->setEnabled(QtWarningMsg, s_loggingFilters.value("Warnings")); } @@ -86,14 +88,14 @@ int main(int argc, char *argv[]) application.setOrganizationName("guh"); application.setApplicationVersion("0.0.1"); - s_loggingFilters.insert("Application", true); s_loggingFilters.insert("Engine", true); + s_loggingFilters.insert("Application", true); s_loggingFilters.insert("JsonRpc", true); + s_loggingFilters.insert("JsonRpcTraffic", true); s_loggingFilters.insert("WebSocketServer", true); s_loggingFilters.insert("WebSocketServerTraffic", false); s_loggingFilters.insert("Authenticator", true); - s_loggingFilters.insert("ConnectionManager", true); - s_loggingFilters.insert("Debug", false); + s_loggingFilters.insert("ProxyServer", true); // command line parser QCommandLineParser parser; @@ -226,7 +228,11 @@ int main(int argc, char *argv[]) if (s_loggingEnabled) qCDebug(dcApplication()) << "Logging enabled. Writing logs to" << s_logFile.fileName(); + // Create default authenticator + AwsAuthenticator *authenticator = new AwsAuthenticator(nullptr); + // Configure and start the engines + Engine::instance()->setAuthenticator(authenticator); Engine::instance()->setWebSocketServerHostAddress(serverHostAddress); Engine::instance()->setWebSocketServerPort(static_cast(port)); Engine::instance()->setSslConfiguration(sslConfiguration); diff --git a/tests/mockauthenticator.cpp b/tests/mockauthenticator.cpp new file mode 100644 index 0000000..19cbde7 --- /dev/null +++ b/tests/mockauthenticator.cpp @@ -0,0 +1,25 @@ +#include "mockauthenticator.h" +#include "loggingcategories.h" +#include "authentication/authenticationreply.h" + + +MockAuthenticator::MockAuthenticator(QObject *parent) : + Authenticator(parent) +{ + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + + connect(m_timer, &QTimer::timeout, this, &MockAuthenticator::onTimeout); +} + +void MockAuthenticator::onTimeout() +{ + +} + +AuthenticationReply *MockAuthenticator::authenticate(const QUuid &clientId, const QString &token) +{ + qCDebug(dcAuthenticator()) << "MockAuthenticator: Start authentication for" << clientId << "using token" << token; + AuthenticationReply *reply = new AuthenticationReply(clientId, token, this); + return reply; +} diff --git a/tests/mockauthenticator.h b/tests/mockauthenticator.h new file mode 100644 index 0000000..00f5d24 --- /dev/null +++ b/tests/mockauthenticator.h @@ -0,0 +1,30 @@ +#ifndef MOCKAUTHENTICATOR_H +#define MOCKAUTHENTICATOR_H + +#include +#include + +#include "authentication/authenticator.h" + +class MockAuthenticator : public Authenticator +{ + Q_OBJECT +public: + explicit MockAuthenticator(QObject *parent = nullptr); + + void setTimeoutDuration(int timeout); + void setExpectedAuthenticationError(Authenticator::AuthenticationError error); + +private: + QTimer * m_timer = nullptr; + int m_timeoutDuration = 1000; + Authenticator::AuthenticationError m_expectedError; + +private slots: + void onTimeout(); + +public slots: + AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) override; +}; + +#endif // MOCKAUTHENTICATOR_H diff --git a/tests/nymea-remoteproxy-tests.cpp b/tests/nymea-remoteproxy-tests.cpp index 188fa38..eaa1295 100644 --- a/tests/nymea-remoteproxy-tests.cpp +++ b/tests/nymea-remoteproxy-tests.cpp @@ -4,6 +4,7 @@ #include "loggingcategories.h" #include "remoteproxyconnector.h" +#include #include RemoteProxyTests::RemoteProxyTests(QObject *parent) : @@ -26,6 +27,7 @@ RemoteProxyTests::RemoteProxyTests(QObject *parent) : QByteArray keyData = keyFile.readAll(); //qDebug() << "Certificate key:" << endl << qUtf8Printable(keyData); + m_authenticator = new MockAuthenticator(this); m_sslConfiguration.setPrivateKey(QSslKey(keyData, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey)); m_sslConfiguration.setLocalCertificate(QSslCertificate(certificateData, QSsl::Pem)); @@ -53,14 +55,29 @@ void RemoteProxyTests::startServer() { restartEngine(); + Engine::instance()->setAuthenticator(m_authenticator); Engine::instance()->setWebSocketServerPort(m_port); Engine::instance()->setWebSocketServerHostAddress(QHostAddress::LocalHost); Engine::instance()->setSslConfiguration(m_sslConfiguration); Engine::instance()->start(); + + QVERIFY(Engine::instance()->running()); + +} + +void RemoteProxyTests::stopServer() +{ + if (!Engine::instance()->running()) + return; + + Engine::instance()->stop(); + QVERIFY(!Engine::instance()->running()); } void RemoteProxyTests::initTestCase() { + qRegisterMetaType(); + qCDebug(dcApplication()) << "Init test case."; restartEngine(); } @@ -73,37 +90,28 @@ void RemoteProxyTests::cleanupTestCase() void RemoteProxyTests::authenticate() { - // Start the server - startServer(); +// // Start the server +// startServer(); - // Connect to the server - RemoteProxyConnector *connector = new RemoteProxyConnector(this); - connector->setIgnoreSslErrors(QList() << QSslError::HostNameMismatch << QSslError::SelfSignedCertificate); +// // Connect to the server +// RemoteProxyConnector *connector = new RemoteProxyConnector(this); +// connector->setInsecureConnection(true); - QSignalSpy spy(connector, &RemoteProxyConnector::error); - connector->connectServer(RemoteProxyConnector::ConnectionTypeWebSocket, QHostAddress::LocalHost, m_port); - //spy.wait(); +// QSignalSpy spy(connector, &RemoteProxyConnector::connected); +// connector->connectServer(QHostAddress::LocalHost, m_port); +// spy.wait(); - connector->disconnectServer(); - connector->deleteLater(); - Engine::instance()->stop(); +// connector->disconnectServer(); +// connector->deleteLater(); +// Engine::instance()->stop(); } void RemoteProxyTests::startStopServer() { + restartEngine(); + startServer(); + stopServer(); cleanUpEngine(); - - QVERIFY(Engine::instance() != nullptr); - QVERIFY(Engine::exists()); - - Engine::instance()->start(); - QVERIFY(Engine::instance()->running()); - - Engine::instance()->stop(); - QVERIFY(!Engine::instance()->running()); - - Engine::instance()->destroy(); - QVERIFY(!Engine::exists()); } void RemoteProxyTests::sslConfigurations() @@ -111,15 +119,30 @@ void RemoteProxyTests::sslConfigurations() // Start the server startServer(); - // Connect to the server + // Connect to the server (insecure disabled) RemoteProxyConnector *connector = new RemoteProxyConnector(this); - connector->setIgnoreSslErrors(QList() << QSslError::HostNameMismatch << QSslError::SelfSignedCertificate); + connector->setInsecureConnection(false); - QSignalSpy spy(connector, &RemoteProxyConnector::connected); - connector->connectServer(RemoteProxyConnector::ConnectionTypeWebSocket, QHostAddress::LocalHost, m_port); - spy.wait(); + QSignalSpy spyError(connector, &RemoteProxyConnector::errorOccured); + connector->connectServer(QHostAddress::LocalHost, m_port); + spyError.wait(); + QCOMPARE(connector->error(), RemoteProxyConnector::ErrorSocketError); + QCOMPARE(connector->socketError(), QAbstractSocket::SslHandshakeFailedError); + QCOMPARE(connector->state(), RemoteProxyConnector::StateDisconnected); + + // Connect to server (insecue enabled) + QSignalSpy spyConnected(connector, &RemoteProxyConnector::connected); + connector->setInsecureConnection(true); + connector->connectServer(QHostAddress::LocalHost, m_port); + spyConnected.wait(); + + QVERIFY(connector->isConnected()); + + // Disconnect and clean up connector->disconnectServer(); + QVERIFY(!connector->isConnected()); + connector->deleteLater(); Engine::instance()->stop(); } diff --git a/tests/nymea-remoteproxy-tests.h b/tests/nymea-remoteproxy-tests.h index 880d944..c74d336 100644 --- a/tests/nymea-remoteproxy-tests.h +++ b/tests/nymea-remoteproxy-tests.h @@ -9,6 +9,8 @@ #include #include +#include "mockauthenticator.h" + class RemoteProxyTests : public QObject { Q_OBJECT @@ -19,10 +21,12 @@ private: quint16 m_port = 1212; QHostAddress m_serverAddress = QHostAddress::LocalHost; QSslConfiguration m_sslConfiguration; + MockAuthenticator *m_authenticator = nullptr; void cleanUpEngine(); void restartEngine(); void startServer(); + void stopServer(); protected slots: void initTestCase(); diff --git a/tests/tests.pro b/tests/tests.pro index 4e5a104..0ee4bf8 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -11,8 +11,10 @@ LIBS += -L$$top_builddir/libnymea-remoteproxy/ -lnymea-remoteproxy \ RESOURCES += certificate.qrc -HEADERS += nymea-remoteproxy-tests.h -SOURCES += nymea-remoteproxy-tests.cpp +HEADERS += nymea-remoteproxy-tests.h \ + mockauthenticator.h +SOURCES += nymea-remoteproxy-tests.cpp \ + mockauthenticator.cpp target.path = /usr/bin INSTALLS += target