From 5a3c7a6cfb444de8c1f280e42aeec363ba6ecaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Sat, 25 Jul 2015 13:46:44 +0200 Subject: [PATCH] first working REST call add httpreply and httprequest to lib --- guh.pri | 6 + guh.pro | 7 ++ libguh/libguh.pro | 7 +- libguh/network/httpreply.cpp | 16 ++- libguh/network/httpreply.h | 5 +- libguh/network/httprequest.cpp | 129 +++++++++++++++++++++ libguh/network/httprequest.h | 62 ++++++++++ server/guhcore.cpp | 13 ++- server/guhcore.h | 6 +- server/jsonrpc/jsonrpcserver.cpp | 10 +- server/jsonrpc/jsonrpcserver.h | 1 + server/rest/restserver.cpp | 136 ++++++++++++++++++++++ server/rest/restserver.h | 59 ++++++++++ server/server.pri | 14 ++- server/server.pro | 4 +- server/servermanager.cpp | 45 ++++++++ server/servermanager.h | 51 +++++++++ server/tcpserver.cpp | 8 +- server/tcpserver.h | 4 +- server/transportinterface.cpp | 4 + server/transportinterface.h | 6 +- server/webserver.cpp | 149 +++++++++++++------------ server/webserver.h | 18 ++- server/websocketserver.cpp | 30 +++++ server/websocketserver.h | 45 ++++++++ tests/auto/webserver/testwebserver.cpp | 4 +- 26 files changed, 728 insertions(+), 111 deletions(-) create mode 100644 libguh/network/httprequest.cpp create mode 100644 libguh/network/httprequest.h create mode 100644 server/rest/restserver.cpp create mode 100644 server/rest/restserver.h create mode 100644 server/servermanager.cpp create mode 100644 server/servermanager.h create mode 100644 server/websocketserver.cpp create mode 100644 server/websocketserver.h diff --git a/guh.pri b/guh.pri index 2847b655..1c40da81 100644 --- a/guh.pri +++ b/guh.pri @@ -26,5 +26,11 @@ enable433gpio { DEFINES += GPIO433 } +# check webserver support +equals(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 3) { + DEFINES += WEBSERVER +} + + top_srcdir=$$PWD top_builddir=$$shadowed($$PWD) diff --git a/guh.pro b/guh.pro index 61b7b657..4dad53fd 100644 --- a/guh.pro +++ b/guh.pro @@ -25,6 +25,7 @@ test.commands = LD_LIBRARY_PATH=$$top_builddir/libguh make check QMAKE_EXTRA_TARGETS += licensecheck doc test +message(Qt version: $$[QT_VERSION]) message("Building guh version $${GUH_VERSION_STRING}") message("JSON-RPC API version $${JSON_PROTOCOL_VERSION}") message("REST API version $${REST_API_VERSION}") @@ -33,6 +34,12 @@ coverage { message("Building coverage.") } +contains(DEFINES, WEBSERVER){ + message("Building guh with webserver.") +} else { + message("Building guh without webserver.") +} + contains(DEFINES, GPIO433){ message("Radio 433 for GPIO's enabled") } else { diff --git a/libguh/libguh.pro b/libguh/libguh.pro index 69527edd..65b48006 100644 --- a/libguh/libguh.pro +++ b/libguh/libguh.pro @@ -39,7 +39,8 @@ SOURCES += plugin/device.cpp \ types/statedescriptor.cpp \ loggingcategories.cpp \ guhsettings.cpp \ - network/httpreply.cpp + network/httpreply.cpp \ + network/httprequest.cpp HEADERS += plugin/device.h \ plugin/deviceclass.h \ @@ -64,7 +65,6 @@ HEADERS += plugin/device.h \ types/event.h \ types/eventdescriptor.h \ types/vendor.h \ - types/typeutils.h \ types/paramtype.h \ types/param.h \ types/paramdescriptor.h \ @@ -74,5 +74,6 @@ HEADERS += plugin/device.h \ typeutils.h \ loggingcategories.h \ guhsettings.h \ - network/httpreply.h + network/httpreply.h \ + network/httprequest.h diff --git a/libguh/network/httpreply.cpp b/libguh/network/httpreply.cpp index 8d4a90dc..7603ea2a 100644 --- a/libguh/network/httpreply.cpp +++ b/libguh/network/httpreply.cpp @@ -187,11 +187,13 @@ void HttpReply::clear() m_payload.clear(); m_rawHeaderList.clear(); } - -/*! Returns the whole reply data of this \l{HttpReply}. The data contain the HTTP header and the payload. */ -QByteArray HttpReply::packReply() +/*! Packs the whole reply data of this \l{HttpReply}. The data can be accessed with \l{HttpReply::data()}. + \sa data() +*/ +void HttpReply::packReply() { // set status code + m_data.clear(); m_rawHeader.clear(); m_rawHeader.append("HTTP/1.1 " + QByteArray::number(m_statusCode) + " " + getHttpReasonPhrase(m_statusCode) + "\r\n"); @@ -201,8 +203,12 @@ QByteArray HttpReply::packReply() } m_rawHeader.append("\r\n"); - m_rawHeader.append(m_payload); - return m_rawHeader; + m_data = m_rawHeader.append(m_payload); +} + +QByteArray HttpReply::data() const +{ + return m_data; } QByteArray HttpReply::getHttpReasonPhrase(const HttpReply::HttpStatusCode &statusCode) diff --git a/libguh/network/httpreply.h b/libguh/network/httpreply.h index 027f8551..a3507fe7 100644 --- a/libguh/network/httpreply.h +++ b/libguh/network/httpreply.h @@ -78,12 +78,15 @@ public: bool isEmpty() const; void clear(); - QByteArray packReply(); + void packReply(); + + QByteArray data() const; private: HttpStatusCode m_statusCode; QByteArray m_rawHeader; QByteArray m_payload; + QByteArray m_data; QHash m_rawHeaderList; diff --git a/libguh/network/httprequest.cpp b/libguh/network/httprequest.cpp new file mode 100644 index 00000000..416e41e1 --- /dev/null +++ b/libguh/network/httprequest.cpp @@ -0,0 +1,129 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "httprequest.h" +#include "loggingcategories.h" + +#include + + +HttpRequest::HttpRequest(QByteArray rawData) : + m_rawData(rawData), + m_valid(false) +{ + // Parese the HTTP request. The request is invalid, until the end of the parse process. + if (m_rawData.isEmpty()) + return; + + // split the data into header and payload + int headerEndIndex = m_rawData.indexOf("\r\n\r\n"); + if (headerEndIndex < 0) { + qCWarning(dcWebServer) << "Could not parse end of HTTP header (empty line between header and body):" << rawData; + return; + } + m_rawHeader = m_rawData.left(headerEndIndex); + m_payload = m_rawData.right(m_rawData.length() - headerEndIndex).simplified(); + + // parse status line + QStringList headerLines = QString(m_rawHeader).split(QRegExp("\r\n")); + QString statusLine = headerLines.takeFirst(); + QStringList statusLineTokens = statusLine.split(QRegExp("[ \r\n][ \r\n]*")); + if (statusLineTokens.count() != 3) { + qCWarning(dcWebServer) << "Could not parse HTTP status line:" << statusLine; + return; + } + + m_method = statusLineTokens.at(0).toUtf8().simplified(); + m_urlQuery = QUrlQuery(statusLineTokens.at(1).simplified()); + m_httpVersion = statusLineTokens.at(2).toUtf8().simplified(); + + if (!m_httpVersion.contains("HTTP")) { + qCWarning(dcWebServer) << "Unknown HTTP version:" << m_httpVersion; + return; + } + + foreach (const QString &line, headerLines) { + if (!line.contains(":")) { + qCWarning(dcWebServer) << "Invalid HTTP header:" << line; + return; + } + int index = line.indexOf(":"); + QByteArray key = line.left(index).toUtf8().simplified(); + QByteArray value = line.right(line.count() - index - 1).toUtf8().simplified(); + m_rawHeaderList.insert(key, value); + } + + m_valid = true; +} + +QByteArray HttpRequest::rawHeader() const +{ + return m_rawHeader; +} + +QHash HttpRequest::rawHeaderList() const +{ + return m_rawHeaderList; +} + +QByteArray HttpRequest::method() const +{ + return m_method; +} + +QByteArray HttpRequest::httpVersion() const +{ + return m_httpVersion; +} + +QUrlQuery HttpRequest::urlQuery() const +{ + return m_urlQuery; +} + +QByteArray HttpRequest::payload() const +{ + return m_payload; +} + +bool HttpRequest::isValid() const +{ + return m_valid; +} + +bool HttpRequest::hasPayload() const +{ + return !m_payload.isEmpty(); +} + +QDebug operator<<(QDebug debug, const HttpRequest &httpRequest) +{ + debug << "===================================" << "\n"; + debug << " http version: " << httpRequest.httpVersion() << "\n"; + debug << " method: " << httpRequest.method() << "\n"; + debug << " URL query: " << httpRequest.urlQuery().query() << "\n"; + debug << " is valid: " << httpRequest.isValid() << "\n"; + debug << "-----------------------------------" << "\n"; + debug << httpRequest.rawHeader() << "\n"; + debug << "-----------------------------------" << "\n"; + debug << httpRequest.payload() << "\n"; + debug << "-----------------------------------" << "\n"; + return debug; +} diff --git a/libguh/network/httprequest.h b/libguh/network/httprequest.h new file mode 100644 index 00000000..705b07cb --- /dev/null +++ b/libguh/network/httprequest.h @@ -0,0 +1,62 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HTTPREQUEST_H +#define HTTPREQUEST_H + +#include +#include +#include +#include + +class HttpRequest +{ +public: + explicit HttpRequest(QByteArray rawData); + + QByteArray rawHeader() const; + QHash rawHeaderList() const; + + QByteArray method() const; + QByteArray httpVersion() const; + QUrlQuery urlQuery() const; + + QByteArray payload() const; + + bool isValid() const; + bool hasPayload() const; + +private: + QByteArray m_rawData; + QByteArray m_rawHeader; + QHash m_rawHeaderList; + + QByteArray m_method; + QByteArray m_httpVersion; + QUrlQuery m_urlQuery; + + QByteArray m_payload; + + bool m_valid; +}; + +QDebug operator<< (QDebug debug, const HttpRequest &httpRequest); + +#endif // HTTPREQUEST_H diff --git a/server/guhcore.cpp b/server/guhcore.cpp index e4a3b989..701b43ab 100644 --- a/server/guhcore.cpp +++ b/server/guhcore.cpp @@ -416,11 +416,8 @@ GuhCore::GuhCore(QObject *parent) : qCDebug(dcApplication) << "Creating Rule Engine"; m_ruleEngine = new RuleEngine(this); - qCDebug(dcApplication) << "Starting JSON RPC Server"; - m_jsonServer = new JsonRPCServer(this); - qCDebug(dcApplication) << "Starting REST Webserver"; - m_webServer = new WebServer(this); + m_serverManager = new ServerManager(this); connect(m_deviceManager, &DeviceManager::eventTriggered, this, &GuhCore::gotEvent); connect(m_deviceManager, &DeviceManager::deviceStateChanged, this, &GuhCore::deviceStateChanged); @@ -438,7 +435,6 @@ GuhCore::GuhCore(QObject *parent) : connect(m_ruleEngine, &RuleEngine::ruleConfigurationChanged, this, &GuhCore::ruleConfigurationChanged); m_logger->logSystemEvent(true); - m_webServer->startServer(); } /*! Connected to the DeviceManager's emitEvent signal. Events received in @@ -530,7 +526,12 @@ LogEngine* GuhCore::logEngine() const JsonRPCServer *GuhCore::jsonRPCServer() const { - return m_jsonServer; + return m_serverManager->jsonServer(); +} + +RestServer *GuhCore::restServer() const +{ + return m_serverManager->restServer(); } void GuhCore::actionExecutionFinished(const ActionId &id, DeviceManager::DeviceError status) diff --git a/server/guhcore.h b/server/guhcore.h index 3744f2a4..9812d315 100644 --- a/server/guhcore.h +++ b/server/guhcore.h @@ -30,7 +30,7 @@ #include "devicemanager.h" #include "ruleengine.h" -#include "webserver.h" +#include "servermanager.h" #include #include @@ -95,6 +95,7 @@ public: LogEngine* logEngine() const; JsonRPCServer *jsonRPCServer() const; + RestServer *restServer() const; signals: void eventTriggered(const Event &event); @@ -123,8 +124,7 @@ private: static GuhCore *s_instance; RunningMode m_runningMode; - WebServer *m_webServer; - JsonRPCServer *m_jsonServer; + ServerManager *m_serverManager; DeviceManager *m_deviceManager; RuleEngine *m_ruleEngine; diff --git a/server/jsonrpc/jsonrpcserver.cpp b/server/jsonrpc/jsonrpcserver.cpp index d2c3eaed..64e6bedf 100644 --- a/server/jsonrpc/jsonrpcserver.cpp +++ b/server/jsonrpc/jsonrpcserver.cpp @@ -52,11 +52,11 @@ namespace guhserver { JsonRPCServer::JsonRPCServer(QObject *parent): JsonHandler(parent), -#ifdef TESTING_ENABLED + #ifdef TESTING_ENABLED m_tcpServer(new MockTcpServer(this)), -#else + #else m_tcpServer(new TcpServer(this)), -#endif + #endif m_notificationId(0) { // First, define our own JSONRPC methods @@ -162,7 +162,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QString &targetName emit commandReceived(targetNamespace, method, params); - JsonHandler *handler = m_handlers.value(targetNamespace); + JsonHandler *handler = m_handlers.value(targetNamespace); QPair validationResult = handler->validateParams(method, params); if (!validationResult.first) { m_tcpServer->sendErrorResponse(clientId, commandId, "Invalid params: " + validationResult.second); @@ -207,6 +207,8 @@ 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); } diff --git a/server/jsonrpc/jsonrpcserver.h b/server/jsonrpc/jsonrpcserver.h index e3d1c96d..19c4e2ec 100644 --- a/server/jsonrpc/jsonrpcserver.h +++ b/server/jsonrpc/jsonrpcserver.h @@ -58,6 +58,7 @@ public: signals: void commandReceived(const QString &targetNamespace, const QString &command, const QVariantMap ¶ms); + void notificationDataReady(const QVariantMap ¬ification); private slots: void setup(); diff --git a/server/rest/restserver.cpp b/server/rest/restserver.cpp new file mode 100644 index 00000000..4c40ecb0 --- /dev/null +++ b/server/rest/restserver.cpp @@ -0,0 +1,136 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "restserver.h" +#include "loggingcategories.h" +#include "network/httprequest.h" +#include "network/httpreply.h" +#include "guhcore.h" + +#include "devicehandler.h" +#include "actionhandler.h" +#include "ruleshandler.h" +#include "eventhandler.h" +#include "logginghandler.h" +#include "statehandler.h" + +#include + +namespace guhserver { + +RestServer::RestServer(QObject *parent) : + QObject(parent) +{ + m_webserver = new WebServer(this); + connect(m_webserver, &WebServer::clientConnected, this, &RestServer::clientConnected); + connect(m_webserver, &WebServer::clientDisconnected, this, &RestServer::clientDisconnected); + connect(m_webserver, &WebServer::httpRequestReady, this, &RestServer::processHttpRequest); + + m_webserver->startServer(); +} + +void RestServer::clientConnected(const QUuid &clientId) +{ + m_clientList.append(clientId); +} + +void RestServer::clientDisconnected(const QUuid &clientId) +{ + m_clientList.removeAll(clientId); +} + +void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &request) +{ + qCDebug(dcWebServer) << "process http request" << clientId << request.method() << request.urlQuery().query(); + + QString targetNamespace; + QString method; + QVariantMap params; + + if (request.urlQuery().hasQueryItem("devices")) { + qCDebug(dcWebServer) << "devices resource"; + + } + + if (request.method() == "GET" && request.urlQuery().query() == "/api/v1/devices.json") { + targetNamespace = "Devices"; + method = "GetConfiguredDevices"; + } else if (request.method() == "GET" && request.urlQuery().query() == "/api/v1/devices.json") { + targetNamespace = "Devices"; + method = "GetConfiguredDevices"; + } else { + HttpReply httpReply(HttpReply::BadRequest); + httpReply.setPayload("400 Bad Request."); + httpReply.packReply(); + m_webserver->sendHttpReply(clientId, httpReply); + return; + } + + JsonHandler *handler = GuhCore::instance()->jsonRPCServer()->handlers().value(targetNamespace); + + QPair validationResult = handler->validateParams(method, params); + if (!validationResult.first) { + qCWarning(dcWebServer) << "Invalid params: " << validationResult.second; + return; + } + + JsonReply *jsonReply; + QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, jsonReply), Q_ARG(QVariantMap, params)); + if (jsonReply->type() == JsonReply::TypeAsync) { + jsonReply->setClientId(clientId); + connect(jsonReply, &JsonReply::finished, this, &RestServer::asyncReplyFinished); + jsonReply->startWait(); + m_asyncReplies.insert(clientId, jsonReply); + return; + } + + HttpReply httpReply(HttpReply::Ok); + httpReply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + httpReply.setPayload(QJsonDocument::fromVariant(jsonReply->data()).toJson()); + httpReply.packReply(); + + m_webserver->sendHttpReply(clientId, httpReply); + + jsonReply->deleteLater(); +} + +void RestServer::asyncReplyFinished() +{ + JsonReply *jsonReply = qobject_cast(sender()); + QUuid clientId = m_asyncReplies.key(jsonReply); + + if (!jsonReply->timedOut()) { + HttpReply httpReply(HttpReply::Ok); + httpReply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + httpReply.setPayload(QJsonDocument::fromVariant(jsonReply->data()).toJson()); + httpReply.packReply(); + m_webserver->sendHttpReply(clientId, httpReply); + } else { + HttpReply httpReply(HttpReply::GatewayTimeout); + httpReply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + httpReply.setPayload(QJsonDocument::fromVariant(jsonReply->data()).toJson()); + httpReply.packReply(); + m_webserver->sendHttpReply(clientId, httpReply); + } + jsonReply->deleteLater(); +} + + +} diff --git a/server/rest/restserver.h b/server/rest/restserver.h new file mode 100644 index 00000000..bfcd1257 --- /dev/null +++ b/server/rest/restserver.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef RESTSERVER_H +#define RESTSERVER_H + +#include + +#include "webserver.h" +#include "jsonhandler.h" + +class HttpRequest; +class HttpReply; + +namespace guhserver { + +class RestServer : public QObject +{ + Q_OBJECT +public: + explicit RestServer(QObject *parent = 0); + +private: + WebServer *m_webserver; + QList m_clientList; + QHash m_asyncReplies; + +signals: + void httpReplyReady(const HttpReply &httpReply); + +private slots: + void clientConnected(const QUuid &clientId); + void clientDisconnected(const QUuid &clientId); + + void processHttpRequest(const QUuid &clientId, const HttpRequest &request); + void asyncReplyFinished(); + +}; + +} + +#endif // RESTSERVER_H diff --git a/server/server.pri b/server/server.pri index 9c1abff3..3ba52857 100644 --- a/server/server.pri +++ b/server/server.pri @@ -1,3 +1,9 @@ + +contains(DEFINES, WEBSERVER){ + QT += websockets +} + + SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/tcpserver.cpp \ $$top_srcdir/server/ruleengine.cpp \ @@ -17,6 +23,9 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/logging/logentry.cpp \ $$top_srcdir/server/webserver.cpp \ $$top_srcdir/server/transportinterface.cpp \ + $$top_srcdir/server/servermanager.cpp \ + $$top_srcdir/server/websocketserver.cpp \ + $$top_srcdir/server/rest/restserver.cpp HEADERS += $$top_srcdir/server/guhcore.h \ @@ -33,11 +42,14 @@ HEADERS += $$top_srcdir/server/guhcore.h \ $$top_srcdir/server/jsonrpc/statehandler.h \ $$top_srcdir/server/jsonrpc/logginghandler.h \ $$top_srcdir/server/stateevaluator.h \ - $$top_srcdir/server/jsontypes.h \ $$top_srcdir/server/logging/logging.h \ $$top_srcdir/server/logging/logengine.h \ $$top_srcdir/server/logging/logfilter.h \ $$top_srcdir/server/logging/logentry.h \ $$top_srcdir/server/webserver.h \ $$top_srcdir/server/transportinterface.h \ + $$top_srcdir/server/servermanager.h \ + $$top_srcdir/server/websocketserver.h \ + $$top_srcdir/server/rest/restserver.h \ + diff --git a/server/server.pro b/server/server.pro index da8a2e07..97d6559b 100644 --- a/server/server.pro +++ b/server/server.pro @@ -8,7 +8,7 @@ INCLUDEPATH += ../libguh jsonrpc target.path = /usr/bin INSTALLS += target -QT += network sql +QT += sql LIBS += -L$$top_builddir/libguh/ -lguh @@ -16,7 +16,7 @@ include(server.pri) include(qtservice/qtservice.pri) SOURCES += main.cpp \ - guhservice.cpp + guhservice.cpp \ boblight { xcompile { diff --git a/server/servermanager.cpp b/server/servermanager.cpp new file mode 100644 index 00000000..41388942 --- /dev/null +++ b/server/servermanager.cpp @@ -0,0 +1,45 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "servermanager.h" + +namespace guhserver { + +ServerManager::ServerManager(QObject *parent) : + QObject(parent) +{ + qCDebug(dcApplication) << "Starting JSON RPC Server"; + m_jsonServer = new JsonRPCServer(this); + + qCDebug(dcApplication) << "Starting REST Webserver"; + m_restServer = new RestServer(this); +} + +JsonRPCServer *ServerManager::jsonServer() const +{ + return m_jsonServer; +} + +RestServer *ServerManager::restServer() const +{ + return m_restServer; +} + +} diff --git a/server/servermanager.h b/server/servermanager.h new file mode 100644 index 00000000..df0d8dd6 --- /dev/null +++ b/server/servermanager.h @@ -0,0 +1,51 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SERVERMANAGER_H +#define SERVERMANAGER_H + +#include + +#include "loggingcategories.h" +#include "jsonrpc/jsonrpcserver.h" +#include "rest/restserver.h" + + + +namespace guhserver { + +class ServerManager : public QObject +{ + Q_OBJECT +public: + explicit ServerManager(QObject *parent = 0); + + JsonRPCServer *jsonServer() const; + RestServer *restServer() const; + +private: + JsonRPCServer *m_jsonServer; + RestServer *m_restServer; + +}; + +} + +#endif // SERVERMANAGER_H diff --git a/server/tcpserver.cpp b/server/tcpserver.cpp index eaf2217c..e654aff1 100644 --- a/server/tcpserver.cpp +++ b/server/tcpserver.cpp @@ -167,7 +167,7 @@ void TcpServer::sendData(const QUuid &clientId, const QVariantMap &data) } } -void TcpServer::newClientConnected() +void TcpServer::onClientConnected() { // got a new client connected QTcpServer *server = qobject_cast(sender()); @@ -185,7 +185,6 @@ void TcpServer::newClientConnected() emit clientConnected(clientId); } - void TcpServer::readPackage() { QTcpSocket *client = qobject_cast(sender()); @@ -278,7 +277,7 @@ void TcpServer::onTimeout() QTcpServer *server = new QTcpServer(this); if(server->listen(address, m_port)) { qCDebug(dcConnection) << "Started TCP server on" << address.toString() << m_port; - connect(server, SIGNAL(newConnection()), SLOT(newClientConnected())); + connect(server, &QTcpServer::newConnection, this, &TcpServer::onClientConnected); m_serverList.insert(QUuid::createUuid(), server); } else { qCWarning(dcConnection) << "Tcp server error: can not listen on" << address.toString() << m_port; @@ -292,7 +291,6 @@ void TcpServer::onTimeout() qCDebug(dcConnection) << " - Tcp server on" << s->serverAddress().toString() << s->serverPort(); } } - } bool TcpServer::startServer() @@ -321,7 +319,7 @@ bool TcpServer::startServer() QTcpServer *server = new QTcpServer(this); if(server->listen(address, m_port)) { qCDebug(dcConnection) << "Started Tcp server on" << server->serverAddress().toString() << server->serverPort(); - connect(server, SIGNAL(newConnection()), SLOT(newClientConnected())); + connect(server, SIGNAL(newConnection()), SLOT(onClientConnected())); m_serverList.insert(QUuid::createUuid(), server); } else { qCWarning(dcConnection) << "Tcp server error: can not listen on" << interface.name() << address.toString() << m_port; diff --git a/server/tcpserver.h b/server/tcpserver.h index 515171c8..554bba46 100644 --- a/server/tcpserver.h +++ b/server/tcpserver.h @@ -60,9 +60,9 @@ public: void sendErrorResponse(const QUuid &clientId, int commandId, const QString &error); private slots: - void newClientConnected(); - void readPackage(); + void onClientConnected(); void onClientDisconnected(); + void readPackage(); void onError(const QAbstractSocket::SocketError &error); void onTimeout(); diff --git a/server/transportinterface.cpp b/server/transportinterface.cpp index 692b5636..8109a47b 100644 --- a/server/transportinterface.cpp +++ b/server/transportinterface.cpp @@ -27,4 +27,8 @@ TransportInterface::TransportInterface(QObject *parent) : { } +TransportInterface::~TransportInterface() +{ +} + } diff --git a/server/transportinterface.h b/server/transportinterface.h index 92ce6afd..62689675 100644 --- a/server/transportinterface.h +++ b/server/transportinterface.h @@ -22,6 +22,9 @@ #define TRANSPORTINTERFACE_H #include +#include +#include +#include namespace guhserver { @@ -30,7 +33,8 @@ class TransportInterface : public QObject Q_OBJECT public: explicit TransportInterface(QObject *parent = 0); - virtual ~TransportInterface() = default; + virtual ~TransportInterface(); + virtual void sendData(const QUuid &clientId, const QVariantMap &data) = 0; virtual void sendData(const QList &clients, const QVariantMap &data) = 0; diff --git a/server/webserver.cpp b/server/webserver.cpp index b15959a1..9eba1a51 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -22,7 +22,9 @@ #include "loggingcategories.h" #include "guhsettings.h" #include "network/httpreply.h" +#include "network/httprequest.h" +#include #include #include #include @@ -36,21 +38,22 @@ WebServer::WebServer(QObject *parent) : TransportInterface(parent), m_enabled(false) { - m_server = new QTcpServer(this); - // load webserver settings GuhSettings settings(GuhSettings::SettingsRoleGlobal); qCDebug(dcTcpServer) << "Loading Webserver settings from:" << settings.fileName(); settings.beginGroup("Webserver"); - m_port = settings.value("port", 3000).toUInt(); - // load the path to the webinterface public folder (qdir to make shore there is no "/" at the end) + m_port = settings.value("port", 3000).toInt(); m_webinterfaceDir = QDir(settings.value("publicFolder", "/usr/share/guh-webinterface/public/").toString()); settings.endGroup(); - qCDebug(dcTcpServer) << "Using port" << m_port; qCDebug(dcTcpServer) << "Publish webinterface from" << m_webinterfaceDir.path(); + if (!m_webinterfaceDir.exists()) + qCWarning(dcWebServer) << "Web interface path" << m_webinterfaceDir.path() << "does not exist."; + + // create webserver + m_server = new QTcpServer(this); connect(m_server, &QTcpServer::newConnection, this, &WebServer::onNewConnection); } @@ -61,19 +64,30 @@ WebServer::~WebServer() void WebServer::sendData(const QUuid &clientId, const QVariantMap &data) { - Q_UNUSED(clientId) - Q_UNUSED(data) - - // TODO: reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); - + QTcpSocket *socket = m_clientList.value(clientId); + HttpReply reply(HttpReply::Ok); + reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + reply.setPayload(QJsonDocument::fromVariant(data).toJson()); + reply.packReply(); + writeData(socket, reply.data()); } void WebServer::sendData(const QList &clients, const QVariantMap &data) { - Q_UNUSED(clients) - Q_UNUSED(data) + foreach (const QUuid &client, clients) { + QTcpSocket *socket = m_clientList.value(client); + HttpReply reply(HttpReply::Ok); + reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + reply.setPayload(QJsonDocument::fromVariant(data).toJson()); + reply.packReply(); + writeData(socket, reply.data()); + } +} - // TODO: reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); +void WebServer::sendHttpReply(const QUuid &clientId, const HttpReply &reply) +{ + QTcpSocket *socket = m_clientList.value(clientId); + writeData(socket, reply.data()); } bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName) @@ -85,7 +99,8 @@ bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName) qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "does not exist."; HttpReply reply(HttpReply::NotFound); reply.setPayload("404 Not found."); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); return false; } @@ -94,7 +109,8 @@ bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName) qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "is outside the public folder."; HttpReply reply(HttpReply::Forbidden); reply.setPayload("403 Forbidden."); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); socket->close(); return false; } @@ -104,7 +120,8 @@ bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName) qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "is not readable."; HttpReply reply(HttpReply::Forbidden); reply.setPayload("403 Forbidden. Page not readable."); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); socket->close(); return false; } @@ -157,10 +174,8 @@ void WebServer::onNewConnection() QUuid clientId = QUuid::createUuid(); m_clientList.insert(clientId, socket); - // TODO: maby check already at this point if this is a ws connection or not - connect(socket, &QTcpSocket::readyRead, this, &WebServer::readClient); - connect(socket, &QTcpSocket::disconnected, this, &WebServer::discardClient); + connect(socket, &QTcpSocket::disconnected, this, &WebServer::onDisconnected); qCDebug(dcConnection) << "Webserver client connected" << socket->peerName() << socket->peerAddress().toString() << socket->peerPort(); emit clientConnected(clientId); @@ -171,75 +186,63 @@ void WebServer::readClient() if (!m_enabled) return; - QTcpSocket* socket = static_cast(sender()); + QTcpSocket *socket = qobject_cast(sender()); + QUuid clientId = m_clientList.key(socket); - // read data - QByteArray data = socket->readAll(); + // check client + if (clientId.isNull()) { + qCWarning(dcWebServer) << "Client not recognized"; + return; + } - QStringList lines = QString(data).split("\r\n"); - QStringList tokens = QString(data).split(QRegExp("[ \r\n][ \r\n]*")); + // read http request + HttpRequest request = HttpRequest(socket->readAll()); + if (!request.isValid()) { + qCWarning(dcWebServer) << "Invalid request."; + HttpReply reply(HttpReply::BadRequest); + reply.setPayload("400 Bad Request."); + reply.packReply(); + writeData(socket, reply.data()); + return; + } // verify HTTP version - if (!lines.first().contains("HTTP/1.1")) { + if (request.httpVersion() != "HTTP/1.1") { qCWarning(dcWebServer) << "HTTP version is not supported." ; HttpReply reply(HttpReply::HttpVersionNotSupported); reply.setPayload("505 HTTP version is not supported."); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); return; } - if (tokens.isEmpty() || tokens.count() < 2) - return; - - QString methodString = tokens.at(0); - QString queryString = tokens.at(1); - - qCDebug(dcWebServer) << QString("Got request from %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); - qCDebug(dcWebServer) << "Request method:" << methodString; - qCDebug(dcWebServer) << "Request query :" << queryString; + qCDebug(dcWebServer) << QString("Got valid request from %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); + qCDebug(dcWebServer) << request; // verify method - RequestMethod requestMethod = getRequestMethodType(methodString); + RequestMethod requestMethod = getRequestMethodType(request.method()); if (requestMethod == RequestMethod::Unhandled) { - qCWarning(dcWebServer) << "method" << methodString << "not allowed"; + qCWarning(dcWebServer) << "method" << request.method() << "not allowed"; HttpReply reply(HttpReply::MethodNotAllowed); reply.setHeader(HttpReply::AllowHeader, "GET, PUT, POST, DELETE"); reply.setPayload("405 Method not allowed."); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); return; } - // TODO: authentification check - - // TODO: parse payload and header - - // TODO: verify header to make shore this is a valid HTTP request - - if (queryString.startsWith("/api/v1")) { - // TODO: check if this is an API call - qCDebug(dcWebServer) << "got api call"; - HttpReply reply(HttpReply::Ok); - reply.setPayload("Got api call. This is not implemented yet..."); - writeData(socket, reply.packReply()); + // verify query + if (request.urlQuery().query().startsWith("/api/v1")) { + emit httpRequestReady(clientId, request); return; } - if (queryString.startsWith("/ws")) { - qCDebug(dcWebServer) << "got websocket request"; - HttpReply reply(HttpReply::Ok); - reply.setPayload("Got api call. This is not implemented yet..."); - writeData(socket, reply.packReply()); - - // TODO: move the ws client to a separat websocket client list and redirect - // the notification stream to thouse clients - } - // request for a file... if (requestMethod == RequestMethod::Get) { - if (!verifyFile(socket, fileName(queryString))) + if (!verifyFile(socket, fileName(request.urlQuery().query()))) return; - QFile file(fileName(queryString)); + QFile file(fileName(request.urlQuery().query())); if (file.open(QFile::ReadOnly | QFile::Truncate)) { qCDebug(dcWebServer) << "load file" << file.fileName(); HttpReply reply(HttpReply::Ok); @@ -247,26 +250,29 @@ void WebServer::readClient() reply.setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";"); } reply.setPayload(file.readAll()); - writeData(socket, reply.packReply()); + reply.packReply(); + writeData(socket, reply.data()); return; } } - qCWarning(dcWebServer) << "Not recognized request."; + // reject everything else... + qCWarning(dcWebServer) << "Unknown message received. Respond client with 501: Not Implemented."; HttpReply reply(HttpReply::NotImplemented); - reply.setPayload("501 Not Implemented."); - writeData(socket, reply.packReply()); - return; + reply.setPayload("501 Not implemented."); + reply.packReply(); + writeData(socket, reply.data()); } -void WebServer::discardClient() +void WebServer::onDisconnected() { - QTcpSocket* socket = static_cast(sender()); + QTcpSocket* socket = qobject_cast(sender()); qCDebug(dcConnection) << "Webserver client disonnected."; // clean up QUuid clientId = m_clientList.key(socket); - m_clientList.take(clientId)->deleteLater(); + m_clientList.remove(clientId); + socket->deleteLater(); emit clientDisconnected(clientId); } @@ -278,8 +284,9 @@ bool WebServer::startServer() m_enabled = false; return false; } - m_enabled = true; qCDebug(dcConnection) << "Started webserver on" << QString("http://%1:%2").arg(m_server->serverAddress().toString()).arg(m_port); + m_enabled = true; + return true; } diff --git a/server/webserver.h b/server/webserver.h index 514ff23a..a626a5c8 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -24,18 +24,22 @@ #include #include #include +#include + #include "transportinterface.h" class QTcpServer; class QTcpSocket; -class QUuid; +class HttpRequest; +class HttpReply; -// Note: Status codes according to HTTP 1.1: https://tools.ietf.org/html/rfc7231 +// Note: Hypertext Transfer Protocol (HTTP/1.1) from the Internet Engineering Task Force (IETF): +// https://tools.ietf.org/html/rfc7231 namespace guhserver { -class WebServer : public TransportInterface +class WebServer : public TransportInterface { Q_OBJECT public: @@ -52,25 +56,31 @@ public: void sendData(const QUuid &clientId, const QVariantMap &data) override; void sendData(const QList &clients, const QVariantMap &data) override; + void sendHttpReply(const QUuid &clientId, const HttpReply &reply); private: QTcpServer *m_server; QHash m_clientList; + bool m_enabled; qint16 m_port; QDir m_webinterfaceDir; bool verifyFile(QTcpSocket *socket, const QString &fileName); + QString fileName(const QString &query); RequestMethod getRequestMethodType(const QString &methodString); void writeData(QTcpSocket *socket, const QByteArray &data); +signals: + void httpRequestReady(const QUuid &clientId, const HttpRequest &httpRequest); + private slots: void onNewConnection(); void readClient(); - void discardClient(); + void onDisconnected(); public slots: bool startServer() override; diff --git a/server/websocketserver.cpp b/server/websocketserver.cpp new file mode 100644 index 00000000..05ba7e27 --- /dev/null +++ b/server/websocketserver.cpp @@ -0,0 +1,30 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "websocketserver.h" + +namespace guhserver { + +WebSocketServer::WebSocketServer(QObject *parent) : + QObject(parent) +{ +} + +} diff --git a/server/websocketserver.h b/server/websocketserver.h new file mode 100644 index 00000000..3044f261 --- /dev/null +++ b/server/websocketserver.h @@ -0,0 +1,45 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef WEBSOCKETSERVER_H +#define WEBSOCKETSERVER_H + +#include + +// Note: WebSocket Protocol from the Internet Engineering Task Force (IETF) -> RFC6455 V13: +// http://tools.ietf.org/html/rfc6455 + +namespace guhserver { + +class WebSocketServer : public QObject +{ + Q_OBJECT +public: + explicit WebSocketServer(QObject *parent = 0); + +signals: + +public slots: + +}; + +} + +#endif // WEBSOCKETSERVER_H diff --git a/tests/auto/webserver/testwebserver.cpp b/tests/auto/webserver/testwebserver.cpp index 06d99f14..257b96e6 100644 --- a/tests/auto/webserver/testwebserver.cpp +++ b/tests/auto/webserver/testwebserver.cpp @@ -69,15 +69,13 @@ void TestWebserver::httpVersion() QSignalSpy clientSpy(socket, SIGNAL(readyRead())); - socket->write("Confusing, non HTTP protocol stuff which should not be accepted."); + socket->write("GET /hello/guh HTTP/1.0\r\n\r\n"); bool filesWritten = socket->waitForBytesWritten(500); QVERIFY2(filesWritten, "could not write to webserver."); clientSpy.wait(500); QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver"); - QByteArray data = socket->readAll(); - QVERIFY2(!data.isEmpty(), "got no response"); QStringList lines = QString(data).split("\r\n");