Rafactor Packet fragmentation

- Move Json packet fragmentation into the JsonRpcServer.
  This way we only have to do it once.

- fixes a bug in TcpServer and BluetoothServer
  where multiple clients would corrupt each others buffer

- fixes a bug in CloudTransport where it would leak client
  sockets after a remote connection disconnect
This commit is contained in:
Michael Zanetti 2018-10-18 15:16:27 +02:00
parent 47b1bdd919
commit 2d98e9d6b9
17 changed files with 226 additions and 124 deletions

View File

@ -82,6 +82,14 @@ void BluetoothServer::sendData(const QList<QUuid> &clients, const QByteArray &da
sendData(client, data); sendData(client, data);
} }
void BluetoothServer::terminateClientConnection(const QUuid &clientId)
{
QBluetoothSocket *client = m_clientList.value(clientId);
if (client) {
client->abort();
}
}
void BluetoothServer::onHostModeChanged(const QBluetoothLocalDevice::HostMode &mode) void BluetoothServer::onHostModeChanged(const QBluetoothLocalDevice::HostMode &mode)
{ {
if (!m_server || !m_localDevice) if (!m_server || !m_localDevice)
@ -140,19 +148,7 @@ void BluetoothServer::readData()
if (!client) if (!client)
return; return;
m_receiveBuffer.append(client->readAll()); emit dataAvailable(m_clientList.key(client), client->readAll());
qCDebug(dcBluetoothServerTraffic()) << "Current data buffer:" << qUtf8Printable(m_receiveBuffer);
int splitIndex = m_receiveBuffer.indexOf("}\n{");
while (splitIndex > -1) {
emit dataAvailable(m_clientList.key(client), m_receiveBuffer.left(splitIndex + 1));
m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 2);
splitIndex = m_receiveBuffer.indexOf("}\n{");
}
if (m_receiveBuffer.endsWith("}\n")) {
emit dataAvailable(m_clientList.key(client), m_receiveBuffer.trimmed());
m_receiveBuffer.clear();
}
} }
bool BluetoothServer::startServer() bool BluetoothServer::startServer()
@ -250,7 +246,6 @@ bool BluetoothServer::stopServer()
m_localDevice = nullptr; m_localDevice = nullptr;
} }
m_receiveBuffer.clear();
return true; return true;
} }

View File

@ -34,7 +34,7 @@ class BluetoothServer : public TransportInterface
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit BluetoothServer(QObject *parent = 0); explicit BluetoothServer(QObject *parent = nullptr);
~BluetoothServer(); ~BluetoothServer();
static bool hardwareAvailable(); static bool hardwareAvailable();
@ -42,6 +42,8 @@ public:
void sendData(const QUuid &clientId, const QByteArray &data) override; void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clients, const QByteArray &data) override; void sendData(const QList<QUuid> &clients, const QByteArray &data) override;
void terminateClientConnection(const QUuid &clientId) override;
private: private:
QBluetoothServer *m_server = nullptr; QBluetoothServer *m_server = nullptr;
QBluetoothLocalDevice *m_localDevice = nullptr; QBluetoothLocalDevice *m_localDevice = nullptr;
@ -49,7 +51,6 @@ private:
// Client storage // Client storage
QHash<QUuid, QBluetoothSocket *> m_clientList; QHash<QUuid, QBluetoothSocket *> m_clientList;
QByteArray m_receiveBuffer;
private slots: private slots:
void onHostModeChanged(const QBluetoothLocalDevice::HostMode &mode); void onHostModeChanged(const QBluetoothLocalDevice::HostMode &mode);

View File

