Implement basic functionality of JSON RPC server

This commit is contained in:
Simon Stürz 2018-08-09 14:19:40 +02:00
parent f20173df9f
commit de6c757a26
42 changed files with 1044 additions and 187 deletions

View File

@ -46,12 +46,11 @@ In order to run the test, you can call `make check` in the build directory or ru
If you want to create a line coverage report from the tests simply run following command in the source directory:
$ apt install lcov
$ apt install lcov gcovr
$ ./create-coverage-html.sh
The resulting coverage report will be place in the `coverage-html` directory.
# Usage
In order to get information about the server you can start the command with the `--help` parameter.

View File

@ -1,22 +1,18 @@
#include "authenticationreply.h"
#include "authentication/authenticator.h"
AuthenticationReply::AuthenticationReply(const QUuid clientId, const QString &token, QObject *parent) :
namespace remoteproxy {
AuthenticationReply::AuthenticationReply(ProxyClient *proxyClient, QObject *parent) :
QObject(parent),
m_clientId(clientId),
m_token(token)
m_proxyClient(proxyClient)
{
}
QUuid AuthenticationReply::clientId() const
ProxyClient *AuthenticationReply::proxyClient() const
{
return m_clientId;
}
QString AuthenticationReply::token() const
{
return m_token;
return m_proxyClient;
}
bool AuthenticationReply::isTimedOut() const
@ -29,18 +25,32 @@ bool AuthenticationReply::isFinished() const
return m_finished;
}
Authenticator::AuthenticationError AuthenticationReply::error() const
{
return m_error;
}
void AuthenticationReply::setError(Authenticator::AuthenticationError error)
{
m_error = error;
}
void AuthenticationReply::setFinished()
{
emit finished();
}
void AuthenticationReply::onTimeout()
{
m_timedOut = true;
m_error = Authenticator::AuthenticationErrorTimeout;
emit finished();
}
void AuthenticationReply::abort()
{
m_error = Authenticator::AuthenticationErrorAborted;
emit finished();
}
}

View File

@ -8,16 +8,15 @@
#include "authenticator.h"
namespace remoteproxy {
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;
ProxyClient *proxyClient() const;
bool isTimedOut() const;
bool isFinished() const;
@ -25,15 +24,18 @@ public:
Authenticator::AuthenticationError error() const;
private:
QUuid m_clientId;
QString m_token;
explicit AuthenticationReply(ProxyClient *proxyClient, QObject *parent = nullptr);
ProxyClient *m_proxyClient = nullptr;
QTimer m_timer;
bool m_timedOut = false;
bool m_finished = false;
Authenticator::AuthenticationError m_error;
Authenticator::AuthenticationError m_error = Authenticator::AuthenticationErrorUnknown;
void setError(Authenticator::AuthenticationError error);
void setFinished();
signals:
void finished();
@ -46,4 +48,6 @@ public slots:
};
}
#endif // AUTHENTICATIONREPLY_H

View File

@ -1,13 +1,33 @@
#include "proxyclient.h"
#include "authenticator.h"
#include "authenticationreply.h"
namespace remoteproxy {
Authenticator::Authenticator(QObject *parent) :
QObject(parent)
{
}
void Authenticator::setReplyError(AuthenticationReply *reply, Authenticator::AuthenticationError error)
{
reply->setError(error);
}
void Authenticator::setReplyFinished(AuthenticationReply *reply)
{
reply->setFinished();
}
AuthenticationReply *Authenticator::createAuthenticationReply(ProxyClient *proxyClient, QObject *parent)
{
return new AuthenticationReply(proxyClient, parent);
}
Authenticator::~Authenticator()
{
}
}

View File

@ -4,6 +4,9 @@
#include <QUuid>
#include <QObject>
namespace remoteproxy {
class ProxyClient;
class AuthenticationReply;
class Authenticator : public QObject
@ -12,6 +15,7 @@ class Authenticator : public QObject
public:
enum AuthenticationError {
AuthenticationErrorNoError,
AuthenticationErrorUnknown,
AuthenticationErrorTimeout,
AuthenticationErrorAborted,
AuthenticationErrorAuthenticationFailed,
@ -22,8 +26,18 @@ public:
explicit Authenticator(QObject *parent = nullptr);
virtual ~Authenticator() = 0;
virtual QString name() const = 0;
public slots:
virtual AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) = 0;
virtual AuthenticationReply *authenticate(ProxyClient *proxyClient) = 0;
protected:
void setReplyError(AuthenticationReply *reply, AuthenticationError error);
void setReplyFinished(AuthenticationReply *reply);
AuthenticationReply *createAuthenticationReply(ProxyClient *proxyClient, QObject *parent = nullptr);
};
}
#endif // AUTHENTICATOR_H

View File

@ -1,17 +1,25 @@
#include "proxyclient.h"
#include "awsauthenticator.h"
#include "loggingcategories.h"
namespace remoteproxy {
AwsAuthenticator::AwsAuthenticator(QObject *parent) :
Authenticator(parent)
{
}
AuthenticationReply *AwsAuthenticator::authenticate(const QUuid &clientId, const QString &token)
QString AwsAuthenticator::name() const
{
qCDebug(dcAuthenticator()) << "Start authenticating" << clientId << "using token" << token;
AuthenticationReply *reply = new AuthenticationReply(clientId, token, this);
return "AWS authenticator";
}
AuthenticationReply *AwsAuthenticator::authenticate(ProxyClient *proxyClient)
{
qCDebug(dcAuthenticator()) << name() << "Start authenticating" << proxyClient << "using token" << proxyClient->token();
AuthenticationReply *reply = createAuthenticationReply(proxyClient, this);
return reply;
}
}

View File

@ -6,6 +6,8 @@
#include "authenticator.h"
#include "authenticationreply.h"
namespace remoteproxy {
class AwsAuthenticator : public Authenticator
{
Q_OBJECT
@ -13,9 +15,13 @@ public:
explicit AwsAuthenticator(QObject *parent = nullptr);
~AwsAuthenticator() override = default;
QString name() const override;
public slots:
AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) override;
AuthenticationReply *authenticate(ProxyClient *proxyClient) override;
};
}
#endif // AWSAUTHENTICATOR_H

View File

@ -1,6 +1,8 @@
#include "engine.h"
#include "loggingcategories.h"
namespace remoteproxy {
Engine *Engine::s_instance = nullptr;
Engine *Engine::instance()
@ -73,12 +75,12 @@ void Engine::stop()
if (m_proxyServer) {
m_proxyServer->stopServer();
m_proxyServer->deleteLater();
delete m_proxyServer;
m_proxyServer = nullptr;
}
if (m_webSocketServer) {
m_webSocketServer->deleteLater();
delete m_webSocketServer;
m_webSocketServer = nullptr;
}
@ -114,7 +116,7 @@ void Engine::setWebSocketServerPort(const quint16 &port)
void Engine::setSslConfiguration(const QSslConfiguration &configuration)
{
qCDebug(dcEngine()) << "SSL Configuration:";
qCDebug(dcEngine()) << "SSL certificate information:";
qCDebug(dcEngine()) << " Common name:" << configuration.localCertificate().issuerInfo(QSslCertificate::CommonName);
qCDebug(dcEngine()) << " Organisation:" << configuration.localCertificate().issuerInfo(QSslCertificate::Organization);
qCDebug(dcEngine()) << " Organisation unit name:" << configuration.localCertificate().issuerInfo(QSslCertificate::OrganizationalUnitName);
@ -134,18 +136,18 @@ void Engine::setAuthenticationServerUrl(const QUrl &url)
void Engine::setAuthenticator(Authenticator *authenticator)
{
if (m_authenticator == authenticator)
return;
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;
// FIXME: connect
}
Authenticator *Engine::authenticator() const
@ -183,3 +185,5 @@ void Engine::setRunning(bool running)
m_running = running;
emit runningChanged(m_running);
}
}

