diff --git a/libguh/loggingcategories.cpp b/libguh/loggingcategories.cpp index 4f840769..f7cc8dcf 100644 --- a/libguh/loggingcategories.cpp +++ b/libguh/loggingcategories.cpp @@ -30,4 +30,4 @@ Q_LOGGING_CATEGORY(dcWebServer, "WebServer") Q_LOGGING_CATEGORY(dcJsonRpc, "JsonRpc") Q_LOGGING_CATEGORY(dcRest, "Rest") Q_LOGGING_CATEGORY(dcLogEngine, "LogEngine") - +Q_LOGGING_CATEGORY(dcWebSocketServer, "WebSocketServer") diff --git a/libguh/loggingcategories.h b/libguh/loggingcategories.h index a7f6aa5c..cd740357 100644 --- a/libguh/loggingcategories.h +++ b/libguh/loggingcategories.h @@ -34,5 +34,6 @@ Q_DECLARE_LOGGING_CATEGORY(dcRest) Q_DECLARE_LOGGING_CATEGORY(dcLogEngine) Q_DECLARE_LOGGING_CATEGORY(dcTcpServer) Q_DECLARE_LOGGING_CATEGORY(dcWebServer) +Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServer) #endif // LOGGINGCATEGORYS_H diff --git a/server/jsonrpc/jsonrpcserver.cpp b/server/jsonrpc/jsonrpcserver.cpp index 64e6bedf..81d27cdd 100644 --- a/server/jsonrpc/jsonrpcserver.cpp +++ b/server/jsonrpc/jsonrpcserver.cpp @@ -45,6 +45,10 @@ #include "logginghandler.h" #include "statehandler.h" +#ifdef WEBSOCKET +#include "websocketserver.h" +#endif + #include #include @@ -57,6 +61,9 @@ JsonRPCServer::JsonRPCServer(QObject *parent): #else m_tcpServer(new TcpServer(this)), #endif + #ifdef WEBSOCKET + m_websocketServer(new WebSocketServer(this)), + #endif m_notificationId(0) { // First, define our own JSONRPC methods @@ -90,6 +97,17 @@ JsonRPCServer::JsonRPCServer(QObject *parent): connect(m_tcpServer, SIGNAL(dataAvailable(QUuid, QString, QString, QVariantMap)), this, SLOT(processData(QUuid, QString, QString, QVariantMap))); m_tcpServer->startServer(); + m_interfaces.append(m_tcpServer); + +#ifdef WEBSOCKET + 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_interfaces.append(m_websocketServer); +#endif + QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); } @@ -156,6 +174,8 @@ void JsonRPCServer::setup() void JsonRPCServer::processData(const QUuid &clientId, const QString &targetNamespace, const QString &method, const QVariantMap &message) { + TransportInterface *interface = qobject_cast(sender()); + // Note: id, targetNamespace and method already checked in TcpServer int commandId = message.value("id").toInt(); QVariantMap params = message.value("params").toMap(); @@ -165,7 +185,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QString &targetName JsonHandler *handler = m_handlers.value(targetNamespace); QPair validationResult = handler->validateParams(method, params); if (!validationResult.first) { - m_tcpServer->sendErrorResponse(clientId, commandId, "Invalid params: " + validationResult.second); + interface->sendErrorResponse(clientId, commandId, "Invalid params: " + validationResult.second); return; } @@ -175,6 +195,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QString &targetName JsonReply *reply; QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params)); if (reply->type() == JsonReply::TypeAsync) { + m_asyncReplies.insert(reply, interface); reply->setClientId(clientId); reply->setCommandId(commandId); connect(reply, &JsonReply::finished, this, &JsonRPCServer::asyncReplyFinished); @@ -182,7 +203,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QString &targetName } else { Q_ASSERT_X((targetNamespace == "JSONRPC" && method == "Introspect") || handler->validateReturns(method, reply->data()).first ,"validating return value", formatAssertion(targetNamespace, method, handler, reply->data()).toLatin1().data()); - m_tcpServer->sendResponse(clientId, commandId, reply->data()); + interface->sendResponse(clientId, commandId, reply->data()); reply->deleteLater(); } } @@ -199,7 +220,7 @@ QString JsonRPCServer::formatAssertion(const QString &targetNamespace, const QSt void JsonRPCServer::sendNotification(const QVariantMap ¶ms) { - JsonHandler *handler = qobject_cast(sender()); + JsonHandler *handler = qobject_cast(sender()); QMetaMethod method = handler->metaObject()->method(senderSignalIndex()); QVariantMap notification; @@ -207,20 +228,21 @@ void JsonRPCServer::sendNotification(const QVariantMap ¶ms) notification.insert("notification", handler->name() + "." + method.name()); notification.insert("params", params); - emit notificationDataReady(notification); - - m_tcpServer->sendData(m_clients.keys(true), notification); + foreach (TransportInterface *interface, m_interfaces) { + interface->sendData(m_clients.keys(true), notification); + } } void JsonRPCServer::asyncReplyFinished() { - JsonReply *reply = qobject_cast(sender()); + JsonReply *reply = qobject_cast(sender()); + TransportInterface *interface = m_asyncReplies.take(reply); 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()); - m_tcpServer->sendResponse(reply->clientId(), reply->commandId(), reply->data()); + interface->sendResponse(reply->clientId(), reply->commandId(), reply->data()); } else { - m_tcpServer->sendErrorResponse(reply->clientId(), reply->commandId(), "Command timed out"); + interface->sendErrorResponse(reply->clientId(), reply->commandId(), "Command timed out"); } reply->deleteLater(); @@ -239,15 +261,17 @@ void JsonRPCServer::registerHandler(JsonHandler *handler) void JsonRPCServer::clientConnected(const QUuid &clientId) { - // Notifications disabled by default - m_clients.insert(clientId, false); + // Notifications enabled by default + m_clients.insert(clientId, true); + + TransportInterface *interface = qobject_cast(sender()); QVariantMap handshake; handshake.insert("id", 0); handshake.insert("server", "guh JSONRPC interface"); handshake.insert("version", GUH_VERSION_STRING); handshake.insert("protocol version", JSON_PROTOCOL_VERSION); - m_tcpServer->sendData(clientId, handshake); + interface->sendData(clientId, handshake); } void JsonRPCServer::clientDisconnected(const QUuid &clientId) diff --git a/server/jsonrpc/jsonrpcserver.h b/server/jsonrpc/jsonrpcserver.h index 19c4e2ec..00f3dd7a 100644 --- a/server/jsonrpc/jsonrpcserver.h +++ b/server/jsonrpc/jsonrpcserver.h @@ -24,6 +24,7 @@ #include "plugin/deviceclass.h" #include "jsonhandler.h" +#include "transportinterface.h" #include "types/action.h" #include "types/event.h" @@ -35,6 +36,9 @@ class Device; namespace guhserver { +#ifdef WEBSOCKET +class WebSocketServer; +#endif #ifdef TESTING_ENABLED class MockTcpServer; @@ -58,7 +62,6 @@ public: signals: void commandReceived(const QString &targetNamespace, const QString &command, const QVariantMap ¶ms); - void notificationDataReady(const QVariantMap ¬ification); private slots: void setup(); @@ -83,7 +86,14 @@ private: #else TcpServer *m_tcpServer; #endif - QHash m_handlers; + +#ifdef WEBSOCKET + WebSocketServer *m_websocketServer; +#endif + + QList m_interfaces; + QHash m_handlers; + QHash m_asyncReplies; // clientId, notificationsEnabled QHash m_clients; diff --git a/server/main.cpp b/server/main.cpp index af0334e6..f5ef33f5 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -64,6 +64,7 @@ int main(int argc, char *argv[]) s_loggingFilters.insert("Connection", true); s_loggingFilters.insert("TcpServer", true); s_loggingFilters.insert("WebServer", true); + s_loggingFilters.insert("WebSocketServer", true); s_loggingFilters.insert("JsonRpc", false); s_loggingFilters.insert("Rest", true); s_loggingFilters.insert("Hardware", false); diff --git a/server/server.pri b/server/server.pri index 973e1227..9a0faf07 100644 --- a/server/server.pri +++ b/server/server.pri @@ -1,6 +1,8 @@ contains(DEFINES, WEBSOCKET){ QT += websockets + SOURCES += $$top_srcdir/server/websocketserver.cpp + HEADERS += $$top_srcdir/server/websocketserver.h } @@ -24,7 +26,6 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/webserver.cpp \ $$top_srcdir/server/transportinterface.cpp \ $$top_srcdir/server/servermanager.cpp \ - $$top_srcdir/server/websocketserver.cpp \ $$top_srcdir/server/httprequest.cpp \ $$top_srcdir/server/httpreply.cpp \ $$top_srcdir/server/rest/restserver.cpp \ @@ -36,7 +37,6 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/rest/pluginsresource.cpp \ $$top_srcdir/server/rest/rulesresource.cpp \ - HEADERS += $$top_srcdir/server/guhcore.h \ $$top_srcdir/server/tcpserver.h \ $$top_srcdir/server/ruleengine.h \ @@ -58,7 +58,6 @@ HEADERS += $$top_srcdir/server/guhcore.h \ $$top_srcdir/server/webserver.h \ $$top_srcdir/server/transportinterface.h \ $$top_srcdir/server/servermanager.h \ - $$top_srcdir/server/websocketserver.h \ $$top_srcdir/server/httprequest.h \ $$top_srcdir/server/httpreply.h \ $$top_srcdir/server/rest/restserver.h \ diff --git a/server/servermanager.cpp b/server/servermanager.cpp index f44b25a6..e04ebf9f 100644 --- a/server/servermanager.cpp +++ b/server/servermanager.cpp @@ -19,6 +19,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "servermanager.h" +#include "guhsettings.h" namespace guhserver { diff --git a/server/servermanager.h b/server/servermanager.h index df0d8dd6..15e00bfd 100644 --- a/server/servermanager.h +++ b/server/servermanager.h @@ -26,8 +26,7 @@ #include "loggingcategories.h" #include "jsonrpc/jsonrpcserver.h" #include "rest/restserver.h" - - +#include "websocketserver.h" namespace guhserver { diff --git a/server/tcpserver.cpp b/server/tcpserver.cpp index c30df41f..1f7d5a66 100644 --- a/server/tcpserver.cpp +++ b/server/tcpserver.cpp @@ -65,6 +65,10 @@ TcpServer::TcpServer(QObject *parent) : settings.endGroup(); } +TcpServer::~TcpServer() +{ +} + void TcpServer::sendData(const QList &clients, const QVariantMap &data) { foreach (const QUuid &client, clients) { @@ -93,75 +97,10 @@ void TcpServer::reloadNetworkInterfaces() settings.endGroup(); } - -void TcpServer::validateMessage(const QUuid &clientId, const QByteArray &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(clientId, -1, QString("Failed to parse JSON data: %1").arg(error.errorString())); - return; - } - - QVariantMap message = jsonDoc.toVariant().toMap(); - - bool success; - int commandId = message.value("id").toInt(&success); - if (!success) { - qCWarning(dcJsonRpc) << "Error parsing command. Missing \"id\":" << message; - sendErrorResponse(clientId, commandId, "Error parsing command. Missing 'id'"); - 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(clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString())); - return; - } - - QString targetNamespace = commandList.first(); - QString method = commandList.last(); - - JsonHandler *handler = GuhCore::instance()->jsonRPCServer()->handlers().value(targetNamespace); - if (!handler) { - sendErrorResponse(clientId, commandId, "No such namespace"); - return; - } - if (!handler->hasMethod(method)) { - sendErrorResponse(clientId, commandId, "No such method"); - return; - } - - emit dataAvailable(clientId, targetNamespace, method, message); -} - -void TcpServer::sendResponse(const QUuid &clientId, int commandId, const QVariantMap ¶ms) -{ - QVariantMap response; - response.insert("id", commandId); - response.insert("status", "success"); - response.insert("params", params); - - sendData(clientId, response); -} - -void TcpServer::sendErrorResponse(const QUuid &clientId, int commandId, const QString &error) -{ - QVariantMap errorResponse; - errorResponse.insert("id", commandId); - errorResponse.insert("status", "error"); - errorResponse.insert("error", error); - - sendData(clientId, errorResponse); -} - - void TcpServer::sendData(const QUuid &clientId, const QVariantMap &data) { - QTcpSocket *client = m_clientList.value(clientId); + QTcpSocket *client = 0; + client = m_clientList.value(clientId); if (client) { client->write(QJsonDocument::fromVariant(data).toJson()); } diff --git a/server/tcpserver.h b/server/tcpserver.h index d4501100..9d23b5a0 100644 --- a/server/tcpserver.h +++ b/server/tcpserver.h @@ -38,7 +38,8 @@ class TcpServer : public TransportInterface Q_OBJECT public: explicit TcpServer(QObject *parent = 0); - + ~TcpServer(); + void sendData(const QUuid &clientId, const QVariantMap &data) override; void sendData(const QList &clients, const QVariantMap &data) override; @@ -53,11 +54,7 @@ private: QStringList m_ipVersions; void reloadNetworkInterfaces(); - void validateMessage(const QUuid &clientId, const QByteArray &data); -public: - void sendResponse(const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap()); - void sendErrorResponse(const QUuid &clientId, int commandId, const QString &error); private slots: void onClientConnected(); diff --git a/server/transportinterface.cpp b/server/transportinterface.cpp index 8109a47b..967ac8e0 100644 --- a/server/transportinterface.cpp +++ b/server/transportinterface.cpp @@ -19,6 +19,11 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "transportinterface.h" +#include "loggingcategories.h" +#include "jsonhandler.h" +#include "guhcore.h" + +#include namespace guhserver { @@ -31,4 +36,70 @@ TransportInterface::~TransportInterface() { } +void TransportInterface::sendResponse(const QUuid &clientId, int commandId, const QVariantMap ¶ms) +{ + QVariantMap response; + response.insert("id", commandId); + response.insert("status", "success"); + response.insert("params", params); + + sendData(clientId, response); +} + +void TransportInterface::sendErrorResponse(const QUuid &clientId, int commandId, const QString &error) +{ + QVariantMap errorResponse; + errorResponse.insert("id", commandId); + errorResponse.insert("status", "error"); + errorResponse.insert("error", error); + + sendData(clientId, errorResponse); +} + + +void TransportInterface::validateMessage(const QUuid &clientId, const QByteArray &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(clientId, -1, QString("Failed to parse JSON data: %1").arg(error.errorString())); + return; + } + + QVariantMap message = jsonDoc.toVariant().toMap(); + + bool success; + int commandId = message.value("id").toInt(&success); + if (!success) { + qCWarning(dcJsonRpc) << "Error parsing command. Missing \"id\":" << message; + sendErrorResponse(clientId, commandId, "Error parsing command. Missing 'id'"); + 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(clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString())); + return; + } + + QString targetNamespace = commandList.first(); + QString method = commandList.last(); + + JsonHandler *handler = GuhCore::instance()->jsonRPCServer()->handlers().value(targetNamespace); + if (!handler) { + sendErrorResponse(clientId, commandId, "No such namespace"); + return; + } + if (!handler->hasMethod(method)) { + sendErrorResponse(clientId, commandId, "No such method"); + return; + } + + emit dataAvailable(clientId, targetNamespace, method, message); +} + + } diff --git a/server/transportinterface.h b/server/transportinterface.h index bd52ce96..a3484a80 100644 --- a/server/transportinterface.h +++ b/server/transportinterface.h @@ -33,11 +33,17 @@ class TransportInterface : public QObject Q_OBJECT public: explicit TransportInterface(QObject *parent = 0); - virtual ~TransportInterface(); + virtual ~TransportInterface() = 0; virtual void sendData(const QUuid &clientId, const QVariantMap &data) = 0; virtual void sendData(const QList &clients, const QVariantMap &data) = 0; + void sendResponse(const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap()); + void sendErrorResponse(const QUuid &clientId, int commandId, const QString &error); + +protected: + void validateMessage(const QUuid &clientId, const QByteArray &data); + signals: void clientConnected(const QUuid &clientId); void clientDisconnected(const QUuid &clientId); diff --git a/server/webserver.cpp b/server/webserver.cpp index d8116d9a..5b103261 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -42,21 +42,15 @@ WebServer::WebServer(QObject *parent) : { // load webserver settings GuhSettings settings(GuhSettings::SettingsRoleGlobal); - qCDebug(dcWebServer) << "Loading webserver settings from:" << settings.fileName(); + qCDebug(dcWebSocketServer) << "Loading web socket server settings from:" << settings.fileName(); - settings.beginGroup("Webserver"); + settings.beginGroup("WebSocketServer"); m_port = settings.value("port", 3000).toInt(); m_useSsl = settings.value("https", false).toBool(); - m_webinterfaceDir = QDir(settings.value("publicFolder", "/usr/share/guh-webinterface/public/").toString()); QString certificateFileName = settings.value("certificate", QVariant("/etc/ssl/certs/guhd-certificate.crt")).toString(); QString keyFileName = settings.value("certificate-key", QVariant("/etc/ssl/private/guhd-certificate.key")).toString(); settings.endGroup(); - // check public directory - qCDebug(dcWebServer) << "Publish webinterface folder" << m_webinterfaceDir.path(); - if (!m_webinterfaceDir.exists()) - qCWarning(dcWebServer) << "Web interface public folder" << m_webinterfaceDir.path() << "does not exist."; - // check SSL if (m_useSsl && !QSslSocket::supportsSsl()) { qCWarning(dcWebServer) << "SSL is not supported/installed on this platform."; diff --git a/server/websocketserver.cpp b/server/websocketserver.cpp index d4f13cb8..7615c4cb 100644 --- a/server/websocketserver.cpp +++ b/server/websocketserver.cpp @@ -19,12 +19,31 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "websocketserver.h" +#include "guhsettings.h" +#include "loggingcategories.h" + +#include namespace guhserver { WebSocketServer::WebSocketServer(QObject *parent) : - TransportInterface(parent) + TransportInterface(parent), + m_server(0) { + // load webserver settings + GuhSettings settings(GuhSettings::SettingsRoleGlobal); + qCDebug(dcWebServer) << "Loading webserver settings from:" << settings.fileName(); + + settings.beginGroup("WebSocketServer"); + m_port = settings.value("port", 3001).toInt(); + m_useSsl = settings.value("https", false).toBool(); + settings.endGroup(); + + // check SSL + if (m_useSsl && !QSslSocket::supportsSsl()) { + qCWarning(dcWebServer) << "SSL is not supported/installed on this platform."; + m_useSsl = false; + } } WebSocketServer::~WebSocketServer() @@ -33,23 +52,122 @@ WebSocketServer::~WebSocketServer() void WebSocketServer::sendData(const QUuid &clientId, const QVariantMap &data) { - Q_UNUSED(clientId) - Q_UNUSED(data) + QWebSocket *client = 0; + client = m_clientList.value(clientId); + if (client) { + client->sendTextMessage(QJsonDocument::fromVariant(data).toJson()); + } } void WebSocketServer::sendData(const QList &clients, const QVariantMap &data) { - Q_UNUSED(clients) - Q_UNUSED(data) + foreach (const QUuid &client, clients) { + sendData(client, data); + } +} + +void WebSocketServer::onClientConnected() +{ + // got a new client connected + QWebSocket *client = m_server->nextPendingConnection(); + qCDebug(dcConnection) << "Websocket server: new client connected:" << client->peerAddress().toString(); + + // check websocket version + if (client->version() != QWebSocketProtocol::Version13) { + qCWarning(dcWebSocketServer) << "Client with invalid protocol version" << client->version() << ". Rejecting."; + client->close(QWebSocketProtocol::CloseCodeProtocolError, QString("invalid protocol version: %1 != Supported Version 13").arg(client->version())); + delete client; + return; + } + + QUuid clientId = QUuid::createUuid(); + + // append the new client to the client list + m_clientList.insert(clientId, client); + + connect(client, SIGNAL(pong(quint64,QByteArray)), this, SLOT(onPing(quint64,QByteArray))); + connect(client, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(onBinaryMessageReceived(QByteArray))); + connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(onTextMessageReceived(QString))); + connect(client, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onClientError(QAbstractSocket::SocketError))); + connect(client, SIGNAL(disconnected()), this, SLOT(onClientDisconnected())); + + emit clientConnected(clientId); +} + +void WebSocketServer::onClientDisconnected() +{ + QWebSocket *client = qobject_cast(sender()); + qCDebug(dcConnection) << "Websocket server: client disconnected:" << client->peerAddress().toString(); + QUuid clientId = m_clientList.key(client); + m_clientList.take(clientId)->deleteLater(); +} + +void WebSocketServer::onBinaryMessageReceived(const QByteArray &data) +{ + QWebSocket *client = qobject_cast(sender()); + qCDebug(dcWebSocketServer) << "Binary message from" << client->peerAddress().toString() << ":" << data; +} + +void WebSocketServer::onTextMessageReceived(const QString &message) +{ + QWebSocket *client = qobject_cast(sender()); + qCDebug(dcWebSocketServer) << "Text message from" << client->peerAddress().toString() << ":" << message; + validateMessage(m_clientList.key(client), message.toUtf8()); +} + +void WebSocketServer::onClientError(QAbstractSocket::SocketError error) +{ + QWebSocket *client = qobject_cast(sender()); + qCWarning(dcConnection) << "Websocket client error:" << error << client->errorString(); +} + +void WebSocketServer::onServerError(QAbstractSocket::SocketError error) +{ + qCWarning(dcConnection) << "Websocket server error:" << error << m_server->errorString(); +} + +void WebSocketServer::onPing(quint64 elapsedTime, const QByteArray &payload) +{ + QWebSocket *client = qobject_cast(sender()); + qCDebug(dcWebSocketServer) << "ping response" << client->peerAddress() << elapsedTime << payload; } bool WebSocketServer::startServer() { - return false; + if (m_server) { + qCWarning(dcConnection) << "There is allready a websocket server instance. This should never happen!!! Please report this bug!"; + return false; + } + + if (m_useSsl) { + m_server = new QWebSocketServer("guh", QWebSocketServer::SecureMode, this); + } else { + m_server = new QWebSocketServer("guh", QWebSocketServer::NonSecureMode, this); + } + connect (m_server, &QWebSocketServer::newConnection, this, &WebSocketServer::onClientConnected); + connect (m_server, &QWebSocketServer::acceptError, this, &WebSocketServer::onServerError); + + if (!m_server->listen(QHostAddress::Any, m_port)) { + qCWarning(dcConnection) << "Websocket server" << m_server->serverName() << QString("could not listen on %1:%2").arg(m_server->serverAddress().toString()).arg(m_port); + return false; + } + + if (m_server->secureMode() == QWebSocketServer::NonSecureMode) { + qCDebug(dcConnection) << "Started websocket server" << m_server->serverName() << QString("on ws://%1:%2").arg(m_server->serverAddress().toString()).arg(m_port); + } else { + qCDebug(dcConnection) << "Started websocket server" << m_server->serverName() << QString("on wss://%1:%2").arg(m_server->serverAddress().toString()).arg(m_port); + } + qCDebug(dcWebSocketServer) << "Supported protocol versions" << m_server->supportedVersions(); + + return true; } bool WebSocketServer::stopServer() { + qCDebug(dcConnection) << "Stopping websocket server"; + m_server->close(); + delete m_server; + m_server = 0; return false; } diff --git a/server/websocketserver.h b/server/websocketserver.h index 9c53c24b..8258d83c 100644 --- a/server/websocketserver.h +++ b/server/websocketserver.h @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include "transportinterface.h" @@ -43,6 +45,23 @@ public: void sendData(const QUuid &clientId, const QVariantMap &data) override; void sendData(const QList &clients, const QVariantMap &data) override; +private: + QWebSocketServer *m_server; + QHash m_clientList; + + bool m_enabled; + bool m_useSsl; + qint16 m_port; + +private slots: + void onClientConnected(); + void onClientDisconnected(); + void onBinaryMessageReceived(const QByteArray &data); + void onTextMessageReceived(const QString &message); + void onClientError(QAbstractSocket::SocketError error); + void onServerError(QAbstractSocket::SocketError error); + void onPing(quint64 elapsedTime, const QByteArray & payload); + public slots: bool startServer() override; bool stopServer() override; diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index aed5d7aa..2630aad4 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -14,3 +14,6 @@ SUBDIRS = versioning \ restplugins \ restvendors \ restrules \ + websocketserver \ + + diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index c023218c..9af90447 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -98,7 +98,8 @@ void TestJSONRPC::testHandshake() QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); QVariantMap handShake = jsonDoc.toVariant().toMap(); - QVERIFY2(handShake.value("version").toString() == GUH_VERSION_STRING, "Handshake version doesn't match Guh version."); + QString guhVersionString(GUH_VERSION_STRING); + QVERIFY2(handShake.value("version").toString() == guhVersionString, "Handshake version doesn't match Guh version."); m_mockTcpServer->clientDisconnected(newClientId); } diff --git a/tests/auto/websocketserver/testwebsocketserver.cpp b/tests/auto/websocketserver/testwebsocketserver.cpp new file mode 100644 index 00000000..a382b8d4 --- /dev/null +++ b/tests/auto/websocketserver/testwebsocketserver.cpp @@ -0,0 +1,93 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2015 Simon Stuerz * + * * + * This file is part of guh. * + * * + * Guh is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, version 2 of the License. * + * * + * Guh is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with guh. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "guhtestbase.h" +#include "guhcore.h" +#include "devicemanager.h" +#include "mocktcpserver.h" +#include "webserver.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace guhserver; + +class TestWebSocketServer: public GuhTestBase +{ + Q_OBJECT + +private slots: + void testHandshake(); + + void pingTest(); + + void introspect(); + +private: + +}; + + +void TestWebSocketServer::testHandshake() +{ + QWebSocket *socket = new QWebSocket("guh tests", QWebSocketProtocol::Version13); + QSignalSpy spy(socket, SIGNAL(textMessageReceived(QString))); + socket->open(QUrl(QStringLiteral("ws://localhost:3001"))); + spy.wait(); + QVERIFY2(spy.count() > 0, "Did not get the handshake message upon connect."); + QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().first().toByteArray()); + QVariantMap handShake = jsonDoc.toVariant().toMap(); + + QString guhVersionString(GUH_VERSION_STRING); + QString jsonProtocolVersionString(JSON_PROTOCOL_VERSION); + QCOMPARE(handShake.value("version").toString(), guhVersionString); + QCOMPARE(handShake.value("protocol version").toString(), jsonProtocolVersionString); + + socket->close(); + socket->deleteLater(); +} + +void TestWebSocketServer::pingTest() +{ + QWebSocket *socket = new QWebSocket("guh tests", QWebSocketProtocol::Version13); + QSignalSpy spyConnection(socket, SIGNAL(connected())); + socket->open(QUrl(QStringLiteral("ws://localhost:3001"))); + spyConnection.wait(); + QVERIFY2(spyConnection.count() > 0, "not connected"); + + QSignalSpy spyPong(socket, SIGNAL(pong(quint64,QByteArray))); + socket->ping("hallo"); + spyPong.wait(); + QVERIFY2(spyPong.count() > 0, "no pong"); + qDebug() << "ping response" << spyPong.first().at(0) << spyPong.first().at(1).toString(); +} + +void TestWebSocketServer::introspect() +{ + +} + +#include "testwebsocketserver.moc" +QTEST_MAIN(TestWebSocketServer) diff --git a/tests/auto/websocketserver/websocketserver.pro b/tests/auto/websocketserver/websocketserver.pro new file mode 100644 index 00000000..4796cfde --- /dev/null +++ b/tests/auto/websocketserver/websocketserver.pro @@ -0,0 +1,9 @@ +include(../../../guh.pri) +include(../autotests.pri) + +TARGET = websocketserver + +contains(DEFINES, WEBSOCKET){ + QT += websockets + SOURCES += testwebsocketserver.cpp +}