From 27a8db73d852b7121b91654e88da9152f45d0704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 17 Jul 2015 12:47:10 +0200 Subject: [PATCH] added tests added httpreply first working version of webserver --- data/config/guhd.conf | 4 + guh.pri | 7 +- guh.pro | 4 +- libguh/libguh.pro | 2 + libguh/network/httpreply.cpp | 186 ++++++++++++++++++++++ libguh/network/httpreply.h | 98 ++++++++++++ server/guhcore.cpp | 4 + server/guhcore.h | 2 + server/tcpserver.h | 8 +- server/transportinterface.h | 1 + server/webserver.cpp | 210 +++++++++++++++++++++++-- server/webserver.h | 29 +++- tests/auto/auto.pro | 2 +- tests/auto/mocktcpserver.h | 8 +- tests/auto/webserver/testwebserver.cpp | 166 +++++++++++++++++++ tests/auto/webserver/webserver.pro | 5 + 16 files changed, 706 insertions(+), 30 deletions(-) create mode 100644 libguh/network/httpreply.cpp create mode 100644 libguh/network/httpreply.h create mode 100644 tests/auto/webserver/testwebserver.cpp create mode 100644 tests/auto/webserver/webserver.pro diff --git a/data/config/guhd.conf b/data/config/guhd.conf index 8aa548e0..38c23999 100644 --- a/data/config/guhd.conf +++ b/data/config/guhd.conf @@ -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 diff --git a/guh.pri b/guh.pri index dff7a0dd..2847b655 100644 --- a/guh.pri +++ b/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 diff --git a/guh.pro b/guh.pro index 78e983d2..61b7b657 100644 --- a/guh.pro +++ b/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.") diff --git a/libguh/libguh.pro b/libguh/libguh.pro index e2a51bb4..69527edd 100644 --- a/libguh/libguh.pro +++ b/libguh/libguh.pro @@ -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 diff --git a/libguh/network/httpreply.cpp b/libguh/network/httpreply.cpp new file mode 100644 index 00000000..856cea86 --- /dev/null +++ b/libguh/network/httpreply.cpp @@ -0,0 +1,186 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 "httpreply.h" + +#include +#include + +// 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 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(); + } +} diff --git a/libguh/network/httpreply.h b/libguh/network/httpreply.h new file mode 100644 index 00000000..7513f257 --- /dev/null +++ b/libguh/network/httpreply.h @@ -0,0 +1,98 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 HTTPREPLY_H +#define HTTPREPLY_H + +#include +#include + +// 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 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 m_rawHeaderList; + + QByteArray getHttpReasonPhrase(const HttpStatusCode &statusCode); + QByteArray getHeaderType(const HeaderType &headerType); + + QByteArray packHeader() const; +}; + +#endif // HTTPREPLY_H diff --git a/server/guhcore.cpp b/server/guhcore.cpp index 628b71cd..e4a3b989 100644 --- a/server/guhcore.cpp +++ b/server/guhcore.cpp @@ -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 diff --git a/server/guhcore.h b/server/guhcore.h index 1251e0d1..3744f2a4 100644 --- a/server/guhcore.h +++ b/server/guhcore.h @@ -30,6 +30,7 @@ #include "devicemanager.h" #include "ruleengine.h" +#include "webserver.h" #include #include @@ -122,6 +123,7 @@ private: static GuhCore *s_instance; RunningMode m_runningMode; + WebServer *m_webServer; JsonRPCServer *m_jsonServer; DeviceManager *m_deviceManager; RuleEngine *m_ruleEngine; diff --git a/server/tcpserver.h b/server/tcpserver.h index 4afa0a1d..515171c8 100644 --- a/server/tcpserver.h +++ b/server/tcpserver.h @@ -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 &clients, const QVariantMap &data); + void sendData(const QUuid &clientId, const QVariantMap &data) override; + void sendData(const QList &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; }; } diff --git a/server/transportinterface.h b/server/transportinterface.h index 28a352d9..92ce6afd 100644 --- a/server/transportinterface.h +++ b/server/transportinterface.h @@ -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 &clients, const QVariantMap &data) = 0; diff --git a/server/webserver.cpp b/server/webserver.cpp index a323e134..b15959a1 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -20,10 +20,15 @@ #include "webserver.h" #include "loggingcategories.h" +#include "guhsettings.h" +#include "network/httpreply.h" #include #include +#include #include +#include +#include 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 &clients, const QByteArray &data) +void WebServer::sendData(const QList &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(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(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; } diff --git a/server/webserver.h b/server/webserver.h index 7da2c589..514ff23a 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -23,6 +23,7 @@ #include #include +#include #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 &clients, const QByteArray &data); + + void sendData(const QUuid &clientId, const QVariantMap &data) override; + void sendData(const QList &clients, const QVariantMap &data) override; private: QTcpServer *m_server; - QHash m_clientList; + QHash 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; }; diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index c75c1b6e..e1682623 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -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 diff --git a/tests/auto/mocktcpserver.h b/tests/auto/mocktcpserver.h index ba20d5e8..74c38f79 100644 --- a/tests/auto/mocktcpserver.h +++ b/tests/auto/mocktcpserver.h @@ -38,8 +38,8 @@ public: explicit MockTcpServer(QObject *parent = 0); ~MockTcpServer(); - void sendData(const QUuid &clientId, const QVariantMap &data); - void sendData(const QList &clients, const QVariantMap &data); + void sendData(const QUuid &clientId, const QVariantMap &data) override; + void sendData(const QList &clients, const QVariantMap &data) override; /************** Used for testing **************************/ static QList 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 s_allServers; diff --git a/tests/auto/webserver/testwebserver.cpp b/tests/auto/webserver/testwebserver.cpp new file mode 100644 index 00000000..8989a978 --- /dev/null +++ b/tests/auto/webserver/testwebserver.cpp @@ -0,0 +1,166 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2015 Simon Stuerz * + * * + * This file is part of guh. * + * * + * Guh is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, version 2 of the License. * + * * + * Guh is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with guh. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "guhtestbase.h" +#include "guhcore.h" +#include "devicemanager.h" +#include "mocktcpserver.h" +#include "webserver.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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("method"); + QTest::addColumn("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) diff --git a/tests/auto/webserver/webserver.pro b/tests/auto/webserver/webserver.pro new file mode 100644 index 00000000..6e516980 --- /dev/null +++ b/tests/auto/webserver/webserver.pro @@ -0,0 +1,5 @@ +include(../../../guh.pri) +include(../autotests.pri) + +TARGET = webserver +SOURCES += testwebserver.cpp