View File

@ -10,6 +10,8 @@
#include "websocketserver.h"
#include "authentication/authenticator.h"
namespace remoteproxy {
class Engine : public QObject
{
Q_OBJECT
@ -62,4 +64,6 @@ signals:
};
}
#endif // ENGINE_H

View File

@ -4,6 +4,8 @@
#include "engine.h"
namespace remoteproxy {
AuthenticationHandler::AuthenticationHandler(QObject *parent) :
JsonHandler(parent)
{
@ -11,31 +13,34 @@ AuthenticationHandler::AuthenticationHandler(QObject *parent) :
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("uuid", JsonTypes::basicTypeToString(JsonTypes::String));
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 &params, const QUuid &clientId)
JsonReply *AuthenticationHandler::Authenticate(const QVariantMap &params, ProxyClient *proxyClient)
{
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);
QString uuid = params.value("uuid").toString();
QString name = params.value("name").toString();
QString token = params.value("token").toString();
qCDebug(dcJsonRpc()) << "Authenticate:" << name << uuid << token;
JsonReply *jsonReply = createAsyncReply("Authenticate");
AuthenticationReply *authReply = Engine::instance()->authenticator()->authenticate(clientId, clientToken);
// Set the token for this proxy client
proxyClient->setUuid(uuid);
proxyClient->setName(name);
proxyClient->setToken(token);
AuthenticationReply *authReply = Engine::instance()->authenticator()->authenticate(proxyClient);
connect(authReply, &AuthenticationReply::finished, this, &AuthenticationHandler::onAuthenticationFinished);
m_runningAuthentications.insert(authReply, jsonReply);
@ -45,8 +50,22 @@ JsonReply *AuthenticationHandler::Authenticate(const QVariantMap &params, const
void AuthenticationHandler::onAuthenticationFinished()
{
//AuthenticationReply *authReply = static_cast<AuthenticationReply *>(sender());
//JsonReply *jsonReply = m_runningAuthentications.take(authReply);
AuthenticationReply *authenticationReply = static_cast<AuthenticationReply *>(sender());
JsonReply *jsonReply = m_runningAuthentications.take(authenticationReply);
authenticationReply->deleteLater();
qCDebug(dcJsonRpc()) << "Authentication respons ready for" << authenticationReply->proxyClient() << authenticationReply->error();
if (authenticationReply->error() != Authenticator::AuthenticationErrorNoError) {
qCWarning(dcJsonRpc()) << "Authentication error occured" << authenticationReply->error();
jsonReply->setSuccess(true);
} else {
jsonReply->setSuccess(false);
}
//emit asyncReply()
jsonReply->setData(errorToReply(authenticationReply->error()));
jsonReply->finished();
}
}

View File

@ -6,6 +6,8 @@
#include "jsonhandler.h"
#include "authentication/authenticationreply.h"
namespace remoteproxy {
class AuthenticationHandler : public JsonHandler
{
Q_OBJECT
@ -15,7 +17,7 @@ public:
QString name() const override;
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params, const QUuid &clientId);
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params, ProxyClient *proxyClient);
private:
QHash<AuthenticationReply *, JsonReply *> m_runningAuthentications;
@ -26,4 +28,6 @@ private slots:
};
}
#endif // AUTHENTICATIONHANDLER_H

View File

@ -7,6 +7,8 @@
#include "jsontypes.h"
#include "loggingcategories.h"
namespace remoteproxy {
JsonHandler::JsonHandler(QObject *parent):
QObject(parent)
{
@ -106,6 +108,13 @@ void JsonHandler::setReturns(const QString &methodName, const QVariantMap &retur
qCWarning(dcJsonRpc()) << "Cannot set returns. No such method:" << methodName;
}
QVariantMap JsonHandler::errorToReply(Authenticator::AuthenticationError error) const
{
QVariantMap returns;
returns.insert("authenticationError", JsonTypes::authenticationErrorToString(error));
return returns;
}
JsonReply *JsonHandler::createReply(const QVariantMap &data) const
{
return JsonReply::createReply(const_cast<JsonHandler*>(this), data);
@ -115,3 +124,5 @@ JsonReply *JsonHandler::createAsyncReply(const QString &method) const
{
return JsonReply::createAsyncReply(const_cast<JsonHandler*>(this), method);
}
}

View File

@ -7,6 +7,10 @@
#include <QVariantMap>
#include <QMetaMethod>
#include "authentication/authenticator.h"
namespace remoteproxy {
class JsonReply;
class JsonHandler : public QObject
@ -36,8 +40,14 @@ protected:
void setParams(const QString &methodName, const QVariantMap &params);
void setReturns(const QString &methodName, const QVariantMap &returns);
QVariantMap errorToReply(Authenticator::AuthenticationError error) const;
JsonReply *createReply(const QVariantMap &data) const;
JsonReply *createAsyncReply(const QString &method) const;
};
}
#endif // JSONHANDLER_H

View File

@ -1,5 +1,7 @@
#include "jsonreply.h"
namespace remoteproxy {
JsonReply *JsonReply::createReply(JsonHandler *handler, const QVariantMap &data)
{
return new JsonReply(TypeSync, handler, QString(), data);
@ -35,14 +37,14 @@ QString JsonReply::method() const
return m_method;
}
QUuid JsonReply::connectionId() const
QUuid JsonReply::clientId() const
{
return m_connectionId;
return m_clientId;
}
void JsonReply::setClientId(const QUuid &connectionId)
void JsonReply::setClientId(const QUuid &clientId)
{
m_connectionId = connectionId;
m_clientId = clientId;
}
int JsonReply::commandId() const
@ -55,6 +57,16 @@ void JsonReply::setCommandId(int commandId)
m_commandId = commandId;
}
bool JsonReply::success() const
{
return m_success;
}
void JsonReply::setSuccess(bool success)
{
m_success = success;
}
bool JsonReply::timedOut() const
{
return m_timedOut;
@ -71,12 +83,16 @@ void JsonReply::timeout()
emit finished();
}
JsonReply::JsonReply(JsonReply::Type type, JsonHandler *handler, const QString &method, const QVariantMap &data):
JsonReply::JsonReply(JsonReply::Type type, JsonHandler *handler, const QString &method, const QVariantMap &data, bool success):
m_type(type),
m_data(data),
m_handler(handler),
m_method(method),
m_timedOut(false)
m_success(success)
{
connect(&m_timeout, &QTimer::timeout, this, &JsonReply::timeout);
}
}

View File