@ -53,6 +53,16 @@ void CloudTransport::sendData(const QList<QUuid> &clientIds, const QByteArray &d
} }
} }
void CloudTransport::terminateClientConnection(const QUuid &clientId)
{
foreach (const ConnectionContext &ctx, m_connections) {
if (ctx.clientId == clientId) {
ctx.proxyConnection->disconnectServer();
return;
}
}
}
bool CloudTransport::startServer() bool CloudTransport::startServer()
{ {
qCDebug(dcCloud()) << "Started cloud transport"; qCDebug(dcCloud()) << "Started cloud transport";
@ -80,28 +90,34 @@ void CloudTransport::connectToCloud(const QString &token, const QString &nonce)
connect(context.proxyConnection, &RemoteProxyConnection::ready, this, &CloudTransport::transportReady); connect(context.proxyConnection, &RemoteProxyConnection::ready, this, &CloudTransport::transportReady);
connect(context.proxyConnection, &RemoteProxyConnection::stateChanged, this, &CloudTransport::remoteConnectionStateChanged); connect(context.proxyConnection, &RemoteProxyConnection::stateChanged, this, &CloudTransport::remoteConnectionStateChanged);
connect(context.proxyConnection, &RemoteProxyConnection::dataReady, this, &CloudTransport::transportDataReady); connect(context.proxyConnection, &RemoteProxyConnection::dataReady, this, &CloudTransport::transportDataReady);
connect(context.proxyConnection, &RemoteProxyConnection::remoteConnectionEstablished, this, &CloudTransport::transportConnected);
connect(context.proxyConnection, &RemoteProxyConnection::disconnected, this, &CloudTransport::transportDisconnected);
context.proxyConnection->connectServer(m_proxyUrl); context.proxyConnection->connectServer(m_proxyUrl);
} }
void CloudTransport::remoteConnectionStateChanged(RemoteProxyConnection::State state) void CloudTransport::remoteConnectionStateChanged(RemoteProxyConnection::State state)
{
qCDebug(dcCloudTraffic()) << "Remote connection state changed" << state;
}
void CloudTransport::transportConnected()
{ {
RemoteProxyConnection *proxyConnection = qobject_cast<RemoteProxyConnection*>(sender()); RemoteProxyConnection *proxyConnection = qobject_cast<RemoteProxyConnection*>(sender());
ConnectionContext context = m_connections.value(proxyConnection); ConnectionContext context = m_connections.value(proxyConnection);
switch (state) { qCDebug(dcCloud()) << "The remote client connected successfully" << proxyConnection->tunnelPartnerName() << proxyConnection->tunnelPartnerUuid();
case RemoteProxyConnection::StateRemoteConnected: emit clientConnected(context.clientId);
qCDebug(dcCloud()) << "The remote client connected successfully" << proxyConnection->tunnelPartnerName() << proxyConnection->tunnelPartnerUuid(); }
emit clientConnected(context.clientId);
break; void CloudTransport::transportDisconnected()
case RemoteProxyConnection::StateDisconnected: {
qCDebug(dcCloud()) << "The remote connection disconnected."; RemoteProxyConnection *proxyConnection = qobject_cast<RemoteProxyConnection*>(sender());
emit clientDisconnected(context.clientId); ConnectionContext context = m_connections.take(proxyConnection);
break; proxyConnection->deleteLater();
default:
qCDebug(dcCloud()) << "Remote connection state changed" << state; qCDebug(dcCloud()) << "The remote connection disconnected." << context.clientId;
break; emit clientDisconnected(context.clientId);
}
} }
void CloudTransport::transportReady() void CloudTransport::transportReady()
@ -118,7 +134,7 @@ void CloudTransport::transportDataReady(const QByteArray &data)
{ {
RemoteProxyConnection *proxyConnection = qobject_cast<RemoteProxyConnection*>(sender()); RemoteProxyConnection *proxyConnection = qobject_cast<RemoteProxyConnection*>(sender());
ConnectionContext context = m_connections.value(proxyConnection); ConnectionContext context = m_connections.value(proxyConnection);
qCDebug(dcCloudTraffic()) << "Date received:" << context.clientId.toString() << data; qCDebug(dcCloudTraffic()) << "Data received:" << context.clientId.toString() << data;
emit dataAvailable(context.clientId, data); emit dataAvailable(context.clientId, data);
} }

