added tests
added httpreply first working version of webserver
This commit is contained in:
parent
f1dd14527e
commit
27a8db73d8
@ -3,6 +3,10 @@ port=12345
|
||||
interfaces="lo","all"
|
||||
ip="IPv4", "IPv6"
|
||||
|
||||
[Webserver]
|
||||
port=3001
|
||||
publicFolder=/usr/share/guh-webinterface/
|
||||
|
||||
[GPIO]
|
||||
rf433rx=27
|
||||
rf433tx=22
|
||||
|
||||
7
guh.pri
7
guh.pri
@ -1,10 +1,13 @@
|
||||
# Parse and export GUH_VERSION_STRING
|
||||
GUH_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p"')
|
||||
|
||||
# define JSON protocol version
|
||||
# define protocol versions
|
||||
JSON_PROTOCOL_VERSION=28
|
||||
REST_API_VERSION=1
|
||||
|
||||
DEFINES += GUH_VERSION_STRING=\\\"$${GUH_VERSION_STRING}\\\" JSON_PROTOCOL_VERSION=\\\"$${JSON_PROTOCOL_VERSION}\\\"
|
||||
DEFINES += GUH_VERSION_STRING=\\\"$${GUH_VERSION_STRING}\\\" \
|
||||
JSON_PROTOCOL_VERSION=\\\"$${JSON_PROTOCOL_VERSION}\\\" \
|
||||
REST_API_VERSION=\\\"$${REST_API_VERSION}\\\"
|
||||
|
||||
QT+= network
|
||||
|
||||
|
||||
4
guh.pro
4
guh.pro
@ -25,7 +25,9 @@ test.commands = LD_LIBRARY_PATH=$$top_builddir/libguh make check
|
||||
|
||||
QMAKE_EXTRA_TARGETS += licensecheck doc test
|
||||
|
||||
message("Building guh version $${GUH_VERSION_STRING} (API version $${JSON_PROTOCOL_VERSION})")
|
||||
message("Building guh version $${GUH_VERSION_STRING}")
|
||||
message("JSON-RPC API version $${JSON_PROTOCOL_VERSION}")
|
||||
message("REST API version $${REST_API_VERSION}")
|
||||
|
||||
coverage {
|
||||
message("Building coverage.")
|
||||
|
||||
@ -39,6 +39,7 @@ SOURCES += plugin/device.cpp \
|
||||
types/statedescriptor.cpp \
|
||||
loggingcategories.cpp \
|
||||
guhsettings.cpp \
|
||||
network/httpreply.cpp
|
||||
|
||||
HEADERS += plugin/device.h \
|
||||
plugin/deviceclass.h \
|
||||
@ -73,4 +74,5 @@ HEADERS += plugin/device.h \
|
||||
typeutils.h \
|
||||
loggingcategories.h \
|
||||
guhsettings.h \
|
||||
network/httpreply.h
|
||||
|
||||
|
||||
186
libguh/network/httpreply.cpp
Normal file
186
libguh/network/httpreply.cpp
Normal file
@ -0,0 +1,186 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* 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 "httpreply.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QPair>
|
||||
|
||||
// Note: RFC 7231 HTTP/1.1 Semantics and Content -> http://tools.ietf.org/html/rfc7231
|
||||
|
||||
HttpReply::HttpReply(const HttpStatusCode &statusCode) :
|
||||
m_statusCode(statusCode),
|
||||
m_payload(QByteArray())
|
||||
{
|
||||
// set known headers
|
||||
//setHeader(HeaderType::ContentTypeHeader, "application/x-www-form-urlencoded; charset=\"utf-8\"");
|
||||
setHeader(HeaderType::ServerHeader, "guh/" + QByteArray(GUH_VERSION_STRING));
|
||||
setHeader(HeaderType::UserAgentHeader, "guh/" + QByteArray(REST_API_VERSION));
|
||||
setHeader(HeaderType::DateHeader, QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8() + " GMT");
|
||||
setHeader(HeaderType::CacheControlHeader, "no-cache");
|
||||
}
|
||||
|
||||
void HttpReply::setHttpStatusCode(const HttpReply::HttpStatusCode &statusCode)
|
||||
{
|
||||
m_statusCode = statusCode;
|
||||
}
|
||||
|
||||
HttpReply::HttpStatusCode HttpReply::httpStatusCode() const
|
||||
{
|
||||
return m_statusCode;
|
||||
}
|
||||
|
||||
void HttpReply::setPayload(const QByteArray &data)
|
||||
{
|
||||
m_payload = data;
|
||||
setHeader(HeaderType::ContentLenghtHeader, QByteArray::number(data.length()));
|
||||
}
|
||||
|
||||
QByteArray HttpReply::payload() const
|
||||
{
|
||||
return m_payload;
|
||||
}
|
||||
|
||||
void HttpReply::setRawHeader(const QByteArray headerType, const QByteArray &value)
|
||||
{
|
||||
// if the header is already set, overwrite it
|
||||
if (m_rawHeaderList.keys().contains(headerType)) {
|
||||
m_rawHeaderList.remove(headerType);
|
||||
}
|
||||
|
||||
m_rawHeaderList.insert(headerType, value);
|
||||
}
|
||||
|
||||
void HttpReply::setHeader(const HttpReply::HeaderType &headerType, const QByteArray &value)
|
||||
{
|
||||
setRawHeader(getHeaderType(headerType), value);
|
||||
}
|
||||
|
||||
QHash<QByteArray, QByteArray> HttpReply::rawHeaderList() const
|
||||
{
|
||||
return m_rawHeaderList;
|
||||
}
|
||||
|
||||
QByteArray HttpReply::rawHeader() const
|
||||
{
|
||||
return m_rawHeader;
|
||||
}
|
||||
|
||||
bool HttpReply::isValid() const
|
||||
{
|
||||
// TODO: verify if header is valid and payload is valid
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HttpReply::isEmpty() const
|
||||
{
|
||||
return m_rawHeader.isEmpty() && m_payload.isEmpty() && m_rawHeaderList.isEmpty();
|
||||
}
|
||||
|
||||
void HttpReply::clear()
|
||||
{
|
||||
m_rawHeader.clear();
|
||||
m_payload.clear();
|
||||
m_rawHeaderList.clear();
|
||||
}
|
||||
|
||||
QByteArray HttpReply::packReply()
|
||||
{
|
||||
// set status code
|
||||
m_rawHeader.clear();
|
||||
m_rawHeader.append("HTTP/1.1 " + QByteArray::number(m_statusCode) + " " + getHttpReasonPhrase(m_statusCode) + "\r\n");
|
||||
|
||||
// write header
|
||||
foreach (const QByteArray &headerName, m_rawHeaderList.keys()) {
|
||||
m_rawHeader.append(headerName + ": " + m_rawHeaderList.value(headerName) + "\r\n" );
|
||||
}
|
||||
|
||||
m_rawHeader.append("\r\n");
|
||||
m_rawHeader.append(m_payload);
|
||||
return m_rawHeader;
|
||||
}
|
||||
|
||||
QByteArray HttpReply::getHttpReasonPhrase(const HttpReply::HttpStatusCode &statusCode)
|
||||
{
|
||||
switch (statusCode) {
|
||||
case HttpStatusCode::Ok:
|
||||
return "Ok";
|
||||
case Created:
|
||||
return "Created";
|
||||
case Accepted:
|
||||
return "Accepted";
|
||||
case NoContent:
|
||||
return "No Content";
|
||||
case Found:
|
||||
return "Found";
|
||||
case BadRequest:
|
||||
return "Bad Request";
|
||||
case Forbidden:
|
||||
return "Forbidden";
|
||||
case NotFound:
|
||||
return "NotFound";
|
||||
case MethodNotAllowed:
|
||||
return "Method Not Allowed";
|
||||
case RequestTimeout:
|
||||
return "Request Timeout";
|
||||
case Conflict:
|
||||
return "Conflict";
|
||||
case InternalServerError:
|
||||
return "Internal Server Error";
|
||||
case NotImplemented:
|
||||
return "Not Implemented";
|
||||
case BadGateway:
|
||||
return "Bad Gateway";
|
||||
case ServiceUnavailable:
|
||||
return "Service Unavailable";
|
||||
case GatewayTimeout:
|
||||
return "Gateway Timeout";
|
||||
case HttpVersionNotSupported:
|
||||
return "HTTP Version Not Supported";
|
||||
default:
|
||||
return QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray HttpReply::getHeaderType(const HttpReply::HeaderType &headerType)
|
||||
{
|
||||
switch (headerType) {
|
||||
case ContentTypeHeader:
|
||||
return "Content-Type";
|
||||
case ContentLenghtHeader:
|
||||
return "Content-Length";
|
||||
case CacheControlHeader:
|
||||
return "Cache-Control";
|
||||
case LocationHeader:
|
||||
return "Location";
|
||||
case ConnectionHeader:
|
||||
return "Connection";
|
||||
case UserAgentHeader:
|
||||
return "User-Agent";
|
||||
case AllowHeader:
|
||||
return "Allow";
|
||||
case DateHeader:
|
||||
return "Date";
|
||||
case ServerHeader:
|
||||
return "Server";
|
||||
default:
|
||||
return QByteArray();
|
||||
}
|
||||
}
|
||||
98
libguh/network/httpreply.h
Normal file
98
libguh/network/httpreply.h
Normal file
@ -0,0 +1,98 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* 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 HTTPREPLY_H
|
||||
#define HTTPREPLY_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
|
||||
// Note: RFC 7231 HTTP/1.1 Semantics and Content -> http://tools.ietf.org/html/rfc7231
|
||||
|
||||
class HttpReply
|
||||
{
|
||||
public:
|
||||
|
||||
enum HttpStatusCode {
|
||||
Ok = 200,
|
||||
Created = 201,
|
||||
Accepted = 202,
|
||||
NoContent = 204,
|
||||
Found = 302,
|
||||
BadRequest = 400,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
MethodNotAllowed = 405,
|
||||
RequestTimeout = 408,
|
||||
Conflict = 409,
|
||||
InternalServerError = 500,
|
||||
NotImplemented = 501,
|
||||
BadGateway = 502,
|
||||
ServiceUnavailable = 503,
|
||||
GatewayTimeout = 504,
|
||||
HttpVersionNotSupported = 505
|
||||
};
|
||||
|
||||
enum HeaderType {
|
||||
ContentTypeHeader,
|
||||
ContentLenghtHeader,
|
||||
ConnectionHeader,
|
||||
LocationHeader,
|
||||
UserAgentHeader,
|
||||
CacheControlHeader,
|
||||
AllowHeader,
|
||||
DateHeader,
|
||||
ServerHeader
|
||||
};
|
||||
|
||||
explicit HttpReply(const HttpStatusCode &statusCode);
|
||||
|
||||
void setHttpStatusCode(const HttpStatusCode &statusCode);
|
||||
HttpStatusCode httpStatusCode() const;
|
||||
|
||||
void setPayload(const QByteArray &data);
|
||||
QByteArray payload() const;
|
||||
|
||||
void setRawHeader(const QByteArray headerType, const QByteArray &value);
|
||||
void setHeader(const HeaderType &headerType, const QByteArray &value);
|
||||
QHash<QByteArray, QByteArray> rawHeaderList() const;
|
||||
QByteArray rawHeader() const;
|
||||
|
||||
bool isValid() const;
|
||||
bool isEmpty() const;
|
||||
|
||||
void clear();
|
||||
|
||||
QByteArray packReply();
|
||||
|
||||
private:
|
||||
HttpStatusCode m_statusCode;
|
||||
QByteArray m_rawHeader;
|
||||
QByteArray m_payload;
|
||||
|
||||
QHash<QByteArray, QByteArray> m_rawHeaderList;
|
||||
|
||||
QByteArray getHttpReasonPhrase(const HttpStatusCode &statusCode);
|
||||
QByteArray getHeaderType(const HeaderType &headerType);
|
||||
|
||||
QByteArray packHeader() const;
|
||||
};
|
||||
|
||||
#endif // HTTPREPLY_H
|
||||
@ -419,6 +419,9 @@ GuhCore::GuhCore(QObject *parent) :
|
||||
qCDebug(dcApplication) << "Starting JSON RPC Server";
|
||||
m_jsonServer = new JsonRPCServer(this);
|
||||
|
||||
qCDebug(dcApplication) << "Starting REST Webserver";
|
||||
m_webServer = new WebServer(this);
|
||||
|
||||
connect(m_deviceManager, &DeviceManager::eventTriggered, this, &GuhCore::gotEvent);
|
||||
connect(m_deviceManager, &DeviceManager::deviceStateChanged, this, &GuhCore::deviceStateChanged);
|
||||
connect(m_deviceManager, &DeviceManager::deviceAdded, this, &GuhCore::deviceAdded);
|
||||
@ -435,6 +438,7 @@ 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
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
|
||||
#include "devicemanager.h"
|
||||
#include "ruleengine.h"
|
||||
#include "webserver.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QDebug>
|
||||
@ -122,6 +123,7 @@ private:
|
||||
static GuhCore *s_instance;
|
||||
RunningMode m_runningMode;
|
||||
|
||||
WebServer *m_webServer;
|
||||
JsonRPCServer *m_jsonServer;
|
||||
DeviceManager *m_deviceManager;
|
||||
RuleEngine *m_ruleEngine;
|
||||
|
||||
@ -39,8 +39,8 @@ class TcpServer : public TransportInterface
|
||||
public:
|
||||
explicit TcpServer(QObject *parent = 0);
|
||||
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data);
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data);
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data) override;
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data) override;
|
||||
|
||||
private:
|
||||
QTimer *m_timer;
|
||||
@ -67,8 +67,8 @@ private slots:
|
||||
void onTimeout();
|
||||
|
||||
public slots:
|
||||
bool startServer();
|
||||
bool stopServer();
|
||||
bool startServer() override;
|
||||
bool stopServer() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ class TransportInterface : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TransportInterface(QObject *parent = 0);
|
||||
virtual ~TransportInterface() = default;
|
||||
virtual void sendData(const QUuid &clientId, const QVariantMap &data) = 0;
|
||||
virtual void sendData(const QList<QUuid> &clients, const QVariantMap &data) = 0;
|
||||
|
||||
|
||||
@ -20,10 +20,15 @@
|
||||
|
||||
#include "webserver.h"
|
||||
#include "loggingcategories.h"
|
||||
#include "guhsettings.h"
|
||||
#include "network/httpreply.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QUrlQuery>
|
||||
#include <QUuid>
|
||||
#include <QUrl>
|
||||
#include <QFile>
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
@ -33,6 +38,19 @@ WebServer::WebServer(QObject *parent) :
|
||||
{
|
||||
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_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();
|
||||
|
||||
connect(m_server, &QTcpServer::newConnection, this, &WebServer::onNewConnection);
|
||||
}
|
||||
|
||||
@ -41,48 +59,216 @@ WebServer::~WebServer()
|
||||
m_server->close();
|
||||
}
|
||||
|
||||
void WebServer::sendData(const QUuid &clientId, const QByteArray &data)
|
||||
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\";");
|
||||
|
||||
}
|
||||
|
||||
void WebServer::sendData(const QList<QUuid> &clients, const QByteArray &data)
|
||||
void WebServer::sendData(const QList<QUuid> &clients, const QVariantMap &data)
|
||||
{
|
||||
Q_UNUSED(clients)
|
||||
Q_UNUSED(data)
|
||||
|
||||
// TODO: reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";");
|
||||
}
|
||||
|
||||
QString WebServer::createContentHeader()
|
||||
bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName)
|
||||
{
|
||||
QString contentHeader(
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: application/json; charset=\"utf-8\"\r\n"
|
||||
"\r\n"
|
||||
);
|
||||
return contentHeader;
|
||||
QFileInfo checkFile(fileName);
|
||||
|
||||
// make shore the file exists
|
||||
if (!checkFile.exists()) {
|
||||
qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "does not exist.";
|
||||
HttpReply reply(HttpReply::NotFound);
|
||||
reply.setPayload("404 Not found.");
|
||||
writeData(socket, reply.packReply());
|
||||
return false;
|
||||
}
|
||||
|
||||
// make shore the file is in the public directory
|
||||
if (!checkFile.canonicalFilePath().startsWith(m_webinterfaceDir.path())) {
|
||||
qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "is outside the public folder.";
|
||||
HttpReply reply(HttpReply::Forbidden);
|
||||
reply.setPayload("403 Forbidden.");
|
||||
writeData(socket, reply.packReply());
|
||||
socket->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// make shore we can read the file
|
||||
if (!checkFile.isReadable()) {
|
||||
qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "is not readable.";
|
||||
HttpReply reply(HttpReply::Forbidden);
|
||||
reply.setPayload("403 Forbidden. Page not readable.");
|
||||
writeData(socket, reply.packReply());
|
||||
socket->close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString WebServer::fileName(const QString &query)
|
||||
{
|
||||
QString fileName;
|
||||
if (query.isEmpty() || query == "/") {
|
||||
fileName = "/index.html";
|
||||
} else {
|
||||
fileName = query;
|
||||
}
|
||||
|
||||
return QFileInfo(m_webinterfaceDir.path() + fileName).canonicalFilePath();
|
||||
}
|
||||
|
||||
|
||||
WebServer::RequestMethod WebServer::getRequestMethodType(const QString &methodString)
|
||||
{
|
||||
if (methodString == "GET") {
|
||||
return RequestMethod::Get;
|
||||
} else if (methodString == "POST") {
|
||||
return RequestMethod::Post;
|
||||
} else if (methodString == "PUT") {
|
||||
return RequestMethod::Put;
|
||||
} else if (methodString == "DELETE") {
|
||||
return RequestMethod::Delete;
|
||||
}
|
||||
return RequestMethod::Unhandled;
|
||||
}
|
||||
|
||||
void WebServer::writeData(QTcpSocket *socket, const QByteArray &data)
|
||||
{
|
||||
QTextStream os(socket);
|
||||
os.setAutoDetectUnicode(true);
|
||||
os << data;
|
||||
socket->close();
|
||||
}
|
||||
|
||||
void WebServer::onNewConnection()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
QTcpSocket* socket = m_server->nextPendingConnection();
|
||||
|
||||
// append the new client to the client list
|
||||
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);
|
||||
qCDebug(dcConnection) << "Webserver client connected" << socket->peerName() << socket->peerAddress().toString() << socket->peerPort();
|
||||
|
||||
emit clientConnected(clientId);
|
||||
}
|
||||
|
||||
void WebServer::readClient()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
QTcpSocket* socket = static_cast<QTcpSocket *>(sender());
|
||||
|
||||
// read data
|
||||
QByteArray data = socket->readAll();
|
||||
|
||||
QStringList lines = QString(data).split("\r\n");
|
||||
QStringList tokens = QString(data).split(QRegExp("[ \r\n][ \r\n]*"));
|
||||
|
||||
// verify HTTP version
|
||||
if (!lines.first().contains("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());
|
||||
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;
|
||||
|
||||
// verify method
|
||||
RequestMethod requestMethod = getRequestMethodType(methodString);
|
||||
if (requestMethod == RequestMethod::Unhandled) {
|
||||
qCWarning(dcWebServer) << "method" << methodString << "not allowed";
|
||||
HttpReply reply(HttpReply::MethodNotAllowed);
|
||||
reply.setHeader(HttpReply::AllowHeader, "GET, PUT, POST, DELETE");
|
||||
reply.setPayload("405 Method not allowed.");
|
||||
writeData(socket, reply.packReply());
|
||||
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());
|
||||
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)))
|
||||
return;
|
||||
|
||||
QFile file(fileName(queryString));
|
||||
if (file.open(QFile::ReadOnly | QFile::Truncate)) {
|
||||
qCDebug(dcWebServer) << "load file" << file.fileName();
|
||||
HttpReply reply(HttpReply::Ok);
|
||||
if (file.fileName().endsWith(".html")) {
|
||||
reply.setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";");
|
||||
}
|
||||
reply.setPayload(file.readAll());
|
||||
writeData(socket, reply.packReply());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qCWarning(dcWebServer) << "Not recognized request.";
|
||||
HttpReply reply(HttpReply::NotImplemented);
|
||||
reply.setPayload("501 Not Implemented.");
|
||||
writeData(socket, reply.packReply());
|
||||
return;
|
||||
}
|
||||
|
||||
void WebServer::discardClient()
|
||||
{
|
||||
QTcpSocket* socket = static_cast<QTcpSocket *>(sender());
|
||||
qCDebug(dcConnection) << "Webserver client disonnected" << socket->peerName() << socket->peerAddress().toString() << socket->peerPort();
|
||||
qCDebug(dcConnection) << "Webserver client disonnected.";
|
||||
|
||||
// clean up
|
||||
QUuid clientId = m_clientList.key(socket);
|
||||
m_clientList.take(clientId)->deleteLater();
|
||||
|
||||
emit clientDisconnected(clientId);
|
||||
}
|
||||
|
||||
bool WebServer::startServer()
|
||||
@ -92,13 +278,15 @@ bool WebServer::startServer()
|
||||
m_enabled = false;
|
||||
return false;
|
||||
}
|
||||
qCDebug(dcConnection) << "Started webserver on" << m_server->serverAddress().toString() << m_port;
|
||||
m_enabled = true;
|
||||
qCDebug(dcConnection) << "Started webserver on" << QString("http://%1:%2").arg(m_server->serverAddress().toString()).arg(m_port);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebServer::stopServer()
|
||||
{
|
||||
m_server->close();
|
||||
m_enabled = false;
|
||||
qCDebug(dcConnection) << "Webserver closed.";
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QDir>
|
||||
|
||||
#include "transportinterface.h"
|
||||
|
||||
@ -30,27 +31,41 @@ class QTcpServer;
|
||||
class QTcpSocket;
|
||||
class QUuid;
|
||||
|
||||
// Note: Status codes according to HTTP 1.1: https://tools.ietf.org/html/rfc7231
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
class WebServer : public TransportInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum RequestMethod {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Unhandled
|
||||
};
|
||||
|
||||
explicit WebServer(QObject *parent = 0);
|
||||
~WebServer();
|
||||
void sendData(const QUuid &clientId, const QByteArray &data);
|
||||
void sendData(const QList<QUuid> &clients, const QByteArray &data);
|
||||
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data) override;
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data) override;
|
||||
|
||||
private:
|
||||
QTcpServer *m_server;
|
||||
QHash<QUuid, QTcpSocket *> m_clientList;
|
||||
|
||||
QHash<QUuid, QTcpSocket *> m_clientList;
|
||||
bool m_enabled;
|
||||
qint16 m_port;
|
||||
QDir m_webinterfaceDir;
|
||||
|
||||
QString createContentHeader();
|
||||
bool verifyFile(QTcpSocket *socket, const QString &fileName);
|
||||
QString fileName(const QString &query);
|
||||
RequestMethod getRequestMethodType(const QString &methodString);
|
||||
|
||||
signals:
|
||||
void writeData(QTcpSocket *socket, const QByteArray &data);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
@ -58,8 +73,8 @@ private slots:
|
||||
void discardClient();
|
||||
|
||||
public slots:
|
||||
bool startServer();
|
||||
bool stopServer();
|
||||
bool startServer() override;
|
||||
bool stopServer() override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
TEMPLATE=subdirs
|
||||
SUBDIRS=versioning devices jsonrpc events states actions rules plugins
|
||||
SUBDIRS=versioning devices jsonrpc events states actions rules plugins webserver
|
||||
|
||||
@ -38,8 +38,8 @@ public:
|
||||
explicit MockTcpServer(QObject *parent = 0);
|
||||
~MockTcpServer();
|
||||
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data);
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data);
|
||||
void sendData(const QUuid &clientId, const QVariantMap &data) override;
|
||||
void sendData(const QList<QUuid> &clients, const QVariantMap &data) override;
|
||||
|
||||
/************** Used for testing **************************/
|
||||
static QList<MockTcpServer*> servers();
|
||||
@ -53,8 +53,8 @@ public:
|
||||
void sendErrorResponse(const QUuid &clientId, int commandId, const QString &error);
|
||||
|
||||
public slots:
|
||||
bool startServer();
|
||||
bool stopServer();
|
||||
bool startServer() override;
|
||||
bool stopServer() override;
|
||||
|
||||
private:
|
||||
static QList<MockTcpServer*> s_allServers;
|
||||
|
||||
166
tests/auto/webserver/testwebserver.cpp
Normal file
166
tests/auto/webserver/testwebserver.cpp
Normal file
@ -0,0 +1,166 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* 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 <QTcpSocket>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QCoreApplication>
|
||||
#include <QMetaType>
|
||||
|
||||
using namespace guhserver;
|
||||
|
||||
class TestWebserver: public GuhTestBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void pingServer();
|
||||
void httpVersion();
|
||||
|
||||
void checkAllowedMethodCall_data();
|
||||
void checkAllowedMethodCall();
|
||||
|
||||
|
||||
|
||||
private:
|
||||
// for debugging
|
||||
void printResponse(QNetworkReply *reply);
|
||||
|
||||
};
|
||||
|
||||
void TestWebserver::pingServer()
|
||||
{
|
||||
// TODO: when QWebsocket will be used
|
||||
}
|
||||
|
||||
void TestWebserver::httpVersion()
|
||||
{
|
||||
QTcpSocket *socket = new QTcpSocket(this);
|
||||
socket->connectToHost(QHostAddress("127.0.0.1"), 3000);
|
||||
bool connected = socket->waitForConnected(1000);
|
||||
QVERIFY2(connected, "could not connect to webserver.");
|
||||
|
||||
QSignalSpy clientSpy(socket, SIGNAL(readyRead()));
|
||||
|
||||
socket->write("Confusing, non HTTP protocol stuff which should not be accepted.");
|
||||
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");
|
||||
QStringList firstLineTokens = lines.first().split(QRegExp("[ \r\n][ \r\n]*"));
|
||||
|
||||
QVERIFY2(firstLineTokens.isEmpty() || firstLineTokens.count() > 2, "could not get tokens of first line");
|
||||
|
||||
bool ok = false;
|
||||
int statusCode = firstLineTokens.at(1).toInt(&ok);
|
||||
QVERIFY2(ok, "Could not convert statuscode from response to int");
|
||||
QCOMPARE(statusCode, 505);
|
||||
|
||||
socket->close();
|
||||
socket->deleteLater();
|
||||
}
|
||||
|
||||
void TestWebserver::checkAllowedMethodCall_data()
|
||||
{
|
||||
QTest::addColumn<QString>("method");
|
||||
QTest::addColumn<int>("expectedStatusCode");
|
||||
|
||||
QTest::newRow("GET") << "GET" << 200;
|
||||
QTest::newRow("PUT") << "PUT" << 200;
|
||||
QTest::newRow("POST") << "POST" << 200;
|
||||
QTest::newRow("DELETE") << "DELETE" << 200;
|
||||
QTest::newRow("HEAD") << "HEAD" << 405;
|
||||
QTest::newRow("CONNECT") << "CONNECT" << 405;
|
||||
QTest::newRow("OPTIONS") << "OPTIONS" << 405;
|
||||
QTest::newRow("TRACE") << "TRACE" << 405;
|
||||
}
|
||||
|
||||
void TestWebserver::checkAllowedMethodCall()
|
||||
{
|
||||
QFETCH(QString, method);
|
||||
QFETCH(int, expectedStatusCode);
|
||||
|
||||
|
||||
QNetworkAccessManager *nam = new QNetworkAccessManager(this);
|
||||
QSignalSpy clientSpy(nam, SIGNAL(finished(QNetworkReply*)));
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setUrl(QUrl("http://localhost:3000"));
|
||||
QNetworkReply *reply;
|
||||
|
||||
if (method == "GET") {
|
||||
reply = nam->get(request);
|
||||
} else if(method == "PUT") {
|
||||
reply = nam->put(request, QByteArray("Hello guh!"));
|
||||
} else if(method == "POST") {
|
||||
reply = nam->post(request, QByteArray("Hello guh!"));
|
||||
} else if(method == "DELETE") {
|
||||
reply = nam->deleteResource(request);
|
||||
} else if(method == "HEAD") {
|
||||
reply = nam->head(request);
|
||||
} else if(method == "CONNECT") {
|
||||
reply = nam->sendCustomRequest(request, "CONNECT");
|
||||
} else if(method == "OPTIONS") {
|
||||
reply = nam->sendCustomRequest(request, "OPTIONS");
|
||||
} else if(method == "TRACE") {
|
||||
reply = nam->sendCustomRequest(request, "TRACE");
|
||||
}
|
||||
|
||||
clientSpy.wait(200);
|
||||
QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver");
|
||||
if (expectedStatusCode == 405){
|
||||
QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), expectedStatusCode);
|
||||
QVERIFY2(reply->hasRawHeader("Allow"), "405 should contain the allowed methods header");
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void TestWebserver::printResponse(QNetworkReply *reply)
|
||||
{
|
||||
qDebug() << "-------------------------------";
|
||||
qDebug() << "Response header:";
|
||||
foreach (const QNetworkReply::RawHeaderPair &headerPair, reply->rawHeaderPairs()) {
|
||||
qDebug() << headerPair.first << ":" << headerPair.second;
|
||||
}
|
||||
qDebug() << "-------------------------------";
|
||||
qDebug() << "Response payload";
|
||||
qDebug() << reply->readAll();
|
||||
qDebug() << "-------------------------------";
|
||||
}
|
||||
|
||||
#include "testwebserver.moc"
|
||||
QTEST_MAIN(TestWebserver)
|
||||
5
tests/auto/webserver/webserver.pro
Normal file
5
tests/auto/webserver/webserver.pro
Normal file
@ -0,0 +1,5 @@
|
||||
include(../../../guh.pri)
|
||||
include(../autotests.pri)
|
||||
|
||||
TARGET = webserver
|
||||
SOURCES += testwebserver.cpp
|
||||
Reference in New Issue
Block a user