@ -7,6 +7,8 @@
#include "jsonhandler.h"
namespace remoteproxy {
class JsonReply: public QObject
{
Q_OBJECT
@ -16,22 +18,29 @@ public:
TypeAsync
};
friend class JsonRpcServer;
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);
QUuid clientId() const;
void setClientId(const QUuid &clientId);
int commandId() const;
void setCommandId(int commandId);
bool success() const;
void setSuccess(bool success);
bool timedOut() const;
public slots:
@ -44,17 +53,22 @@ private slots:
void timeout();
private:
JsonReply(Type type, JsonHandler *handler, const QString &method, const QVariantMap &data = QVariantMap());
Type m_type;
JsonReply(Type type, JsonHandler *handler, const QString &method, const QVariantMap &data = QVariantMap(), bool success = true);
Type m_type = TypeSync;
QVariantMap m_data;
JsonHandler *m_handler;
JsonHandler *m_handler = nullptr;
QString m_method;
QUuid m_connectionId;
QUuid m_clientId;
int m_commandId;
bool m_timedOut;
bool m_timedOut = false;
bool m_success = false;
QTimer m_timeout;
};
}
#endif // JSONRPCREPLY_H

View File

@ -5,6 +5,8 @@
#include "loggingcategories.h"
namespace remoteproxy {
bool JsonTypes::s_initialized = false;
QString JsonTypes::s_lastError;
@ -198,6 +200,9 @@ QPair<bool, QString> JsonTypes::validateList(const QVariantList &templateList, c
QPair<bool, QString> JsonTypes::validateBasicType(const QVariant &variant)
{
if (variant.canConvert(QVariant::Uuid) && QVariant(variant).convert(QVariant::Uuid)) {
if (QUuid(variant.toString()).isNull()) {
return report(false, "Invalid uuid format.");
}
return report(true, "");
}
if (variant.canConvert(QVariant::String) && QVariant(variant).convert(QVariant::String)) {
@ -259,3 +264,5 @@ QVariantList JsonTypes::enumToStrings(const QMetaObject &metaObject, const QStri
}
return enumStrings;
}
}

View File

@ -8,6 +8,8 @@
#include "authentication/authenticator.h"
namespace remoteproxy {
#define DECLARE_OBJECT(typeName, jsonName) \
public: \
static QString typeName##Ref() { return QStringLiteral("$ref:") + QStringLiteral(jsonName); if (!s_initialized) { init(); } } \
@ -87,4 +89,6 @@ private:
static QVariantList enumToStrings(const QMetaObject &metaObject, const QString &enumName);
};
}
#endif // JSONTYPES_H

View File

@ -4,8 +4,11 @@
#include "jsonrpc/jsontypes.h"
#include <QJsonDocument>
#include <QJsonParseError>
#include <QCoreApplication>
namespace remoteproxy {
JsonRpcServer::JsonRpcServer(QObject *parent) :
JsonHandler(parent)
{
@ -13,7 +16,8 @@ JsonRpcServer::JsonRpcServer(QObject *parent) :
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.");
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));
@ -35,6 +39,9 @@ JsonRpcServer::JsonRpcServer(QObject *parent) :
JsonRpcServer::~JsonRpcServer()
{
qCDebug(dcJsonRpc()) << "Shutting down Json RPC server";
foreach (JsonHandler *handler, m_handlers.values()) {
unregisterHandler(handler);
}
}
QString JsonRpcServer::name() const
@ -42,33 +49,10 @@ QString JsonRpcServer::name() const
return "RemoteProxy";
}
QHash<QString, JsonHandler *> 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 &params) const
JsonReply *JsonRpcServer::Hello(const QVariantMap &params, ProxyClient *proxyClient) const
{
Q_UNUSED(params)
Q_UNUSED(proxyClient)
QVariantMap data;
data.insert("server", QCoreApplication::applicationName());
@ -79,9 +63,12 @@ JsonReply *JsonRpcServer::Hello(const QVariantMap &params) const
return createReply(data);
}
JsonReply *JsonRpcServer::Introspect(const QVariantMap &params) const
JsonReply *JsonRpcServer::Introspect(const QVariantMap &params, ProxyClient *proxyClient) const
{
Q_UNUSED(params)
Q_UNUSED(proxyClient)
qCDebug(dcJsonRpc()) << "Introspect called.";
QVariantMap data;
data.insert("types", JsonTypes::allTypes());
@ -103,7 +90,7 @@ JsonReply *JsonRpcServer::Introspect(const QVariantMap &params) const
return createReply(data);
}
void JsonRpcServer::sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params)
void JsonRpcServer::sendResponse(ProxyClient *client, int commandId, const QVariantMap &params)
{
QVariantMap response;
response.insert("id", commandId);
@ -112,10 +99,10 @@ void JsonRpcServer::sendResponse(TransportInterface *interface, const QUuid &cli
QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
interface->sendData(clientId, data);
client->interface()->sendData(client->clientId(), data);
}
void JsonRpcServer::sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error)
void JsonRpcServer::sendErrorResponse(ProxyClient *client, int commandId, const QString &error)
{
QVariantMap errorResponse;
errorResponse.insert("id", commandId);
@ -124,7 +111,7 @@ void JsonRpcServer::sendErrorResponse(TransportInterface *interface, const QUuid
QByteArray data = QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
interface->sendData(clientId, data);
client->interface()->sendData(client->clientId(), data);
}
QString JsonRpcServer::formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const
@ -149,25 +136,149 @@ void JsonRpcServer::registerHandler(JsonHandler *handler)
}
}
void JsonRpcServer::unregisterHandler(JsonHandler *handler)
{
m_handlers.remove(handler->name());
qCDebug(dcJsonRpc()) << "Unregister 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));
}
void JsonRpcServer::clientConnected(const QUuid &clientId)
void JsonRpcServer::asyncReplyFinished()
{
Q_UNUSED(clientId)
JsonReply *reply = static_cast<JsonReply *>(sender());
ProxyClient *proxyClient = m_asyncReplies.take(reply);
reply->deleteLater();
if (!proxyClient) {
qCWarning(dcJsonRpc()) << "Got an async reply but the client does not exist any more";
return;
}
if (!reply->timedOut()) {
Q_ASSERT_X(reply->handler()->validateReturns(reply->method(), reply->data()).first
,"validating return value", formatAssertion(reply->handler()->name(), reply->method(), reply->handler(), reply->data()).toLatin1().data());
sendResponse(proxyClient, reply->commandId(), reply->data());
if (!reply->success()) {
// Disconnect this client since the request was not successfully
proxyClient->interface()->killClientConnection(proxyClient->clientId());
}
} else {
sendErrorResponse(proxyClient, reply->commandId(), "Command timed out");
// Disconnect this client since he requested something that created a timeout
proxyClient->interface()->killClientConnection(proxyClient->clientId());
}
}
void JsonRpcServer::clientDisconnected(const QUuid &clientId)
void JsonRpcServer::registerClient(ProxyClient *proxyClient)
{
Q_UNUSED(clientId)
qCDebug(dcJsonRpc()) << "Register client" << proxyClient;
if (m_clients.contains(proxyClient)) {
qCWarning(dcJsonRpc()) << "Client already registered" << proxyClient;
return;
}
m_clients.append(proxyClient);
}
void JsonRpcServer::processData(const QUuid &clientId, const QByteArray &data)
void JsonRpcServer::unregisterClient(ProxyClient *proxyClient)
{
Q_UNUSED(clientId)
Q_UNUSED(data)
qCDebug(dcJsonRpc()) << "Unregister client" << proxyClient;
if (!m_clients.contains(proxyClient)) {
qCWarning(dcJsonRpc()) << "Client was not registered" << proxyClient;
return;
}
m_clients.removeAll(proxyClient);
}
void JsonRpcServer::processData(ProxyClient *proxyClient, const QByteArray &data)
{
if (!m_clients.contains(proxyClient))
return;
qCDebug(dcJsonRpcTraffic()) << "Incoming data from" << proxyClient << ": " << data;
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if(error.error != QJsonParseError::NoError) {
qCWarning(dcJsonRpc) << "Failed to parse JSON data" << data << ":" << error.errorString();
sendErrorResponse(proxyClient, -1, QString("Failed to parse JSON data: %1").arg(error.errorString()));
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
QVariantMap message = jsonDoc.toVariant().toMap();
bool success = false;
int commandId = message.value("id").toInt(&success);
if (!success) {
qCWarning(dcJsonRpc()) << "Error parsing command. Missing \"id\":" << message;
sendErrorResponse(proxyClient, -1, "Error parsing command. Missing 'id'");
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
QStringList commandList = message.value("method").toString().split('.');
if (commandList.count() != 2) {
qCWarning(dcJsonRpc) << "Error parsing method.\nGot:" << message.value("method").toString() << "\nExpected: \"Namespace.method\"";
sendErrorResponse(proxyClient, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString()));
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
QString targetNamespace = commandList.first();
QString method = commandList.last();
JsonHandler *handler = m_handlers.value(targetNamespace);
if (!handler) {
sendErrorResponse(proxyClient, commandId, "No such namespace");
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
if (!handler->hasMethod(method)) {
sendErrorResponse(proxyClient, commandId, "No such namespace");
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
QVariantMap params = message.value("params").toMap();
QPair<bool, QString> validationResult = handler->validateParams(method, params);
if (!validationResult.first) {
sendErrorResponse(proxyClient, commandId, "Invalid params: " + validationResult.second);
proxyClient->interface()->killClientConnection(proxyClient->clientId());
return;
}
JsonReply *reply;
QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params), Q_ARG(ProxyClient *, proxyClient));
if (reply->type() == JsonReply::TypeAsync) {
m_asyncReplies.insert(reply, proxyClient);
reply->setClientId(proxyClient->clientId());
reply->setCommandId(commandId);
connect(reply, &JsonReply::finished, this, &JsonRpcServer::asyncReplyFinished);
reply->startWait();
} else {
Q_ASSERT_X((targetNamespace == "RemoteProxy" && method == "Introspect") || handler->validateReturns(method, reply->data()).first
,"validating return value", formatAssertion(targetNamespace, method, handler, reply->data()).toLatin1().data());
reply->setClientId(proxyClient->clientId());
reply->setCommandId(commandId);
sendResponse(proxyClient, commandId, reply->data());
}
}
}

View File

@ -2,11 +2,16 @@
#define JSONRPCSERVER_H
#include <QObject>
#include <QVariant>
#include "proxyclient.h"
#include "jsonrpc/jsonreply.h"
#include "transportinterface.h"
#include "jsonrpc/jsonhandler.h"
#include "jsonrpc/authenticationhandler.h"
namespace remoteproxy {
class JsonRpcServer : public JsonHandler
{
Q_OBJECT
@ -16,39 +21,40 @@ public:
QString name() const override;
QHash<QString, JsonHandler *> handlers() const;
Q_INVOKABLE JsonReply *Hello(const QVariantMap &params, ProxyClient *proxyClient = nullptr) const;
Q_INVOKABLE JsonReply *Introspect(const QVariantMap &params, ProxyClient *proxyClient = nullptr) const;
void registerTransportInterface(TransportInterface *interface);
void unregisterTransportInterface(TransportInterface *interface);
Q_INVOKABLE JsonReply *Hello(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *Introspect(const QVariantMap &params) const;
signals:
void TunnelEstablished(const QVariantMap &params, ProxyClient *proxyClient = nullptr);
private:
QList<TransportInterface *> m_interfaces;
QHash<QString, JsonHandler *> m_handlers;
QHash<JsonReply *, TransportInterface *> m_asyncReplies;
QHash<QUuid, TransportInterface*> m_clientTransports;
QHash<QString, JsonReply*> m_pairingRequests;
QHash<JsonReply *, ProxyClient *> m_asyncReplies;
QList<ProxyClient *> m_clients;
int m_notificationId;
void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params = QVariantMap());
void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
void sendResponse(ProxyClient *client, int commandId, const QVariantMap &params = QVariantMap());
void sendErrorResponse(ProxyClient *client, int commandId, const QString &error);
QString formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const;
void registerHandler(JsonHandler *handler);
void unregisterHandler(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 &params);
// void asyncReplyFinished();
void asyncReplyFinished();
public slots:
// Client registration for JSON RPC traffic
void registerClient(ProxyClient *proxyClient);
void unregisterClient(ProxyClient *proxyClient);
// Process data from client
void processData(ProxyClient *proxyClient, const QByteArray &data);
};
}
#endif // JSONRPCSERVER_H

View File

@ -1,7 +1,10 @@
#include "proxyclient.h"
ProxyClient::ProxyClient(const QUuid &clientId, QObject *parent) :
namespace remoteproxy {
ProxyClient::ProxyClient(TransportInterface *interface, const QUuid &clientId, QObject *parent) :
QObject(parent),
m_interface(interface),
m_clientId(clientId)
{
@ -17,7 +20,77 @@ bool ProxyClient::authenticated() const
return m_authenticated;
}
void ProxyClient::setAuthenticated(bool authenticated)
{
if (m_authenticated == authenticated)
return;
m_authenticated = authenticated;
emit authenticatedChanged(m_authenticated);
}
bool ProxyClient::tunnelConnected() const
{
return m_tunnelConnected;
}
void ProxyClient::setTunnelConnected(bool tunnelConnected)
{
if (m_tunnelConnected == tunnelConnected)
return;
m_tunnelConnected = tunnelConnected;
emit tunnelConnectedChanged(m_tunnelConnected);
}
TransportInterface *ProxyClient::interface() const
{
return m_interface;
}
QString ProxyClient::uuid() const
{
return m_uuid;
}
void ProxyClient::setUuid(const QString &uuid)
{
m_uuid = uuid;
}
QString ProxyClient::name() const
{
return m_name;
}
void ProxyClient::setName(const QString &name)
{
m_name = name;
}
QString ProxyClient::token() const
{
return m_token;
}
void ProxyClient::setToken(const QString &token)
{
m_token = token;
}
QDebug operator<<(QDebug debug, ProxyClient *proxyClient)
{
debug.nospace() << "ProxyClient(" << proxyClient->interface()->serverName();
debug.nospace() << ", " << proxyClient->clientId().toString() << ") :" << endl;
debug.nospace() << " tunnel: " << proxyClient->tunnelConnected() << endl;
debug.nospace() << " authenticated: " << proxyClient->authenticated() << endl;
if (!proxyClient->name().isEmpty() && !proxyClient->token().isEmpty() && !proxyClient->uuid().isEmpty()) {
debug.nospace() << " name: " << proxyClient->name() << endl;
debug.nospace() << " uuid: " << proxyClient->uuid() << endl;
debug.nospace() << " token: " << proxyClient->token() << endl;
}
return debug;
}
}

View File

@ -2,25 +2,58 @@
#define PROXYCLIENT_H
#include <QUuid>
#include <QDebug>
#include <QObject>
#include "transportinterface.h"
namespace remoteproxy {
class ProxyClient : public QObject
{
Q_OBJECT
public:
explicit ProxyClient(const QUuid &clientId, QObject *parent = nullptr);
explicit ProxyClient(TransportInterface *interface, const QUuid &clientId, QObject *parent = nullptr);
QUuid clientId() const;
bool authenticated() const;
void setAuthenticated(bool authenticated);
bool tunnelConnected() const;
void setTunnelConnected(bool tunnelConnected);
TransportInterface *interface() const;
// Properties from auth request
QString uuid() const;
void setUuid(const QString &uuid);
QString name() const;
void setName(const QString &name);
QString token() const;
void setToken(const QString &token);
private:
TransportInterface *m_interface = nullptr;
QUuid m_clientId;
bool m_authenticated = false;
bool m_tunnelConnected = false;
QString m_uuid;
QString m_name;
QString m_token;
signals:
void authenticatedChanged(bool authenticated);
void tunnelConnectedChanged(bool tunnelConnected);
};
QDebug operator<< (QDebug debug, ProxyClient *proxyClient);
}
#endif // PROXYCLIENT_H

View File

@ -1,6 +1,10 @@
#include "proxyconfiguration.h"
namespace remoteproxy {
ProxyConfiguration::ProxyConfiguration(QObject *parent) : QObject(parent)
{
}
}

View File

@ -3,6 +3,8 @@
#include <QObject>
namespace remoteproxy {
class ProxyConfiguration : public QObject
{
Q_OBJECT
@ -15,4 +17,6 @@ public slots:
};
}
#endif // PROXYCONFIGURATION_H

View File

@ -1,11 +1,21 @@
#include "proxyserver.h"
#include "loggingcategories.h"
#include <QJsonDocument>
namespace remoteproxy {
ProxyServer::ProxyServer(QObject *parent) : QObject(parent)
{
m_jsonRpcServer = new JsonRpcServer(this);
}
ProxyServer::~ProxyServer()
{
qCDebug(dcProxyServer()) << "Shutting down proxy server";
}
void ProxyServer::registerTransportInterface(TransportInterface *interface)
{
qCDebug(dcProxyServer()) << "Register transport interface" << interface->serverName();
@ -22,12 +32,23 @@ void ProxyServer::registerTransportInterface(TransportInterface *interface)
m_transportInterfaces.append(interface);
}
void ProxyServer::sendResponse(TransportInterface *interface, const QUuid &clientId, const QVariantMap &response)
{
QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
interface->sendData(clientId, data);
}
void ProxyServer::onClientConnected(const QUuid &clientId)
{
TransportInterface *interface = static_cast<TransportInterface *>(sender());
qCDebug(dcProxyServer()) << "New client connected" << interface->serverName() << clientId.toString();
m_unauthenticatedClients.append(clientId);
ProxyClient *proxyClient = new ProxyClient(interface, clientId, this);
m_proxyClients.insert(clientId, proxyClient);
m_jsonRpcServer->registerClient(proxyClient);
}
void ProxyServer::onClientDisconnected(const QUuid &clientId)
@ -35,14 +56,33 @@ void ProxyServer::onClientDisconnected(const QUuid &clientId)
TransportInterface *interface = static_cast<TransportInterface *>(sender());
qCDebug(dcProxyServer()) << "Client disconnected" << interface->serverName() << clientId.toString();
// Clean up this client since it does not exist any more
m_unauthenticatedClients.removeAll(clientId);
if (m_proxyClients.contains(clientId)) {
ProxyClient *proxyClient = m_proxyClients.take(clientId);
m_jsonRpcServer->unregisterClient(proxyClient);
proxyClient->deleteLater();
// Check if client is in tunnel and clean up tunnel
// Disconnect also the other tunnel client
}
// TODO: Clean up this client since it does not exist any more
}
void ProxyServer::onClientDataAvailable(const QUuid &clientId, const QByteArray &data)
{
TransportInterface *interface = static_cast<TransportInterface *>(sender());
qCDebug(dcProxyServer()) << "Client data available" << interface->serverName() << clientId.toString() << qUtf8Printable(data);
ProxyClient *proxyClient = m_proxyClients.value(clientId);
if (!proxyClient) {
qCWarning(dcProxyServer()) << "Could not find client for uuid" << clientId;
return;
}
qCDebug(dcProxyServer()) << "Client data available" << proxyClient << qUtf8Printable(data);
if (proxyClient->tunnelConnected()) {
// Pipe the data
} else {
// Pipe data into json rpc server
m_jsonRpcServer->processData(proxyClient, data);
}
}
void ProxyServer::startServer()
@ -60,3 +100,5 @@ void ProxyServer::stopServer()
interface->stopServer();
}
}
}

