added tests

added httpreply
first working version of webserver
This commit is contained in:
Simon Stürz 2015-07-17 12:47:10 +02:00 committed by Michael Zanetti
parent f1dd14527e
commit 27a8db73d8
16 changed files with 706 additions and 30 deletions

View File

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

View File

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

View File

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

View File

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

View 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();
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,5 @@
include(../../../guh.pri)
include(../autotests.pri)
TARGET = webserver
SOURCES += testwebserver.cpp