View File

@ -36,6 +36,8 @@ public:
void sendData(const QUuid &clientId, const QByteArray &data) override; void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clientIds, const QByteArray &data) override; void sendData(const QList<QUuid> &clientIds, const QByteArray &data) override;
void terminateClientConnection(const QUuid &clientId) override;
bool startServer() override; bool startServer() override;
bool stopServer() override; bool stopServer() override;
@ -43,11 +45,13 @@ signals:
public slots: public slots:
void connectToCloud(const QString &token, const QString &nonce); void connectToCloud(const QString &token, const QString &nonce);
void remoteConnectionStateChanged(remoteproxyclient::RemoteProxyConnection::State state);
private slots: private slots:
void remoteConnectionStateChanged(remoteproxyclient::RemoteProxyConnection::State state);
void transportConnected();
void transportReady(); void transportReady();
void transportDataReady(const QByteArray &data); void transportDataReady(const QByteArray &data);
void transportDisconnected();
private: private:
QUrl m_proxyUrl; QUrl m_proxyUrl;

View File

@ -500,6 +500,30 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data)
qCDebug(dcJsonRpcTraffic()) << "Incoming data:" << data; qCDebug(dcJsonRpcTraffic()) << "Incoming data:" << data;
TransportInterface *interface = qobject_cast<TransportInterface *>(sender()); TransportInterface *interface = qobject_cast<TransportInterface *>(sender());
// Handle packet fragmentation
QByteArray buffer = m_clientBuffers[clientId];
buffer.append(data);
int splitIndex = buffer.indexOf("}\n{");
while (splitIndex > -1) {
processJsonPacket(interface, clientId, buffer.left(splitIndex + 1));
buffer = buffer.right(buffer.length() - splitIndex - 2);
splitIndex = buffer.indexOf("}\n{");
}
if (buffer.trimmed().endsWith("}")) {
processJsonPacket(interface, clientId, buffer);
buffer.clear();
}
m_clientBuffers[clientId] = buffer;
if (buffer.size() > 1024 * 10) {
qCWarning(dcJsonRpc()) << "Client buffer larger than 10KB and no valid data. Dropping client connection.";
interface->terminateClientConnection(clientId);
}
}
void JsonRPCServer::processJsonPacket(TransportInterface *interface, const QUuid &clientId, const QByteArray &data)
{
QJsonParseError error; QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
@ -716,6 +740,7 @@ void JsonRPCServer::clientDisconnected(const QUuid &clientId)
qCDebug(dcJsonRpc()) << "Client disconnected:" << clientId; qCDebug(dcJsonRpc()) << "Client disconnected:" << clientId;
m_clientTransports.remove(clientId); m_clientTransports.remove(clientId);
m_clientNotifications.remove(clientId); m_clientNotifications.remove(clientId);
m_clientBuffers.remove(clientId);
if (m_pushButtonTransactions.values().contains(clientId)) { if (m_pushButtonTransactions.values().contains(clientId)) {
NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId)); NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId));
} }

View File

@ -79,6 +79,8 @@ private:
void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
QVariantMap createWelcomeMessage(TransportInterface *interface) const; QVariantMap createWelcomeMessage(TransportInterface *interface) const;
void processJsonPacket(TransportInterface *interface, const QUuid &clientId, const QByteArray &data);
private slots: private slots:
void setup(); void setup();
@ -101,6 +103,7 @@ private:
QHash<JsonReply *, TransportInterface *> m_asyncReplies; QHash<JsonReply *, TransportInterface *> m_asyncReplies;
QHash<QUuid, TransportInterface*> m_clientTransports; QHash<QUuid, TransportInterface*> m_clientTransports;
QHash<QUuid, QByteArray> m_clientBuffers;
QHash<QUuid, bool> m_clientNotifications; QHash<QUuid, bool> m_clientNotifications;
QHash<int, QUuid> m_pushButtonTransactions; QHash<int, QUuid> m_pushButtonTransactions;