View File

@ -2,22 +2,32 @@
#define PROXYSERVER_H
#include <QUuid>
#include <QHash>
#include <QObject>
#include "proxyclient.h"
#include "jsonrpcserver.h"
#include "transportinterface.h"
namespace remoteproxy {
class ProxyServer : public QObject
{
Q_OBJECT
public:
explicit ProxyServer(QObject *parent = nullptr);
~ProxyServer();
void registerTransportInterface(TransportInterface *interface);
private:
JsonRpcServer *m_jsonRpcServer = nullptr;
QList<TransportInterface *> m_transportInterfaces;
QList<QUuid> m_unauthenticatedClients;
QHash<QUuid, ProxyClient *> m_proxyClients;
QHash<ProxyClient *, ProxyClient *> m_tunnels;
void sendResponse(TransportInterface *interface, const QUuid &clientId, const QVariantMap &response = QVariantMap());
private slots:
void onClientConnected(const QUuid &clientId);
@ -30,4 +40,6 @@ public slots:
};
}
#endif // PROXYSERVER_H

View File

@ -1,5 +1,7 @@
#include "transportinterface.h"
namespace remoteproxy {
TransportInterface::TransportInterface(QObject *parent) :
QObject(parent)
{
@ -15,3 +17,5 @@ TransportInterface::~TransportInterface()
{
}
}

View File

@ -3,6 +3,8 @@
#include <QObject>
namespace remoteproxy {
class TransportInterface : public QObject
{
Q_OBJECT
@ -15,6 +17,8 @@ public:
virtual void sendData(const QUuid &clientId, const QByteArray &data) = 0;
virtual void sendData(const QList<QUuid> &clients, const QByteArray &data) = 0;
virtual void killClientConnection(const QUuid &clientId) = 0;
signals:
void clientConnected(const QUuid &clientId);
void clientDisconnected(const QUuid &clientId);
@ -29,4 +33,6 @@ public slots:
};
}
#endif // TRANSPORTINTERFACE_H

View File

@ -3,6 +3,8 @@
#include <QCoreApplication>
namespace remoteproxy {
WebSocketServer::WebSocketServer(const QSslConfiguration &sslConfiguration, QObject *parent) :
TransportInterface(parent),
m_sslConfiguration(sslConfiguration)
@ -54,6 +56,16 @@ void WebSocketServer::sendData(const QList<QUuid> &clients, const QByteArray &da
}
}
void WebSocketServer::killClientConnection(const QUuid &clientId)
{
QWebSocket *client = m_clientList.value(clientId);
if (!client)
return;
qCWarning(dcWebSocketServer()) << "Kill client connection" << clientId.toString();
client->close(QWebSocketProtocol::CloseCodeBadOperation);
}
void WebSocketServer::onClientConnected()
{
// Got a new client connected
@ -69,7 +81,7 @@ void WebSocketServer::onClientConnected()
// Create new uuid for this connection
QUuid clientId = QUuid::createUuid();
qCDebug(dcWebSocketServer()) << "New client connected:" << client->peerAddress().toString() << clientId;
qCDebug(dcWebSocketServer()) << "New client connected:" << client << client->peerAddress().toString() << clientId.toString();
// Append the new client to the client list
m_clientList.insert(clientId, client);
@ -88,7 +100,7 @@ void WebSocketServer::onClientDisconnected()
QWebSocket *client = static_cast<QWebSocket *>(sender());
QUuid clientId = m_clientList.key(client);
qCDebug(dcWebSocketServer()) << "Client disconnected:" << client->peerAddress().toString() << clientId;
qCDebug(dcWebSocketServer()) << "Client disconnected:" << client << client->peerAddress().toString() << clientId.toString();
m_clientList.take(clientId)->deleteLater();
emit clientDisconnected(clientId);
@ -159,3 +171,5 @@ bool WebSocketServer::stopServer()
return true;
}
}

