add websocket server
This commit is contained in:
parent
7f87cdd777
commit
557baff39d
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -45,6 +45,10 @@
|
||||
#include "logginghandler.h"
|
||||
#include "statehandler.h"
|
||||
|
||||
#ifdef WEBSOCKET
|
||||
#include "websocketserver.h"
|
||||
#endif
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QStringList>
|
||||
|
||||
@ -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<TransportInterface *>(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<bool, QString> 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<JsonHandler*>(sender());
|
||||
JsonHandler *handler = qobject_cast<JsonHandler *>(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<JsonReply*>(sender());
|
||||
JsonReply *reply = qobject_cast<JsonReply *>(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<TransportInterface *>(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)
|
||||
|
||||
@ -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<QString, JsonHandler*> m_handlers;
|
||||
|
||||
#ifdef WEBSOCKET
|
||||
WebSocketServer *m_websocketServer;
|
||||
#endif
|
||||
|
||||
QList<TransportInterface *> m_interfaces;
|
||||
QHash<QString, JsonHandler *> m_handlers;
|
||||
QHash<JsonReply *, TransportInterface *> m_asyncReplies;
|
||||
|
||||
// clientId, notificationsEnabled
|
||||
QHash<QUuid, bool> m_clients;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "servermanager.h"
|
||||
#include "guhsettings.h"
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
|
||||
@ -26,8 +26,7 @@
|
||||
#include "loggingcategories.h"
|
||||
#include "jsonrpc/jsonrpcserver.h"
|
||||
#include "rest/restserver.h"
|
||||
|
||||
|
||||
#include "websocketserver.h"
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
|
||||
@ -65,6 +65,10 @@ TcpServer::TcpServer(QObject *parent) :
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
TcpServer::~TcpServer()
|
||||
{
|
||||
}
|
||||
|
||||
void TcpServer::sendData(const QList<QUuid> &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());
|
||||
}
|
||||
|
||||
@ -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<QUuid> &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();
|
||||
|
||||
@ -19,6 +19,11 @@
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "transportinterface.h"
|
||||
#include "loggingcategories.h"
|
||||
#include "jsonhandler.h"
|
||||
#include "guhcore.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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<QUuid> &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);
|
||||
|
||||
@ -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.";
|
||||
|
||||
@ -19,12 +19,31 @@
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "websocketserver.h"
|
||||
#include "guhsettings.h"
|
||||
#include "loggingcategories.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
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<QUuid> &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<QWebSocket *>(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<QWebSocket *>(sender());
|
||||
qCDebug(dcWebSocketServer) << "Binary message from" << client->peerAddress().toString() << ":" << data;
|
||||
}
|
||||
|
||||
void WebSocketServer::onTextMessageReceived(const QString &message)
|
||||
{
|
||||
QWebSocket *client = qobject_cast<QWebSocket *>(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<QWebSocket *>(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<QWebSocket *>(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;
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
#include <QList>
|
||||
#include <QWebSocket>
|
||||
#include <QWebSocketServer>
|
||||
|
||||
#include "transportinterface.h"
|
||||
|
||||
@ -43,6 +45,23 @@ public:
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data) override;
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data) override;
|
||||
|
||||
private:
|
||||
QWebSocketServer *m_server;
|
||||
QHash<QUuid, QWebSocket *> 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;
|
||||
|
||||
@ -14,3 +14,6 @@ SUBDIRS = versioning \
|
||||
restplugins \
|
||||
restvendors \
|
||||
restrules \
|
||||
websocketserver \
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
93
tests/auto/websocketserver/testwebsocketserver.cpp
Normal file
93
tests/auto/websocketserver/testwebsocketserver.cpp
Normal file
@ -0,0 +1,93 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2015 Simon Stuerz <simon.stuerz@guh.guru> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "guhtestbase.h"
|
||||
#include "guhcore.h"
|
||||
#include "devicemanager.h"
|
||||
#include "mocktcpserver.h"
|
||||
#include "webserver.h"
|
||||
|
||||
#include <QtTest/QtTest>
|
||||
#include <QCoreApplication>
|
||||
#include <QMetaType>
|
||||
#include <QByteArray>
|
||||
#include <QSignalSpy>
|
||||
#include <QWebSocket>
|
||||
#include <QJsonDocument>
|
||||
|
||||
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)
|
||||
9
tests/auto/websocketserver/websocketserver.pro
Normal file
9
tests/auto/websocketserver/websocketserver.pro
Normal file
@ -0,0 +1,9 @@
|
||||
include(../../../guh.pri)
|
||||
include(../autotests.pri)
|
||||
|
||||
TARGET = websocketserver
|
||||
|
||||
contains(DEFINES, WEBSOCKET){
|
||||
QT += websockets
|
||||
SOURCES += testwebsocketserver.cpp
|
||||
}
|
||||
Reference in New Issue
Block a user