View File

@ -52,6 +52,12 @@ void MockTcpServer::sendData(const QList<QUuid> &clients, const QByteArray &data
} }
} }
void MockTcpServer::terminateClientConnection(const QUuid &clientId)
{
emit connectionTerminated(clientId);
emit clientDisconnected(clientId);
}
QList<MockTcpServer *> MockTcpServer::servers() QList<MockTcpServer *> MockTcpServer::servers()
{ {
return s_allServers; return s_allServers;

View File

@ -35,17 +35,19 @@ class MockTcpServer : public TransportInterface
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit MockTcpServer(QObject *parent = 0); explicit MockTcpServer(QObject *parent = nullptr);
~MockTcpServer(); ~MockTcpServer() override;
void sendData(const QUuid &clientId, const QByteArray &data) override; void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clients, const QByteArray &data) override; void sendData(const QList<QUuid> &clients, const QByteArray &data) override;
void terminateClientConnection(const QUuid &clientId) override;
/************** Used for testing **************************/ /************** Used for testing **************************/
static QList<MockTcpServer*> servers(); static QList<MockTcpServer*> servers();
void injectData(const QUuid &clientId, const QByteArray &data); void injectData(const QUuid &clientId, const QByteArray &data);
signals: signals:
void outgoingData(const QUuid &clientId, const QByteArray &data); void outgoingData(const QUuid &clientId, const QByteArray &data);
void connectionTerminated(const QUuid &clientId);
/************** Used for testing **************************/ /************** Used for testing **************************/
public slots: public slots:

View File

@ -105,6 +105,14 @@ void TcpServer::sendData(const QList<QUuid> &clients, const QByteArray &data)
} }
} }
void TcpServer::terminateClientConnection(const QUuid &clientId)
{
QTcpSocket *client = m_clientList.value(clientId);
if (client) {
client->abort();
}
}
/*! Sending \a data to the client with the given \a clientId.*/ /*! Sending \a data to the client with the given \a clientId.*/
void TcpServer::sendData(const QUuid &clientId, const QByteArray &data) void TcpServer::sendData(const QUuid &clientId, const QByteArray &data)
{ {
@ -245,6 +253,8 @@ void SslServer::incomingConnection(qintptr socketDescriptor)
{ {
QSslSocket *sslSocket = new QSslSocket(this); QSslSocket *sslSocket = new QSslSocket(this);
qCDebug(dcTcpServer()) << "New client socket connection:" << sslSocket;
connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ emit clientConnected(sslSocket); }); connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ emit clientConnected(sslSocket); });
connect(sslSocket, &QSslSocket::readyRead, this, &SslServer::onSocketReadyRead); connect(sslSocket, &QSslSocket::readyRead, this, &SslServer::onSocketReadyRead);
connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected); connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected);
@ -265,6 +275,7 @@ void SslServer::incomingConnection(qintptr socketDescriptor)
void SslServer::onClientDisconnected() void SslServer::onClientDisconnected()
{ {
QSslSocket *socket = static_cast<QSslSocket*>(sender()); QSslSocket *socket = static_cast<QSslSocket*>(sender());
qCDebug(dcTcpServer()) << "Client socket disconnected:" << socket;
emit clientDisconnected(socket); emit clientDisconnected(socket);
socket->deleteLater(); socket->deleteLater();
} }
@ -273,18 +284,8 @@ void SslServer::onSocketReadyRead()
{ {
QSslSocket *socket = static_cast<QSslSocket*>(sender()); QSslSocket *socket = static_cast<QSslSocket*>(sender());
QByteArray data = socket->readAll(); QByteArray data = socket->readAll();
qCDebug(dcTcpServerTraffic()) << "SocketReadyRead:" << data; qCDebug(dcTcpServerTraffic()) << "Reading socket data:" << data;
m_receiveBuffer.append(data); emit dataAvailable(socket, data);
int splitIndex = m_receiveBuffer.indexOf("}\n{");
while (splitIndex > -1) {
emit dataAvailable(socket, m_receiveBuffer.left(splitIndex + 1));
m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 2);
splitIndex = m_receiveBuffer.indexOf("}\n{");
}
if (m_receiveBuffer.endsWith("}\n")) {
emit dataAvailable(socket, m_receiveBuffer);
m_receiveBuffer.clear();
}
} }
} }