View File

@ -10,6 +10,8 @@
#include "transportinterface.h"
namespace remoteproxy {
class WebSocketServer : public TransportInterface
{
Q_OBJECT
@ -27,6 +29,8 @@ public:
void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clients, const QByteArray &data) override;
void killClientConnection(const QUuid &clientId) override;
private:
QUrl m_serverUrl;
QWebSocketServer *m_server = nullptr;
@ -50,4 +54,6 @@ public slots:
};
}
#endif // WEBSOCKETSERVER_H

View File

@ -0,0 +1,6 @@
#include "jsonrpcclient.h"
JsonRpcClient::JsonRpcClient(QObject *parent) : QObject(parent)
{
}

View File

@ -0,0 +1,17 @@
#ifndef JSONRPCCLIENT_H
#define JSONRPCCLIENT_H
#include <QObject>
class JsonRpcClient : public QObject
{
Q_OBJECT
public:
explicit JsonRpcClient(QObject *parent = nullptr);
signals:
public slots:
};
#endif // JSONRPCCLIENT_H

View File

@ -5,12 +5,16 @@ TARGET = nymea-remoteproxyclient
HEADERS += \
remoteproxyconnector.h \
websocketconnector.h
websocketconnector.h \
socketconnector.h \
jsonrpcclient.h
SOURCES += \
remoteproxyconnector.cpp \
websocketconnector.cpp
websocketconnector.cpp \
socketconnector.cpp \
jsonrpcclient.cpp
# install header file with relative subdirectory

