first working REST call

add httpreply and httprequest to lib
This commit is contained in:
Simon Stürz 2015-07-25 13:46:44 +02:00 committed by Michael Zanetti
parent 478c832ec8
commit 5a3c7a6cfb
26 changed files with 728 additions and 111 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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<QByteArray, QByteArray> m_rawHeaderList;

View File

@ -0,0 +1,129 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 "httprequest.h"
#include "loggingcategories.h"
#include <QUrlQuery>
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<QByteArray, QByteArray> 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;
}

View File

@ -0,0 +1,62 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef HTTPREQUEST_H
#define HTTPREQUEST_H
#include <QByteArray>
#include <QUrlQuery>
#include <QString>
#include <QHash>
class HttpRequest
{
public:
explicit HttpRequest(QByteArray rawData);
QByteArray rawHeader() const;
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> 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

View File

@ -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)

View File

@ -30,7 +30,7 @@
#include "devicemanager.h"
#include "ruleengine.h"
#include "webserver.h"
#include "servermanager.h"
#include <QObject>
#include <QDebug>
@ -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;

View File

@ -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<bool, QString> 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 &params)
notification.insert("notification", handler->name() + "." + method.name());
notification.insert("params", params);
emit notificationDataReady(notification);
m_tcpServer->sendData(m_clients.keys(true), notification);
}

View File

@ -58,6 +58,7 @@ public:
signals:
void commandReceived(const QString &targetNamespace, const QString &command, const QVariantMap &params);
void notificationDataReady(const QVariantMap &notification);
private slots:
void setup();

136
server/rest/restserver.cpp Normal file
View File

@ -0,0 +1,136 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 "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 <QJsonDocument>
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<bool, QString> 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<JsonReply*>(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();
}
}

59
server/rest/restserver.h Normal file
View File

@ -0,0 +1,59 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef RESTSERVER_H
#define RESTSERVER_H
#include <QObject>
#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<QUuid> m_clientList;
QHash<QUuid, JsonReply *> 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

View File

@ -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 \

View File

@ -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 {

45
server/servermanager.cpp Normal file
View File

@ -0,0 +1,45 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 "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;
}
}

51
server/servermanager.h Normal file
View File

@ -0,0 +1,51 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef SERVERMANAGER_H
#define SERVERMANAGER_H
#include <QObject>
#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

View File

@ -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<QTcpServer*>(sender());
@ -185,7 +185,6 @@ void TcpServer::newClientConnected()
emit clientConnected(clientId);
}
void TcpServer::readPackage()
{
QTcpSocket *client = qobject_cast<QTcpSocket*>(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;

View File

@ -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();

View File

@ -27,4 +27,8 @@ TransportInterface::TransportInterface(QObject *parent) :
{
}
TransportInterface::~TransportInterface()
{
}
}

View File

@ -22,6 +22,9 @@
#define TRANSPORTINTERFACE_H
#include <QObject>
#include <QString>
#include <QList>
#include <QUuid>
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<QUuid> &clients, const QVariantMap &data) = 0;

View File

@ -22,7 +22,9 @@
#include "loggingcategories.h"
#include "guhsettings.h"
#include "network/httpreply.h"
#include "network/httprequest.h"
#include <QJsonDocument>
#include <QTcpServer>
#include <QTcpSocket>
#include <QUrlQuery>
@ -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<QUuid> &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<QTcpSocket *>(sender());
QTcpSocket *socket = qobject_cast<QTcpSocket *>(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<QTcpSocket *>(sender());
QTcpSocket* socket = qobject_cast<QTcpSocket *>(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;
}

View File

@ -24,18 +24,22 @@
#include <QObject>
#include <QHash>
#include <QDir>
#include <QUuid>
#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<QUuid> &clients, const QVariantMap &data) override;
void sendHttpReply(const QUuid &clientId, const HttpReply &reply);
private:
QTcpServer *m_server;
QHash<QUuid, QTcpSocket *> 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;

View File

@ -0,0 +1,30 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 "websocketserver.h"
namespace guhserver {
WebSocketServer::WebSocketServer(QObject *parent) :
QObject(parent)
{
}
}

45
server/websocketserver.h Normal file
View File

@ -0,0 +1,45 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef WEBSOCKETSERVER_H
#define WEBSOCKETSERVER_H
#include <QObject>
// 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

View File

@ -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");