View File

@ -65,7 +65,6 @@ private slots:
private: private:
bool m_sslEnabled = false; bool m_sslEnabled = false;
QSslConfiguration m_config; QSslConfiguration m_config;
QByteArray m_receiveBuffer;
}; };
class TcpServer : public TransportInterface class TcpServer : public TransportInterface
@ -80,6 +79,8 @@ public:
void sendData(const QUuid &clientId, const QByteArray &data) override; void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clients, const QByteArray &data) override; void sendData(const QList<QUuid> &clients, const QByteArray &data) override;
void terminateClientConnection(const QUuid &clientId) override;
private: private:
QTimer *m_timer; QTimer *m_timer;

View File

@ -62,6 +62,11 @@
Pure virtual method for sending \a data to \a clients over the corresponding \l{TransportInterface}. Pure virtual method for sending \a data to \a clients over the corresponding \l{TransportInterface}.
*/ */
/*! \fn void nymeaserver::TransportInterface::terminateClientConnection(const QUuid &clientId);
Pure virtual method for terminating \a clients connection. The JSON RPC server might call this when a
client violates the protocol. Transports should immediately abort the connection to the client.
*/
/*! \fn void nymeaserver::TransportInterface::dataAvailable(const QUuid &clientId, const QByteArray &data); /*! \fn void nymeaserver::TransportInterface::dataAvailable(const QUuid &clientId, const QByteArray &data);
This signal is emitted when valid \a data from the client with the given \a clientId are available. This signal is emitted when valid \a data from the client with the given \a clientId are available.

View File

@ -40,6 +40,8 @@ public:
virtual void sendData(const QUuid &clientId, const QByteArray &data) = 0; virtual void sendData(const QUuid &clientId, const QByteArray &data) = 0;
virtual void sendData(const QList<QUuid> &clients, const QByteArray &data) = 0; virtual void sendData(const QList<QUuid> &clients, const QByteArray &data) = 0;
virtual void terminateClientConnection(const QUuid &clientId) = 0;
void setConfiguration(const ServerConfiguration &config); void setConfiguration(const ServerConfiguration &config);
ServerConfiguration configuration() const; ServerConfiguration configuration() const;

View File

@ -109,6 +109,14 @@ void WebSocketServer::sendData(const QList<QUuid> &clients, const QByteArray &da
} }
} }
void WebSocketServer::terminateClientConnection(const QUuid &clientId)
{
QWebSocket *client = m_clientList.value(clientId);
if (client) {
client->abort();
}
}
QHash<QString, QString> WebSocketServer::createTxtRecord() QHash<QString, QString> WebSocketServer::createTxtRecord()
{ {
// Note: reversed order // Note: reversed order

View File

@ -50,6 +50,8 @@ public:
void sendData(const QUuid &clientId, const QByteArray &data) override; void sendData(const QUuid &clientId, const QByteArray &data) override;
void sendData(const QList<QUuid> &clients, const QByteArray &data) override; void sendData(const QList<QUuid> &clients, const QByteArray &data) override;
void terminateClientConnection(const QUuid &clientId) override;
private: private:
QWebSocketServer *m_server = nullptr; QWebSocketServer *m_server = nullptr;
QHash<QUuid, QWebSocket *> m_clientList; QHash<QUuid, QWebSocket *> m_clientList;

View File

@ -41,6 +41,8 @@ class TestJSONRPC: public NymeaTestBase
Q_OBJECT Q_OBJECT
private slots: private slots:
void initTestCase();
void testHandshake(); void testHandshake();
void testInitialSetup(); void testInitialSetup();
@ -100,6 +102,12 @@ private slots:
void testPushButtonAuthConnectionDrop(); void testPushButtonAuthConnectionDrop();
void testInitialSetupWithPushButtonAuth(); void testInitialSetupWithPushButtonAuth();
void testDataFragmentation_data();
void testDataFragmentation();
void testGarbageData();
private: private:
QStringList extractRefs(const QVariant &variant); QStringList extractRefs(const QVariant &variant);
@ -129,6 +137,13 @@ QStringList TestJSONRPC::extractRefs(const QVariant &variant)
return QStringList(); return QStringList();
} }
void TestJSONRPC::initTestCase()
{
NymeaTestBase::initTestCase();
QLoggingCategory::setFilterRules("*.debug=false\n"
"JsonRpc*.debug=true");
}
void TestJSONRPC::testHandshake() void TestJSONRPC::testHandshake()
{ {
// first test if the handshake message is auto-sent upon connecting // first test if the handshake message is auto-sent upon connecting
@ -179,7 +194,7 @@ void TestJSONRPC::testInitialSetup()
QVERIFY(spy.isValid()); QVERIFY(spy.isValid());
// Introspect call should work in any case // Introspect call should work in any case
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Introspect\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Introspect\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -192,7 +207,7 @@ void TestJSONRPC::testInitialSetup()
// Hello call should work in any case too // Hello call should work in any case too
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -205,7 +220,7 @@ void TestJSONRPC::testInitialSetup()
// Any other call should fail with "unauthorized" even if we use a previously valid token // Any other call should fail with "unauthorized" even if we use a previously valid token
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -219,7 +234,7 @@ void TestJSONRPC::testInitialSetup()
// But it should still fail when giving a an invalid username // But it should still fail when giving a an invalid username
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -232,7 +247,7 @@ void TestJSONRPC::testInitialSetup()
// or when giving a bad password // or when giving a bad password
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"weak\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"weak\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -245,7 +260,7 @@ void TestJSONRPC::testInitialSetup()
// Now lets play by the rules (with an uppercase email) // Now lets play by the rules (with an uppercase email)
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -258,7 +273,7 @@ void TestJSONRPC::testInitialSetup()
// Now that we have a user, initialSetup should be false in the Hello call // Now that we have a user, initialSetup should be false in the Hello call
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -271,7 +286,7 @@ void TestJSONRPC::testInitialSetup()
// Calls should still fail, given we didn't get a new token yet // Calls should still fail, given we didn't get a new token yet
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -283,7 +298,7 @@ void TestJSONRPC::testInitialSetup()
// Now lets authenticate with a wrong user // Now lets authenticate with a wrong user
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@wrong.domain\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@wrong.domain\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -298,7 +313,7 @@ void TestJSONRPC::testInitialSetup()
// Now lets authenticate with a wrong password // Now lets authenticate with a wrong password
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -313,7 +328,7 @@ void TestJSONRPC::testInitialSetup()
// Now lets authenticate for real (but intentionally use a lowercase email here, should still work) // Now lets authenticate for real (but intentionally use a lowercase email here, should still work)
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -328,7 +343,7 @@ void TestJSONRPC::testInitialSetup()
// Now do a Version call with the valid token and it should work // Now do a Version call with the valid token and it should work
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -347,7 +362,7 @@ void TestJSONRPC::testRevokeToken()
// Now get all the tokens // Now get all the tokens
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -362,7 +377,7 @@ void TestJSONRPC::testRevokeToken()
// Authenticate and create a new token // Authenticate and create a new token
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -377,7 +392,7 @@ void TestJSONRPC::testRevokeToken()
// Now do a Version call with the new token and it should work // Now do a Version call with the new token and it should work
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -389,7 +404,7 @@ void TestJSONRPC::testRevokeToken()
// Now get all the tokens using the old token // Now get all the tokens using the old token
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -412,7 +427,7 @@ void TestJSONRPC::testRevokeToken()
// Revoke the new token // Revoke the new token
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -424,7 +439,7 @@ void TestJSONRPC::testRevokeToken()
// Do a call with the now removed token, it should be forbidden // Do a call with the now removed token, it should be forbidden
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -441,14 +456,16 @@ void TestJSONRPC::testBasicCall_data()
QTest::addColumn<bool>("idValid"); QTest::addColumn<bool>("idValid");
QTest::addColumn<bool>("valid"); QTest::addColumn<bool>("valid");
QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}") << true << true; QTest::newRow("valid call 1") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}") << true << true;
QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\"}") << false << false; QTest::newRow("valid call 2") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n") << true << true;
QTest::newRow("missing method") << QByteArray("{\"id\":42}") << true << false; QTest::newRow("valid call 3") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n\n\n\n") << true << true;
QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"JSO") << false << false; QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\"}\n") << false << false;
QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\"}") << true << false; QTest::newRow("missing method") << QByteArray("{\"id\":42}\n") << true << false;
QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\"}") << true << false; QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"JSO}\n") << false << false;
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\"}") << true << false; QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\"}\n") << true << false;
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << true << false; QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\"}\n") << true << false;
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\"}\n") << true << false;
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"params\":{\"törööö\":\"chooo-chooo\"}}\n") << true << false;
} }
void TestJSONRPC::testBasicCall() void TestJSONRPC::testBasicCall()
@ -1136,7 +1153,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
// Hello call should work in any case, telling us initial setup is required // Hello call should work in any case, telling us initial setup is required
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -1176,7 +1193,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
// initialSetupRequired should be false in Hello call now // initialSetupRequired should be false in Hello call now
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -1190,7 +1207,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
// CreateUser without a token should fail now even though there are 0 users in the DB // CreateUser without a token should fail now even though there are 0 users in the DB
spy.clear(); spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}"); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();
} }
@ -1203,6 +1220,66 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
} }
void TestJSONRPC::testDataFragmentation_data()
{
QTest::addColumn<QList<QByteArray> >("packets");
QList<QByteArray> packets;
packets.append("{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
QTest::newRow("1 packet") << packets;
packets.clear();
packets.append("{\"id\": 555, \"m");
packets.append("ethod\": \"JSONRPC.Hello\"}\n");
QTest::newRow("2 packets") << packets;
packets.clear();
packets.append("{\"id\": 555, \"m");
packets.append("ethod\": \"JSONRP");
packets.append("C.Hello\"}\n");
QTest::newRow("3 packets") << packets;
packets.clear();
packets.append("{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n{\"id\": 5556, \"metho");
QTest::newRow("next packet start appended") << packets;
}
void TestJSONRPC::testDataFragmentation()
{
QJsonDocument jsonDoc;
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
QFETCH(QList<QByteArray>, packets);
foreach (const QByteArray &packet, packets) {
spy.clear();
m_mockTcpServer->injectData(m_clientId, packet);
}
if (spy.count() == 0) {
spy.wait();
}
QCOMPARE(spy.count(), 1);
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
QCOMPARE(jsonDoc.toVariant().toMap().value("status").toString(), QStringLiteral("success"));
}
void TestJSONRPC::testGarbageData()
{
QSignalSpy spy(m_mockTcpServer, &MockTcpServer::connectionTerminated);
QByteArray data;
for (int i = 0; i < 1024; i++) {
data.append("a");
}
for (int i = 0; i < 11; i ++) {
m_mockTcpServer->injectData(m_clientId, data);
}
QCOMPARE(spy.count(), 1);
}
#include "testjsonrpc.moc" #include "testjsonrpc.moc"
QTEST_MAIN(TestJSONRPC) QTEST_MAIN(TestJSONRPC)

View File

@ -98,20 +98,6 @@ EventTypeId mockParentChildEventId = EventTypeId("d24ede5f-4064-4898-bb84-cfb533
ActionTypeId mockParentChildActionId = ActionTypeId("d24ede5f-4064-4898-bb84-cfb533b1fbc0"); ActionTypeId mockParentChildActionId = ActionTypeId("d24ede5f-4064-4898-bb84-cfb533b1fbc0");
StateTypeId mockParentChildStateId = StateTypeId("d24ede5f-4064-4898-bb84-cfb533b1fbc0"); StateTypeId mockParentChildStateId = StateTypeId("d24ede5f-4064-4898-bb84-cfb533b1fbc0");
static QHash<QString, bool> s_loggingFilters;
static void loggingCategoryFilter(QLoggingCategory *category)
{
if (s_loggingFilters.contains(category->categoryName())) {
bool debugEnabled = s_loggingFilters.value(category->categoryName());
category->setEnabled(QtDebugMsg, debugEnabled);
category->setEnabled(QtWarningMsg, debugEnabled || s_loggingFilters.value("Warnings"));
} else {
category->setEnabled(QtDebugMsg, true);
category->setEnabled(QtWarningMsg, true);
}
}
NymeaTestBase::NymeaTestBase(QObject *parent) : NymeaTestBase::NymeaTestBase(QObject *parent) :
QObject(parent), QObject(parent),
m_commandId(0) m_commandId(0)
@ -144,39 +130,7 @@ void NymeaTestBase::initTestCase()
NymeaSettings nymeadSettings(NymeaSettings::SettingsRoleGlobal); NymeaSettings nymeadSettings(NymeaSettings::SettingsRoleGlobal);
nymeadSettings.clear(); nymeadSettings.clear();
// debug categories QLoggingCategory::setFilterRules("*.debug=false");
// logging filers for core and libnymea
s_loggingFilters.insert("Application", true);
s_loggingFilters.insert("Warnings", true);
s_loggingFilters.insert("DeviceManager", true);
s_loggingFilters.insert("RuleEngine", true);
s_loggingFilters.insert("Hardware", true);
s_loggingFilters.insert("Connection", true);
s_loggingFilters.insert("LogEngine", true);
s_loggingFilters.insert("TcpServer", true);
s_loggingFilters.insert("WebServer", true);
s_loggingFilters.insert("WebSocketServer", true);
s_loggingFilters.insert("JsonRpc", true);
s_loggingFilters.insert("Rest", true);
s_loggingFilters.insert("OAuth2", true);
s_loggingFilters.insert("TimeManager", true);
QHash<QString, bool> loggingFiltersPlugins;
foreach (const QJsonObject &pluginMetadata, DeviceManager::pluginsMetadata()) {
loggingFiltersPlugins.insert(pluginMetadata.value("idName").toString(), false);
}
// add plugin metadata to the static hash
foreach (const QString &category, loggingFiltersPlugins.keys()) {
if (category == "MockDevice") {
s_loggingFilters.insert(category, true);
} else {
s_loggingFilters.insert(category, false);
}
}
QLoggingCategory::installFilter(loggingCategoryFilter);
// Start the server // Start the server
NymeaCore::instance(); NymeaCore::instance();
@ -239,7 +193,7 @@ QVariant NymeaTestBase::injectAndWait(const QString &method, const QVariantMap &
QJsonDocument jsonDoc = QJsonDocument::fromVariant(call); QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
m_mockTcpServer->injectData(clientId.isNull() ? m_clientId : clientId, jsonDoc.toJson(QJsonDocument::Compact)); m_mockTcpServer->injectData(clientId.isNull() ? m_clientId : clientId, jsonDoc.toJson(QJsonDocument::Compact) + "\n");
if (spy.count() == 0) { if (spy.count() == 0) {
spy.wait(); spy.wait();

View File

@ -101,7 +101,7 @@ class NymeaTestBase : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit NymeaTestBase(QObject *parent = 0); explicit NymeaTestBase(QObject *parent = nullptr);
protected slots: protected slots:
void initTestCase(); void initTestCase();