View File

@ -119,6 +119,12 @@ bool RemoteProxyConnector::sendData(const QByteArray &data)
return false;
}
if (!isConnected()) {
qCWarning(dcRemoteProxyConnector()) << "Not connected";
return false;
}
qCDebug(dcRemoteProxyConnector()) << "Sending data:" << data;
qint64 dataSendCount = m_webSocket->sendTextMessage(QString::fromUtf8(data));
if (dataSendCount != data.count()) {
qCWarning(dcRemoteProxyConnector()) << "Could not send all data to" << serverUrl().toString();

View File

@ -0,0 +1,11 @@
#include "socketconnector.h"
SocketConnector::SocketConnector(QObject *parent) : QObject(parent)
{
}
SocketConnector::~SocketConnector()
{
}

View File

@ -0,0 +1,29 @@
#ifndef SOCKETCONNECTOR_H
#define SOCKETCONNECTOR_H
#include <QObject>
#include <QHostAddress>
class SocketConnector : public QObject
{
Q_OBJECT
public:
explicit SocketConnector(QObject *parent = nullptr);
virtual ~SocketConnector() = 0;
virtual void sendData(const QByteArray &data) = 0;
virtual bool isConnected() = 0;
signals:
void connected();
void disconnected();
void dataReceived(const QByteArray &data);
public slots:
virtual void connectServer(const QHostAddress &address, quint16 port) = 0;
virtual void disconnectServer() = 0;
};
#endif // SOCKETCONNECTOR_H

View File

@ -1,6 +1,12 @@
#include "websocketconnector.h"
WebSocketConnector::WebSocketConnector(QObject *parent) : QObject(parent)
WebSocketConnector::WebSocketConnector(QObject *parent) :
SocketConnector(parent)
{
}
void WebSocketConnector::sendData(const QByteArray &data)
{
Q_UNUSED(data)
}

View File

@ -3,15 +3,19 @@
#include <QObject>
class WebSocketConnector : public QObject
#include "socketconnector.h"
class WebSocketConnector : public SocketConnector
{
Q_OBJECT
public:
explicit WebSocketConnector(QObject *parent = nullptr);
void sendData(const QByteArray &data) override;
signals:
public slots:
};
#endif // WEBSOCKETCONNECTOR_H
#endif // WEBSOCKETCONNECTOR_H

View File

@ -21,6 +21,7 @@
#include "loggingcategories.h"
#include "authentication/awsauthenticator.h"
using namespace remoteproxy;
static QHash<QString, bool> s_loggingFilters;

View File

@ -1,25 +1,59 @@
#include "proxyclient.h"
#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()
QString MockAuthenticator::name() const
{
return "Mock authenticator";
}
AuthenticationReply *MockAuthenticator::authenticate(const QUuid &clientId, const QString &token)
void MockAuthenticator::setTimeoutDuration(int timeout)
{
qCDebug(dcAuthenticator()) << "MockAuthenticator: Start authentication for" << clientId << "using token" << token;
AuthenticationReply *reply = new AuthenticationReply(clientId, token, this);
return reply;
m_timeoutDuration = timeout;
}
void MockAuthenticator::setExpectedAuthenticationError(Authenticator::AuthenticationError error)
{
m_expectedError = error;
}
void MockAuthenticator::replyFinished()
{
MockAuthenticationReply *reply = static_cast<MockAuthenticationReply *>(sender());
reply->deleteLater();
qCDebug(dcAuthenticator()) << name() << "Authentication finished.";
setReplyError(reply->authenticationReply(), reply->error());
setReplyFinished(reply->authenticationReply());
}
AuthenticationReply *MockAuthenticator::authenticate(ProxyClient *proxyClient)
{
qCDebug(dcAuthenticator()) << name() << "Start authentication for" << proxyClient << "using token" << proxyClient->token();
AuthenticationReply *authenticationReply = createAuthenticationReply(proxyClient, this);
MockAuthenticationReply *reply = new MockAuthenticationReply(m_timeoutDuration, m_expectedError, authenticationReply, this);
connect(reply, &MockAuthenticationReply::finished, this, &MockAuthenticator::replyFinished);
return authenticationReply;
}
MockAuthenticationReply::MockAuthenticationReply(int timeout, Authenticator::AuthenticationError error, AuthenticationReply *authenticationReply, QObject *parent) :
QObject(parent),
m_error(error),
m_authenticationReply(authenticationReply)
{
QTimer::singleShot(timeout, this, &MockAuthenticationReply::finished);
}

View File

@ -6,25 +6,47 @@
#include "authentication/authenticator.h"
using namespace remoteproxy;
class MockAuthenticationReply : public QObject
{
Q_OBJECT
public:
explicit MockAuthenticationReply(int timeout, Authenticator::AuthenticationError error, AuthenticationReply *authenticationReply, QObject *parent = nullptr);
AuthenticationReply *authenticationReply() const { return m_authenticationReply; }
Authenticator::AuthenticationError error() const { return m_error; }
private:
Authenticator::AuthenticationError m_error;
AuthenticationReply *m_authenticationReply;
signals:
void finished();
};
class MockAuthenticator : public Authenticator
{
Q_OBJECT
public:
explicit MockAuthenticator(QObject *parent = nullptr);
QString name() const override;
void setTimeoutDuration(int timeout);
void setExpectedAuthenticationError(Authenticator::AuthenticationError error);
void setExpectedAuthenticationError(Authenticator::AuthenticationError error = AuthenticationErrorNoError);
private:
QTimer * m_timer = nullptr;
int m_timeoutDuration = 1000;
Authenticator::AuthenticationError m_expectedError;
private slots:
void onTimeout();
void replyFinished();
public slots:
AuthenticationReply *authenticate(const QUuid &clientId, const QString &token) override;
AuthenticationReply *authenticate(ProxyClient *proxyClient) override;
};
#endif // MOCKAUTHENTICATOR_H

View File

@ -7,6 +7,7 @@
#include <QMetaType>
#include <QSignalSpy>
#include <QWebSocket>
#include <QJsonDocument>
#include <QWebSocketServer>
RemoteProxyTests::RemoteProxyTests(QObject *parent) :
@ -40,6 +41,7 @@ RemoteProxyTests::RemoteProxyTests(QObject *parent) :
void RemoteProxyTests::cleanUpEngine()
{
if (Engine::exists()) {
Engine::instance()->stop();
Engine::instance()->destroy();
QVERIFY(!Engine::exists());
}
@ -48,16 +50,22 @@ void RemoteProxyTests::cleanUpEngine()
void RemoteProxyTests::restartEngine()
{
cleanUpEngine();
QVERIFY(Engine::instance() != nullptr);
QVERIFY(Engine::exists());
startEngine();
}
void RemoteProxyTests::startEngine()
{
if (!Engine::exists()) {
Engine::instance();
QString serverName = "nymea-remoteproxy-testserver";
Engine::instance()->setAuthenticator(m_authenticator);
Engine::instance()->setAuthenticationServerUrl(QUrl("https://localhost"));
Engine::instance()->setServerName(serverName);
Engine::instance()->setWebSocketServerPort(m_port);
Engine::instance()->setWebSocketServerHostAddress(QHostAddress::LocalHost);
Engine::instance()->setSslConfiguration(m_sslConfiguration);
QVERIFY(Engine::exists());
QVERIFY(Engine::instance()->serverName() == serverName);
}
}
@ -65,16 +73,7 @@ void RemoteProxyTests::startServer()
{
startEngine();
QString serverName = "nymea-remoteproxy-testserver";
Engine::instance()->setAuthenticationServerUrl(QUrl("https://localhost"));
Engine::instance()->setServerName(serverName);
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()->serverName() == serverName);
QVERIFY(Engine::instance()->running());
QVERIFY(Engine::instance()->webSocketServer()->running());
}
@ -88,6 +87,99 @@ void RemoteProxyTests::stopServer()
QVERIFY(!Engine::instance()->running());
}
QVariant RemoteProxyTests::invokeApiCall(const QString &method, const QVariantMap params, bool remainsConnected)
{
Q_UNUSED(remainsConnected)
QVariantMap request;
request.insert("id", m_commandCounter);
request.insert("method", method);
request.insert("params", params);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(request);
QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13);
connect(socket, &QWebSocket::sslErrors, this, &RemoteProxyTests::sslErrors);
QSignalSpy spyConnection(socket, SIGNAL(connected()));
socket->open(Engine::instance()->webSocketServer()->serverUrl());
spyConnection.wait();
if (spyConnection.count() == 0) {
return QVariant();
}
// QSignalSpy disconnectedSpy(socket, SIGNAL(disconnected()));
QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString)));
socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact)));
dataSpy.wait();
// if (remainsConnected) {
// disconnectedSpy.wait(1000);
// if (socket->state() != QAbstractSocket::UnconnectedState) {
// qWarning() << "!!!!!!!!!!!!! socket still connected but should be disconnected!";
// }
// } else {
// disconnectedSpy.wait();
// if (socket->state() != QAbstractSocket::ConnectedState) {
// qWarning() << "!!!!!!!!!!!!! socket not connected but should be!";
// }
// }
socket->close();
socket->deleteLater();
for (int i = 0; i < dataSpy.count(); i++) {
// Make sure the response it a valid JSON string
QJsonParseError error;
jsonDoc = QJsonDocument::fromJson(dataSpy.at(i).last().toByteArray(), &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "JSON parser error" << error.errorString();
return QVariant();
}
QVariantMap response = jsonDoc.toVariant().toMap();
// skip notifications
if (response.contains("notification"))
continue;
if (response.value("id").toInt() == m_commandCounter) {
m_commandCounter++;
return jsonDoc.toVariant();
}
}
m_commandCounter++;
return QVariant();
}
QVariant RemoteProxyTests::injectSocketData(const QByteArray &data)
{
QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13);
connect(socket, &QWebSocket::sslErrors, this, &RemoteProxyTests::sslErrors);
QSignalSpy spyConnection(socket, SIGNAL(connected()));
socket->open(Engine::instance()->webSocketServer()->serverUrl());
spyConnection.wait();
if (spyConnection.count() == 0) {
return QVariant();
}
QSignalSpy spy(socket, SIGNAL(textMessageReceived(QString)));
socket->sendTextMessage(QString(data));
spy.wait();
socket->close();
socket->deleteLater();
for (int i = 0; i < spy.count(); i++) {
// Make sure the response it a valid JSON string
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "JSON parser error" << error.errorString();
return QVariant();
}
return jsonDoc.toVariant();
}
m_commandCounter++;
return QVariant();
}
void RemoteProxyTests::initTestCase()
{
qRegisterMetaType<RemoteProxyConnector::Error>();
@ -104,10 +196,8 @@ void RemoteProxyTests::cleanupTestCase()
void RemoteProxyTests::startStopServer()
{
startEngine();
startServer();
stopServer();
cleanUpEngine();
}
void RemoteProxyTests::webserverConnectionBlocked()
@ -134,7 +224,6 @@ void RemoteProxyTests::webserverConnectionBlocked()
// Clean up
stopServer();
cleanUpEngine();
}
void RemoteProxyTests::webserverSocketVersion()
@ -153,20 +242,11 @@ void RemoteProxyTests::webserverSocketVersion()
// Clean up
stopServer();
cleanUpEngine();
}
void RemoteProxyTests::webserverConnection()
{
// Start the server
startServer();
// Clean up
stopServer();
cleanUpEngine();
}
void RemoteProxyTests::sslConfigurations()
@ -199,25 +279,143 @@ void RemoteProxyTests::sslConfigurations()
QVERIFY(!connector->isConnected());
connector->deleteLater();
Engine::instance()->stop();
stopServer();
}
void RemoteProxyTests::getIntrospect()
{
// Start the server
startServer();
QVariant response = invokeApiCall("RemoteProxy.Introspect");
qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));
// Clean up
stopServer();
}
void RemoteProxyTests::getHello()
{
// Start the server
startServer();
QVariant response = invokeApiCall("RemoteProxy.Hello");
qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));
// Clean up
stopServer();
}
void RemoteProxyTests::apiBasicCalls_data()
{
QTest::addColumn<QByteArray>("data");
QTest::addColumn<bool>("valid");
QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << true;
QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}")<< false;
QTest::newRow("missing method") << QByteArray("{\"id\":42}") << false;
QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"RemoteProx")<< false;
QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << false;
QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << false;
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << false;
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << false;
}
void RemoteProxyTests::apiBasicCalls()
{
QFETCH(QByteArray, data);
QFETCH(bool, valid);
// Start the server
startServer();
QVariant response = injectSocketData(data);
if (valid) {
QVERIFY2(response.toMap().value("status").toString() == "success", "Call wasn't parsed correctly by nymea.");
}
// Clean up
stopServer();
}
void RemoteProxyTests::authenticate_data()
{
QTest::addColumn<QString>("uuid");
QTest::addColumn<QString>("name");
QTest::addColumn<QString>("token");
QTest::addColumn<int>("timeout");
QTest::addColumn<Authenticator::AuthenticationError>("expectedError");
QTest::newRow("success") << QUuid::createUuid().toString() << "Testclient, hello form the test!" << "Hh5JrkdFVstVVyCnfE3vVT3zG_JTacXEhPLZhCei"
<< 100 << Authenticator::AuthenticationErrorNoError;
QTest::newRow("failed") << QUuid::createUuid().toString() << "Testclient, hello form the test!" << "invalid_token_42"
<< 100 << Authenticator::AuthenticationErrorAuthenticationFailed;
QTest::newRow("not responding") << QUuid::createUuid().toString() << "Testclient, hello form the test!" << "invalid_token_42"
<< 200 << Authenticator::AuthenticationErrorAuthenticationServerNotResponding;
QTest::newRow("aborted") << QUuid::createUuid().toString() << "Testclient, hello form the test!" << "invalid_token_42"
<< 100 << Authenticator::AuthenticationErrorAborted;
QTest::newRow("unknown") << QUuid::createUuid().toString() << "Testclient, hello form the test!" << "invalid_token_42"
<< 100 << Authenticator::AuthenticationErrorUnknown;
}
void RemoteProxyTests::authenticate()
{
// // Start the server
// startServer();
QFETCH(QString, uuid);
QFETCH(QString, name);
QFETCH(QString, token);
QFETCH(int, timeout);
QFETCH(Authenticator::AuthenticationError, expectedError);
// // Connect to the server
// RemoteProxyConnector *connector = new RemoteProxyConnector(this);
// connector->setInsecureConnection(true);
// Start the server
startServer();
// QSignalSpy spy(connector, &RemoteProxyConnector::connected);
// connector->connectServer(QHostAddress::LocalHost, m_port);
// spy.wait();
// Configure result
m_authenticator->setExpectedAuthenticationError(expectedError);
m_authenticator->setTimeoutDuration(timeout);
// connector->disconnectServer();
// connector->deleteLater();
// Engine::instance()->stop();
// Create request
QVariantMap params;
params.insert("uuid", uuid);
params.insert("name", name);
params.insert("token", token);
QVariant response = invokeApiCall("Authentication.Authenticate", params);
qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));
verifyAuthenticationError(response, expectedError);
// Clean up
stopServer();
}
void RemoteProxyTests::timeout()
{
// Start the server
startServer();
// Configure result
// Start the server
startServer();
// Configure result
m_authenticator->setExpectedAuthenticationError();
m_authenticator->setTimeoutDuration(6000);
// Create request
QVariantMap params;
params.insert("uuid", "uuid");
params.insert("name", "name");
params.insert("token", "token");
QVariant response = invokeApiCall("Authentication.Authenticate", params);
qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));
// Clean up
stopServer();
}

