diff --git a/data/config/guhd.conf b/data/config/guhd.conf index e51571c3..39f20628 100644 --- a/data/config/guhd.conf +++ b/data/config/guhd.conf @@ -9,8 +9,8 @@ https=false publicFolder=/usr/share/guh-webinterface/public [WebSocketServer] -port=4444 https=false +port=4444 [SSL-Configuration] certificate=/etc/ssl/certs/guhd-certificate.crt diff --git a/debian/changelog b/debian/changelog index e36dda51..68d7cb52 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +guh (0.6.0) vivid; urgency=medium + + * Add websocket server + + -- Simon Stürz Sat, 04 Aug 2015 16:13:43 +0200 + guh (0.5.0) vivid; urgency=medium * Add webserver and REST API diff --git a/debian/control b/debian/control index 7032e457..e3cbcbb8 100644 --- a/debian/control +++ b/debian/control @@ -30,6 +30,8 @@ Depends: libqt5network5, libguh1 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} +Recommends: + libqt5websockets5 Description: Server daemon for home automation systems guh is an open source home automation server, which allows to control a lot of different devices from many different manufacturers. With the diff --git a/guh.pri b/guh.pri index 95849cda..4d3ef9c5 100644 --- a/guh.pri +++ b/guh.pri @@ -26,7 +26,7 @@ enable433gpio { DEFINES += GPIO433 } -# check webserver support +# check websocket support equals(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 3) { DEFINES += WEBSOCKET } diff --git a/server/jsonrpc/jsonrpcserver.cpp b/server/jsonrpc/jsonrpcserver.cpp index 29c44f96..322f07b6 100644 --- a/server/jsonrpc/jsonrpcserver.cpp +++ b/server/jsonrpc/jsonrpcserver.cpp @@ -104,8 +104,8 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject connect(m_websocketServer, SIGNAL(clientConnected(const QUuid &)), this, SLOT(clientConnected(const QUuid &))); connect(m_websocketServer, SIGNAL(clientDisconnected(const QUuid &)), this, SLOT(clientDisconnected(const QUuid &))); connect(m_websocketServer, SIGNAL(dataAvailable(QUuid, QString, QString, QVariantMap)), this, SLOT(processData(QUuid, QString, QString, QVariantMap))); - m_websocketServer->startServer(); + m_websocketServer->startServer(); m_interfaces.append(m_websocketServer); #else Q_UNUSED(sslConfiguration) diff --git a/tests/auto/websocketserver/testwebsocketserver.cpp b/tests/auto/websocketserver/testwebsocketserver.cpp index e543baa8..d5b9416c 100644 --- a/tests/auto/websocketserver/testwebsocketserver.cpp +++ b/tests/auto/websocketserver/testwebsocketserver.cpp @@ -43,15 +43,22 @@ class TestWebSocketServer: public GuhTestBase private slots: #ifdef WEBSOCKET - void testHandshake(); void pingTest(); - void introspect(); -#endif -private: + void testBasicCall_data(); + void testBasicCall(); + void introspect(); + +private: + int m_socketCommandId; + + QVariant injectSocketAndWait(const QString &method, const QVariantMap ¶ms = QVariantMap()); + QVariant injectSocketData(const QByteArray &data); + +#endif }; #ifdef WEBSOCKET @@ -88,11 +95,127 @@ void TestWebSocketServer::pingTest() spyPong.wait(); QVERIFY2(spyPong.count() > 0, "no pong"); qDebug() << "ping response" << spyPong.first().at(0) << spyPong.first().at(1).toString(); + socket->close(); + socket->deleteLater(); } + +void TestWebSocketServer::testBasicCall_data() +{ + QTest::addColumn("data"); + QTest::addColumn("valid"); + + QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}") << true; + QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\"}")<< false; + QTest::newRow("missing method") << QByteArray("{\"id\":42}") << false; + QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"JSO")<< false; + QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\"}") << false; + QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\"}") << false; + QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\"}") << false; + QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << false; +} + +void TestWebSocketServer::testBasicCall() +{ + QFETCH(QByteArray, data); + QFETCH(bool, valid); + + QVariant response = injectSocketData(data); + if (valid) + QVERIFY2(response.toMap().value("status").toString() == "success", "Call wasn't parsed correctly by guh."); +} + + void TestWebSocketServer::introspect() { + QVariant response = injectSocketAndWait("JSONRPC.Introspect"); + QVariantMap methods = response.toMap().value("params").toMap().value("methods").toMap(); + QVariantMap notifications = response.toMap().value("params").toMap().value("notifications").toMap(); + QVariantMap types = response.toMap().value("params").toMap().value("types").toMap(); + + QVERIFY2(methods.count() > 0, "No methods in Introspect response!"); + QVERIFY2(notifications.count() > 0, "No notifications in Introspect response!"); + QVERIFY2(types.count() > 0, "No types in Introspect response!"); + +} + +QVariant TestWebSocketServer::injectSocketAndWait(const QString &method, const QVariantMap ¶ms) +{ + + QVariantMap call; + call.insert("id", m_socketCommandId); + call.insert("method", method); + call.insert("params", params); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(call); + + QWebSocket *socket = new QWebSocket("guh tests", QWebSocketProtocol::Version13); + QSignalSpy spyConnection(socket, SIGNAL(connected())); + socket->open(QUrl(QStringLiteral("ws://localhost:4444"))); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy spy(socket, SIGNAL(textMessageReceived(QString))); + socket->sendTextMessage(QString(jsonDoc.toJson())); + 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; + jsonDoc = QJsonDocument::fromJson(spy.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_socketCommandId) { + m_socketCommandId++; + return jsonDoc.toVariant(); + } + } + m_socketCommandId++; + return QVariant(); +} + +QVariant TestWebSocketServer::injectSocketData(const QByteArray &data) +{ + QWebSocket *socket = new QWebSocket("guh tests", QWebSocketProtocol::Version13); + QSignalSpy spyConnection(socket, SIGNAL(connected())); + socket->open(QUrl(QStringLiteral("ws://localhost:4444"))); + 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_socketCommandId++; + return QVariant(); } #endif