View File

@ -5,12 +5,16 @@
#include <QtTest>
#include <QSslKey>
#include <QObject>
#include <QWebSocket>
#include <QHostAddress>
#include <QSslCertificate>
#include <QSslConfiguration>
#include "jsonrpc/jsontypes.h"
#include "mockauthenticator.h"
using namespace remoteproxy;
class RemoteProxyTests : public QObject
{
Q_OBJECT
@ -23,25 +27,71 @@ private:
QSslConfiguration m_sslConfiguration;
MockAuthenticator *m_authenticator = nullptr;
int m_commandCounter = 0;
void cleanUpEngine();
void restartEngine();
void startEngine();
void startServer();
void stopServer();
QVariant invokeApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true);
QVariant injectSocketData(const QByteArray &data);
protected slots:
void initTestCase();
void cleanupTestCase();
private slots:
// Basic stuff
void startStopServer();
// WebSocket connection
void webserverConnectionBlocked();
void webserverSocketVersion();
void webserverConnection();
void sslConfigurations();
// Api
void getIntrospect();
void getHello();
void apiBasicCalls_data();
void apiBasicCalls();
void authenticate_data();
void authenticate();
void timeout();
public slots:
void sslErrors(const QList<QSslError> &) {
QWebSocket *socket = static_cast<QWebSocket*>(sender());
socket->ignoreSslErrors();
}
inline void verifyError(const QVariant &response, const QString &fieldName, const QString &error)
{
QJsonDocument jsonDoc = QJsonDocument::fromVariant(response);
QVERIFY2(response.toMap().value("status").toString() == QString("success"),
QString("\nExpected status: \"success\"\nGot: %2\nFull message: %3")
.arg(response.toMap().value("status").toString())
.arg(jsonDoc.toJson().data())
.toLatin1().data());
QVERIFY2(response.toMap().value("params").toMap().value(fieldName).toString() == error,
QString("\nExpected: %1\nGot: %2\nFull message: %3\n")
.arg(error)
.arg(response.toMap().value("params").toMap().value(fieldName).toString())
.arg(jsonDoc.toJson().data())
.toLatin1().data());
}
inline void verifyAuthenticationError(const QVariant &response, Authenticator::AuthenticationError error = Authenticator::AuthenticationErrorNoError) {
verifyError(response, "authenticationError", JsonTypes::authenticationErrorToString(error));
}
};
#endif // NYMEA_REMOTEPROXY_TESTS_H