added more tests

move httprequest and reply to server
added request parsing logic
This commit is contained in:
Simon Stürz 2015-07-30 17:28:22 +02:00 committed by Michael Zanetti
parent 70a929f760
commit ea9d8d6d90
28 changed files with 753 additions and 182 deletions

View File

@ -39,8 +39,7 @@ SOURCES += plugin/device.cpp \
types/statedescriptor.cpp \
loggingcategories.cpp \
guhsettings.cpp \
network/httpreply.cpp \
network/httprequest.cpp
HEADERS += plugin/device.h \
plugin/deviceclass.h \
@ -74,6 +73,4 @@ HEADERS += plugin/device.h \
typeutils.h \
loggingcategories.h \
guhsettings.h \
network/httpreply.h \
network/httprequest.h

View File

@ -30,7 +30,6 @@
\note RFC 7231 HTTP/1.1 Semantics and Content -> \l{http://tools.ietf.org/html/rfc7231}{http://tools.ietf.org/html/rfc7231}
*/
/*! \enum HttpReply::HttpStatusCode
@ -105,6 +104,7 @@
#include <QDateTime>
#include <QPair>
#include <QDebug>
/*! Construct a HttpReply with the given \a statusCode. */
HttpReply::HttpReply(QObject *parent) :
@ -114,7 +114,10 @@ HttpReply::HttpReply(QObject *parent) :
m_payload(QByteArray()),
m_timedOut(false)
{
connect(&m_timer, &QTimer::timeout, this, &HttpReply::timedOut);
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &HttpReply::timedOut);
m_reasonPhrase = getHttpReasonPhrase(m_statusCode);
// set known headers
setHeader(HttpHeaderType::ServerHeader, "guh/" + QByteArray(GUH_VERSION_STRING));
@ -130,7 +133,10 @@ HttpReply::HttpReply(const HttpReply::HttpStatusCode &statusCode, const HttpRepl
m_payload(QByteArray()),
m_timedOut(false)
{
connect(&m_timer, &QTimer::timeout, this, &HttpReply::timedOut);
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &HttpReply::timeout);
m_reasonPhrase = getHttpReasonPhrase(m_statusCode);
// set known headers
setHeader(HttpHeaderType::ServerHeader, "guh/" + QByteArray(GUH_VERSION_STRING));
@ -152,6 +158,11 @@ HttpReply::HttpStatusCode HttpReply::httpStatusCode() const
return m_statusCode;
}
QByteArray HttpReply::httpReasonPhrase() const
{
return m_reasonPhrase;
}
/*! Returns the type of this \l{HttpReply}.
* \sa Type
*/
@ -332,11 +343,12 @@ QByteArray HttpReply::getHeaderType(const HttpReply::HttpHeaderType &headerType)
void HttpReply::startWait()
{
m_timer.start(5000);
m_timer->start(5000);
}
void HttpReply::timeout()
{
qDebug() << "Http reply timeout";
m_timedOut = true;
emit finished();
}

View File

@ -77,6 +77,8 @@ public:
void setHttpStatusCode(const HttpStatusCode &statusCode);
HttpStatusCode httpStatusCode() const;
QByteArray httpReasonPhrase() const;
Type type() const;
void setClientId(const QUuid &clientId);
@ -101,6 +103,7 @@ public:
private:
HttpStatusCode m_statusCode;
QByteArray m_reasonPhrase;
Type m_type;
QUuid m_clientId;
@ -110,20 +113,21 @@ private:
QHash<QByteArray, QByteArray> m_rawHeaderList;
QTimer m_timer;
QTimer *m_timer;
bool m_timedOut;
QByteArray getHttpReasonPhrase(const HttpStatusCode &statusCode);
QByteArray getHeaderType(const HttpHeaderType &headerType);
private slots:
void timeout();
public slots:
void startWait();
signals:
void finished();
private slots:
void timeout();
};

View File

@ -24,61 +24,19 @@
#include <QUrlQuery>
HttpRequest::HttpRequest() :
m_rawData(QByteArray()),
m_valid(false),
m_isComplete(false)
{
}
HttpRequest::HttpRequest(QByteArray rawData) :
m_rawData(rawData),
m_valid(false)
m_valid(false),
m_isComplete(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;
}
// verify http version
m_httpVersion = statusLineTokens.at(2).toUtf8().simplified();
if (!m_httpVersion.contains("HTTP")) {
qCWarning(dcWebServer) << "Unknown HTTP version:" << m_httpVersion;
return;
}
m_methodString = statusLineTokens.at(0).simplified();
m_method = getRequestMethodType(m_methodString);
m_url = QUrl("http://example.com" + statusLineTokens.at(1).simplified());
if (m_url.hasQuery())
m_urlQuery = QUrlQuery(m_url.query());
// verify headers
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);
}
// TODO: check content size
m_valid = true;
validate();
}
QByteArray HttpRequest::rawHeader() const
@ -126,11 +84,109 @@ bool HttpRequest::isValid() const
return m_valid;
}
bool HttpRequest::isComplete() const
{
return m_isComplete;
}
bool HttpRequest::hasPayload() const
{
return !m_payload.isEmpty();
}
void HttpRequest::appendData(const QByteArray &data)
{
m_rawData.append(data);
validate();
}
void HttpRequest::validate()
{
m_isComplete = true; 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):" << m_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;
}
// verify http version
m_httpVersion = statusLineTokens.at(2).toUtf8().simplified();
if (!m_httpVersion.contains("HTTP")) {
qCWarning(dcWebServer) << "Unknown HTTP version:" << m_httpVersion;
return;
}
m_methodString = statusLineTokens.at(0).simplified();
m_method = getRequestMethodType(m_methodString);
m_url = QUrl("http://example.com" + statusLineTokens.at(1).simplified());
if (m_url.hasQuery())
m_urlQuery = QUrlQuery(m_url.query());
// verify header formating
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);
}
// check User-Agent
if (!m_rawHeaderList.contains("User-Agent")) {
qWarning() << "User-Agent header is missing";
return;
}
// verify content length with actual payload
if (m_rawHeaderList.contains("Content-Length")) {
bool ok = false;
int contentLength = m_rawHeaderList.value("Content-Length").toInt(&ok);
if (!ok) {
qCWarning(dcWebServer) << "Could not parse Content-Length.";
return;
}
// check if we have all data
if (m_payload.size() < contentLength) {
qCDebug(dcWebServer) << "Request incomplete:";
qCDebug(dcWebServer) << " -> Content-Length:" << contentLength;
qCDebug(dcWebServer) << " -> Payload size :" << payload().size();
m_isComplete = false;
return;
}
// check if the content lenght bigger than header Content-Length
if (m_payload.size() > contentLength) {
qCWarning(dcWebServer) << "Payload size greater than header Content-Length:";
qCWarning(dcWebServer) << " -> Content-Length:" << contentLength;
qCWarning(dcWebServer) << " -> Payload size :" << payload().size();
m_isComplete = true;
return;
}
}
m_valid = true;
}
HttpRequest::RequestMethod HttpRequest::getRequestMethodType(const QString &methodString)
{
if (methodString == "GET") {
@ -149,7 +205,7 @@ HttpRequest::RequestMethod HttpRequest::getRequestMethodType(const QString &meth
QDebug operator<<(QDebug debug, const HttpRequest &httpRequest)
{
debug << "\n===================================" << "\n";
debug << " http version: " << httpRequest.httpVersion() << "\n";
debug << " HTTP version: " << httpRequest.httpVersion() << "\n";
debug << " method: " << httpRequest.methodString() << "\n";
debug << " URL path: " << httpRequest.url().path() << "\n";
debug << " URL query: " << httpRequest.urlQuery().query() << "\n";

View File

@ -37,7 +37,8 @@ public:
Unhandled
};
explicit HttpRequest(QByteArray rawData);
HttpRequest();
HttpRequest(QByteArray rawData);
QByteArray rawHeader() const;
QHash<QByteArray, QByteArray> rawHeaderList() const;
@ -52,8 +53,11 @@ public:
QByteArray payload() const;
bool isValid() const;
bool isComplete() const;
bool hasPayload() const;
void appendData(const QByteArray &data);
private:
QByteArray m_rawData;
QByteArray m_rawHeader;
@ -69,7 +73,9 @@ private:
QByteArray m_payload;
bool m_valid;
bool m_isComplete;
void validate();
RequestMethod getRequestMethodType(const QString &methodString);
};

View File

@ -632,7 +632,6 @@ void DeviceHandler::pairingFinished(const PairingTransactionId &pairingTransacti
reply->finished();
return;
}
m_asynDeviceAdditions.insert(deviceId, reply);
}

View File

@ -702,7 +702,11 @@ QVariantList JsonTypes::packSupportedDevices(const VendorId &vendorId)
QVariantList JsonTypes::packConfiguredDevices()
{
return packRules(GuhCore::instance()->rules());
QVariantList configuredDeviceList;
foreach (Device *device, GuhCore::instance()->configuredDevices()) {
configuredDeviceList.append(packDevice(device));
}
return configuredDeviceList;
}
QVariantList JsonTypes::packDeviceStates(Device *device)
@ -736,7 +740,7 @@ QVariantList JsonTypes::packRuleDescriptions()
return rulesList;
}
QVariantList JsonTypes::packRuleDescriptions(const QList<Rule> rules)
QVariantList JsonTypes::packRuleDescriptions(const QList<Rule> &rules)
{
QVariantList rulesList;
foreach (const Rule &rule, rules) {

View File

@ -169,7 +169,7 @@ public:
static QVariantList packDeviceDescriptors(const QList<DeviceDescriptor> deviceDescriptors);
static QVariantList packRuleDescriptions();
static QVariantList packRuleDescriptions(const QList<Rule> rules);
static QVariantList packRuleDescriptions(const QList<Rule> &rules);
static QVariantList packActionTypes(const DeviceClass &deviceClass);
static QVariantList packStateTypes(const DeviceClass &deviceClass);

View File

@ -19,7 +19,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "deviceclassesresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
#include "guhcore.h"
#include <QJsonDocument>
@ -135,19 +135,17 @@ HttpReply *DeviceClassesResource::proccessGetRequest(const HttpRequest &request,
// GET /api/v1/deviceclasses/{deviceClassId}/discover?name=paramName&value=paramValue
if (urlTokens.count() == 5 && urlTokens.at(4) == "discover") {
ParamList params;
if (request.urlQuery().hasQueryItem("params")) {
QString paramMapString = request.urlQuery().queryItemValue("params");
// FIXME: find a better way to get discovery params
ParamList paramList;
if (request.url().hasQuery()) {
if (request.urlQuery().hasQueryItem("name") && request.urlQuery().hasQueryItem("name")) {
paramList.append(Param(request.urlQuery().queryItemValue("name"), QVariant(request.urlQuery().queryItemValue("value"))));
} else {
qCWarning(dcRest) << "Invalid discovery params in" << request.urlQuery().query();
QPair<bool, QVariant> verification = verifyPayload(paramMapString.toUtf8());
if (!verification.first)
return createErrorReply(HttpReply::BadRequest);
}
params = JsonTypes::unpackParams(verification.second.toList());
}
return getDiscoverdDevices(paramList);
return getDiscoverdDevices(params);
}
return createErrorReply(HttpReply::BadRequest);
@ -230,6 +228,7 @@ HttpReply *DeviceClassesResource::getEventType(const EventTypeId &eventTypeId)
HttpReply *DeviceClassesResource::getDiscoverdDevices(const ParamList &discoveryParams)
{
qCDebug(dcRest) << "Discover devices for DeviceClass" << m_deviceClass.id();
qCDebug(dcRest) << discoveryParams;
DeviceManager::DeviceError status = GuhCore::instance()->discoverDevices(m_deviceClass.id(), discoveryParams);

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;

View File

@ -19,8 +19,8 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "devicesresource.h"
#include "network/httpreply.h"
#include "network/httprequest.h"
#include "httpreply.h"
#include "httprequest.h"
#include "jsontypes.h"
#include "guhcore.h"
@ -45,6 +45,7 @@ HttpReply *DevicesResource::proccessRequest(const HttpRequest &request, const QS
{
m_device = 0;
// get the main resource
if (urlTokens.count() >= 4) {
DeviceId deviceId = DeviceId(urlTokens.at(3));
@ -147,6 +148,7 @@ HttpReply *DevicesResource::proccessPutRequest(const HttpRequest &request, const
HttpReply *DevicesResource::proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens)
{
// POST /api/v1/devices
if (urlTokens.count() == 3)
return addConfiguredDevice(request.payload());
@ -162,6 +164,19 @@ HttpReply *DevicesResource::proccessPostRequest(const HttpRequest &request, cons
qCWarning(dcRest) << "Could not parse ActionTypeId:" << urlTokens.at(5);
return createErrorReply(HttpReply::BadRequest);
}
bool found = false;
DeviceClass deviceClass = GuhCore::instance()->findDeviceClass(m_device->deviceClassId());
foreach (const ActionType actionType, deviceClass.actionTypes()) {
if (actionType.id() == actionTypeId) {
found = true;
break;
}
}
if (!found) {
qCWarning(dcRest) << "Could not find ActionTypeId:" << actionTypeId.toString();
return createErrorReply(HttpReply::NotFound);
}
return executeAction(m_device, actionTypeId, request.payload());
}
@ -217,8 +232,6 @@ HttpReply *DevicesResource::removeDevice(Device *device) const
HttpReply *DevicesResource::executeAction(Device *device, const ActionTypeId &actionTypeId, const QByteArray &payload) const
{
qCDebug(dcRest) << "Execute action" << actionTypeId.toString();
QPair<bool, QVariant> verification = RestResource::verifyPayload(payload);
if (!verification.first)
return createErrorReply(HttpReply::BadRequest);
@ -230,6 +243,8 @@ HttpReply *DevicesResource::executeAction(Device *device, const ActionTypeId &ac
ParamList actionParams = JsonTypes::unpackParams(message.value("params").toList());
qCDebug(dcRest) << "Execute action with" << actionParams;
Action action(actionTypeId, device->id());
action.setParams(actionParams);
@ -252,33 +267,39 @@ HttpReply *DevicesResource::addConfiguredDevice(const QByteArray &payload) const
if (!verification.first)
return createErrorReply(HttpReply::BadRequest);
qCDebug(dcRest) << "Add device";
QVariantMap params = verification.second.toMap();
DeviceClassId deviceClass(params.value("deviceClassId").toString());
DeviceClassId deviceClassId(params.value("deviceClassId").toString());
if (deviceClassId.isNull())
return createErrorReply(HttpReply::BadRequest);
DeviceId newDeviceId = DeviceId::createDeviceId();
ParamList deviceParams = JsonTypes::unpackParams(params.value("deviceParams").toList());
DeviceDescriptorId deviceDescriptorId(params.value("deviceDescriptorId").toString());
DeviceId newDeviceId = DeviceId::createDeviceId();
DeviceManager::DeviceError status;
if (deviceDescriptorId.isNull()) {
qCDebug(dcRest) << "Adding device with params" << deviceParams;
status = GuhCore::instance()->addConfiguredDevice(deviceClass, deviceParams, newDeviceId);
qCDebug(dcRest) << "Adding device with " << deviceParams;
status = GuhCore::instance()->addConfiguredDevice(deviceClassId, deviceParams, newDeviceId);
} else {
qCDebug(dcRest) << "Adding discovered device";
status = GuhCore::instance()->addConfiguredDevice(deviceClass, deviceDescriptorId, newDeviceId);
status = GuhCore::instance()->addConfiguredDevice(deviceClassId, deviceDescriptorId, newDeviceId);
}
if (status == DeviceManager::DeviceErrorAsync) {
HttpReply *reply = createAsyncReply();
m_asynDeviceAdditions.insert(newDeviceId, reply);
qCDebug(dcRest) << "Device setup async reply";
m_asyncDeviceAdditions.insert(newDeviceId, reply);
return reply;
}
if (status != DeviceManager::DeviceErrorNoError)
return createErrorReply(HttpReply::InternalServerError);
return createSuccessReply();
QVariantMap result;
result.insert("deviceId", newDeviceId);
HttpReply *reply = createSuccessReply();
reply->setPayload(QJsonDocument::fromVariant(result).toJson());
return reply;
}
HttpReply *DevicesResource::editDevice(Device *device, const QByteArray &payload) const
@ -302,6 +323,7 @@ HttpReply *DevicesResource::editDevice(Device *device, const QByteArray &payload
if (status == DeviceManager::DeviceErrorAsync) {
HttpReply *reply = createAsyncReply();
qCDebug(dcRest) << "Device edit async reply";
m_asyncEditDevice.insert(device, reply);
return reply;
}
@ -319,9 +341,11 @@ void DevicesResource::actionExecuted(const ActionId &actionId, DeviceManager::De
HttpReply *reply = m_asyncActionExecutions.take(actionId);
if (status == DeviceManager::DeviceErrorNoError) {
qCDebug(dcRest) << "Action execution finished successfully";
reply->setHttpStatusCode(HttpReply::Ok);
} else {
reply->setHttpStatusCode(HttpReply::BadRequest);
qCDebug(dcRest) << "Action execution finished with error" << status;
reply->setHttpStatusCode(HttpReply::InternalServerError);
}
reply->finished();
@ -329,33 +353,36 @@ void DevicesResource::actionExecuted(const ActionId &actionId, DeviceManager::De
void DevicesResource::deviceSetupFinished(Device *device, DeviceManager::DeviceError status)
{
if (!m_asyncEditDevice.contains(device))
if (!m_asyncDeviceAdditions.contains(device->id()))
return; // Not the device we are waiting for.
HttpReply *reply = m_asyncEditDevice.take(device);
HttpReply *reply = m_asyncDeviceAdditions.take(device->id());
if (status == DeviceManager::DeviceErrorNoError) {
qCDebug(dcRest) << "Device setup finished successfully";
reply->setHttpStatusCode(HttpReply::Ok);
} else {
reply->setHttpStatusCode(HttpReply::BadRequest);
qCDebug(dcRest) << "Device setup finished with error" << status;
reply->setHttpStatusCode(HttpReply::InternalServerError);
}
QVariantMap result;
result.insert("deviceId", device->id());
reply->setPayload(QJsonDocument::fromVariant(result).toJson());
reply->finished();
}
void DevicesResource::deviceEditFinished(Device *device, DeviceManager::DeviceError status)
{
if (!m_asynDeviceAdditions.contains(device->id()))
if (!m_asyncEditDevice.contains(device))
return; // Not the device we are waiting for.
HttpReply *reply = m_asyncEditDevice.take(device);
if (status == DeviceManager::DeviceErrorNoError) {
qCDebug(dcRest) << "Device edit finished successfully";
reply->setHttpStatusCode(HttpReply::Ok);
} else {
reply->setHttpStatusCode(HttpReply::BadRequest);
qCDebug(dcRest) << "Device edit finished with error" << status;
reply->setHttpStatusCode(HttpReply::InternalServerError);
}
reply->finished();

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;
@ -44,7 +44,7 @@ public:
private:
mutable QHash<ActionId, HttpReply *> m_asyncActionExecutions;
mutable QHash<DeviceId, HttpReply *> m_asynDeviceAdditions;
mutable QHash<DeviceId, HttpReply *> m_asyncDeviceAdditions;
mutable QHash<Device *, HttpReply *> m_asyncEditDevice;
mutable QHash<QUuid, HttpReply *> m_asyncPairingRequests;

View File

@ -19,7 +19,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "logsresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
namespace guhserver {

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;

View File

@ -19,7 +19,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "pluginsresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
#include "loggingcategories.h"
#include "guhcore.h"
@ -30,6 +30,7 @@ namespace guhserver {
PluginsResource::PluginsResource(QObject *parent) :
RestResource(parent)
{
}
QString PluginsResource::name() const

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;

View File

@ -19,7 +19,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "restresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
#include "loggingcategories.h"
#include "guhcore.h"
@ -47,6 +47,7 @@ HttpReply *RestResource::createSuccessReply()
HttpReply *RestResource::createErrorReply(const HttpReply::HttpStatusCode &statusCode)
{
HttpReply *reply = new HttpReply(statusCode, HttpReply::TypeSync);
reply->setPayload(QByteArray::number(reply->httpStatusCode()) + " " + reply->httpReasonPhrase());
return reply;
}

View File

@ -24,10 +24,9 @@
#include <QObject>
#include <QPair>
#include "network/httpreply.h"
#include "httpreply.h"
#include "httprequest.h"
class HttpRequest;
class HttpReply;
class QVariant;
namespace guhserver {
@ -54,6 +53,10 @@ private:
virtual HttpReply *proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens);
virtual HttpReply *proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens);
QHash<QPair<HttpRequest::RequestMethod, QString>, QString> m_descriptions;
QHash<QString, QVariantMap> m_params;
QHash<QString, QVariantMap> m_returns;
};
}

View File

@ -20,8 +20,8 @@
#include "restserver.h"
#include "loggingcategories.h"
#include "network/httprequest.h"
#include "network/httpreply.h"
#include "httprequest.h"
#include "httpreply.h"
#include "guhcore.h"
#include <QJsonDocument>
@ -64,7 +64,6 @@ void RestServer::clientDisconnected(const QUuid &clientId)
void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &request)
{
qCDebug(dcRest) << "Process HTTP request";
qCDebug(dcRest) << request;
QStringList urlTokens = request.url().path().split("/");
urlTokens.removeAll(QString());
@ -86,8 +85,8 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re
reply->setClientId(clientId);
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &RestServer::asyncReplyFinished);
reply->startWait();
m_asyncReplies.insert(clientId, reply);
reply->startWait();
return;
}
m_webserver->sendHttpReply(reply);
@ -100,8 +99,8 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re
reply->setClientId(clientId);
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &RestServer::asyncReplyFinished);
reply->startWait();
m_asyncReplies.insert(clientId, reply);
reply->startWait();
return;
}
m_webserver->sendHttpReply(reply);
@ -114,8 +113,8 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re
reply->setClientId(clientId);
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &RestServer::asyncReplyFinished);
reply->startWait();
m_asyncReplies.insert(clientId, reply);
reply->startWait();
return;
}
m_webserver->sendHttpReply(reply);
@ -128,8 +127,8 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re
reply->setClientId(clientId);
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &RestServer::asyncReplyFinished);
reply->startWait();
m_asyncReplies.insert(clientId, reply);
reply->startWait();
return;
}
m_webserver->sendHttpReply(reply);
@ -142,8 +141,8 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re
reply->setClientId(clientId);
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &RestServer::asyncReplyFinished);
reply->startWait();
m_asyncReplies.insert(clientId, reply);
reply->startWait();
return;
}
m_webserver->sendHttpReply(reply);
@ -160,13 +159,10 @@ void RestServer::asyncReplyFinished()
{
HttpReply *reply = qobject_cast<HttpReply*>(sender());
qCDebug(dcWebServer) << "sending reply" << reply->data();
qCDebug(dcWebServer) << "Async reply finished";
if (!reply->timedOut()) {
reply->setHttpStatusCode(HttpReply::Ok);
} else {
if (reply->timedOut())
reply->setHttpStatusCode(HttpReply::GatewayTimeout);
}
m_webserver->sendHttpReply(reply);
reply->deleteLater();

View File

@ -19,7 +19,8 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "rulesresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
#include "typeutils.h"
#include "loggingcategories.h"
#include "guhcore.h"
@ -46,6 +47,10 @@ HttpReply *RulesResource::proccessRequest(const HttpRequest &request, const QStr
qCWarning(dcRest) << "Could not parse RuleId:" << urlTokens.at(3);
return createErrorReply(HttpReply::BadRequest);
}
if (GuhCore::instance()->findRule(m_ruleId).id().isNull())
return createErrorReply(HttpReply::NotFound);
}
// check method
@ -72,8 +77,16 @@ HttpReply *RulesResource::proccessGetRequest(const HttpRequest &request, const Q
Q_UNUSED(request)
// GET /api/v1/rules
if (urlTokens.count() == 3)
return getRules();
if (urlTokens.count() == 3) {
// check if we should filter for rules containing a certain device
DeviceId deviceId;
if (request.url().hasQuery() && request.urlQuery().hasQueryItem("deviceId")) {
deviceId = DeviceId(request.urlQuery().queryItemValue("deviceId"));
if (deviceId.isNull())
createErrorReply(HttpReply::BadRequest);
}
return getRules(deviceId);
}
// GET /api/v1/rules/{ruleId}
if (urlTokens.count() == 4)
@ -85,48 +98,64 @@ HttpReply *RulesResource::proccessGetRequest(const HttpRequest &request, const Q
HttpReply *RulesResource::proccessDeleteRequest(const HttpRequest &request, const QStringList &urlTokens)
{
Q_UNUSED(request)
Q_UNUSED(urlTokens)
// DELETE /api/v1/rules
if (urlTokens.count() == 3)
return createErrorReply(HttpReply::BadRequest);
// DELETE /api/v1/rules/{ruleId}
if (urlTokens.count() == 4)
return removeRule(m_ruleId);
return createErrorReply(HttpReply::NotImplemented);
}
HttpReply *RulesResource::proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens)
{
Q_UNUSED(request)
Q_UNUSED(urlTokens)
// PUT /api/v1/rules
if (urlTokens.count() == 3)
return createErrorReply(HttpReply::BadRequest);
// PUT /api/v1/rules/{ruleId}
if (urlTokens.count() == 4)
return editRule(m_ruleId, request.payload());
return createErrorReply(HttpReply::NotImplemented);
}
HttpReply *RulesResource::proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens)
{
Q_UNUSED(request)
Q_UNUSED(urlTokens)
{
// POST /api/v1/rules
if (urlTokens.count() == 3)
return addRule(request.payload());
return createErrorReply(HttpReply::NotImplemented);
}
HttpReply *RulesResource::getRules() const
HttpReply *RulesResource::getRules(const DeviceId &deviceId) const
{
qCDebug(dcRest) << "Get rule descriptions";
HttpReply *reply = createSuccessReply();
reply->setPayload(QJsonDocument::fromVariant(JsonTypes::packRuleDescriptions()).toJson());
return reply;
}
Rule RulesResource::findRule(const RuleId &ruleId) const
{
foreach (const Rule &rule, GuhCore::instance()->rules()) {
if (rule.id() == ruleId) {
return rule;
if (deviceId.isNull()) {
qCDebug(dcRest) << "Get rule descriptions";
reply->setPayload(QJsonDocument::fromVariant(JsonTypes::packRuleDescriptions()).toJson());
} else {
qCDebug(dcRest) << "Get rule descriptions which contain the device with id" << deviceId.toString();
QList<RuleId> ruleIdsList = GuhCore::instance()->findRules(deviceId);
QList<Rule> ruleList;
foreach (const RuleId &ruleId, ruleIdsList) {
Rule rule = GuhCore::instance()->findRule(ruleId);
if (!rule.id().isNull())
ruleList.append(rule);
}
reply->setPayload(QJsonDocument::fromVariant(JsonTypes::packRuleDescriptions(ruleList)).toJson());
}
return Rule();
return reply;
}
HttpReply *RulesResource::getRuleDetails(const RuleId &ruleId) const
{
Rule rule = findRule(ruleId);
Rule rule = GuhCore::instance()->findRule(ruleId);
if (rule.id().isNull())
return createErrorReply(HttpReply::NotFound);
@ -136,5 +165,34 @@ HttpReply *RulesResource::getRuleDetails(const RuleId &ruleId) const
return reply;
}
HttpReply *RulesResource::removeRule(const RuleId &ruleId) const
{
qCDebug(dcRest) << "Remove rule with id" << ruleId.toString();
RuleEngine::RuleError status = GuhCore::instance()->removeRule(ruleId);
if (status == RuleEngine::RuleErrorNoError)
return createSuccessReply();
return createErrorReply(HttpReply::InternalServerError);
}
HttpReply *RulesResource::addRule(const QByteArray &payload) const
{
Q_UNUSED(payload)
qCDebug(dcRest) << "Add new rule";
return createErrorReply(HttpReply::NotImplemented);
}
HttpReply *RulesResource::editRule(const RuleId &ruleId, const QByteArray &payload) const
{
Q_UNUSED(payload)
qCDebug(dcRest) << "Edit rule with id" << ruleId;
return createErrorReply(HttpReply::NotImplemented);
}
}

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;
@ -52,17 +52,18 @@ private:
HttpReply *proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens) override;
// Get methods
HttpReply *getRules() const;
HttpReply *getRules(const DeviceId &deviceId) const;
HttpReply *getRuleDetails(const RuleId &ruleId) const;
// Delete methods
HttpReply *removeRule(const RuleId &ruleId) const;
// Post methods
HttpReply *addRule(const QByteArray &payload) const;
// Put methods
HttpReply *editRule(const RuleId &ruleId, const QByteArray &payload) const;
Rule findRule(const RuleId &ruleId) const;
};
}

View File

@ -19,7 +19,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "vendorsresource.h"
#include "network/httprequest.h"
#include "httprequest.h"
#include "loggingcategories.h"
#include "guhcore.h"

View File

@ -26,7 +26,7 @@
#include "jsontypes.h"
#include "restresource.h"
#include "network/httpreply.h"
#include "httpreply.h"
class HttpRequest;

View File

@ -25,6 +25,8 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \
$$top_srcdir/server/transportinterface.cpp \
$$top_srcdir/server/servermanager.cpp \
$$top_srcdir/server/websocketserver.cpp \
$$top_srcdir/server/httprequest.cpp \
$$top_srcdir/server/httpreply.cpp \
$$top_srcdir/server/rest/restserver.cpp \
$$top_srcdir/server/rest/restresource.cpp \
$$top_srcdir/server/rest/devicesresource.cpp \
@ -57,6 +59,8 @@ HEADERS += $$top_srcdir/server/guhcore.h \
$$top_srcdir/server/transportinterface.h \
$$top_srcdir/server/servermanager.h \
$$top_srcdir/server/websocketserver.h \
$$top_srcdir/server/httprequest.h \
$$top_srcdir/server/httpreply.h \
$$top_srcdir/server/rest/restserver.h \
$$top_srcdir/server/rest/restresource.h \
$$top_srcdir/server/rest/devicesresource.h \

View File

@ -21,8 +21,8 @@
#include "webserver.h"
#include "loggingcategories.h"
#include "guhsettings.h"
#include "network/httpreply.h"
#include "network/httprequest.h"
#include "httpreply.h"
#include "httprequest.h"
#include <QJsonDocument>
#include <QTcpServer>
@ -98,11 +98,11 @@ void WebServer::sendHttpReply(HttpReply *reply)
bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName)
{
QFileInfo checkFile(fileName);
QFileInfo file(fileName);
// make shore the file exists
if (!checkFile.exists()) {
qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "does not exist.";
if (!file.exists()) {
qCWarning(dcWebServer) << "requested file" << file.fileName() << "does not exist.";
HttpReply reply(HttpReply::NotFound);
reply.setPayload("404 Not found.");
reply.packReply();
@ -111,8 +111,8 @@ bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName)
}
// 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.";
if (!file.canonicalFilePath().startsWith(m_webinterfaceDir.path())) {
qCWarning(dcWebServer) << "requested file" << file.fileName() << "is outside the public folder.";
HttpReply reply(HttpReply::Forbidden);
reply.setPayload("403 Forbidden.");
reply.packReply();
@ -122,8 +122,8 @@ bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName)
}
// make shore we can read the file
if (!checkFile.isReadable()) {
qCWarning(dcWebServer) << "requested file" << checkFile.fileName() << "is not readable.";
if (!file.isReadable()) {
qCWarning(dcWebServer) << "requested file" << file.fileName() << "is not readable.";
HttpReply reply(HttpReply::Forbidden);
reply.setPayload("403 Forbidden. Page not readable.");
reply.packReply();
@ -143,7 +143,7 @@ QString WebServer::fileName(const QString &query)
fileName = query;
}
return QFileInfo(m_webinterfaceDir.path() + fileName).canonicalFilePath();
return m_webinterfaceDir.path() + fileName;
}
void WebServer::writeData(QTcpSocket *socket, const QByteArray &data)
@ -167,6 +167,8 @@ void WebServer::onNewConnection()
connect(socket, &QTcpSocket::readyRead, this, &WebServer::readClient);
connect(socket, &QTcpSocket::disconnected, this, &WebServer::onDisconnected);
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError)));
qCDebug(dcConnection) << "Webserver client connected" << socket->peerName() << socket->peerAddress().toString() << socket->peerPort();
emit clientConnected(clientId);
@ -183,16 +185,32 @@ void WebServer::readClient()
// check client
if (clientId.isNull()) {
qCWarning(dcWebServer) << "Client not recognized";
socket->close();
socket->deleteLater();
return;
}
// read HTTP request
HttpRequest request = HttpRequest(socket->readAll());
QByteArray data = socket->readAll();
HttpRequest request;
if (m_incompleteRequests.contains(socket)) {
request = m_incompleteRequests.take(socket);
request.appendData(data);
} else {
request = HttpRequest(data);
}
if (!request.isComplete()) {
qCWarning(dcWebServer) << "Hash incomplete message.";
m_incompleteRequests.insert(socket, request);
return;
}
if (!request.isValid()) {
qCWarning(dcWebServer) << "Got invalid request.";
HttpReply reply(HttpReply::BadRequest);
reply.setPayload("400 Bad Request.");
reply.packReply();
writeData(socket, reply.data());
return;
}
@ -202,24 +220,23 @@ void WebServer::readClient()
qCWarning(dcWebServer) << "HTTP version is not supported." ;
HttpReply reply(HttpReply::HttpVersionNotSupported);
reply.setPayload("505 HTTP version is not supported.");
reply.packReply();
writeData(socket, reply.data());
return;
}
qCDebug(dcWebServer) << QString("Got valid request from %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort());
qCDebug(dcWebServer) << request.methodString() << request.url().path();
// verify method
if (request.method() == HttpRequest::Unhandled) {
HttpReply reply(HttpReply::MethodNotAllowed);
reply.setHeader(HttpReply::AllowHeader, "GET, PUT, POST, DELETE");
reply.setPayload("405 Method not allowed.");
reply.packReply();
writeData(socket, reply.data());
return;
}
// verify query
// verify API query
if (request.url().path().startsWith("/api/v1")) {
emit httpRequestReady(clientId, request);
return;
@ -227,10 +244,11 @@ void WebServer::readClient()
// request for a file...
if (request.method() == HttpRequest::Get) {
if (!verifyFile(socket, fileName(request.urlQuery().query())))
QString path = fileName(request.url().path());
if (!verifyFile(socket, path))
return;
QFile file(fileName(request.urlQuery().query()));
QFile file(path);
if (file.open(QFile::ReadOnly | QFile::Truncate)) {
qCDebug(dcWebServer) << "load file" << file.fileName();
HttpReply reply(HttpReply::Ok);
@ -238,7 +256,6 @@ void WebServer::readClient()
reply.setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";");
}
reply.setPayload(file.readAll());
reply.packReply();
writeData(socket, reply.data());
return;
}
@ -248,7 +265,6 @@ void WebServer::readClient()
qCWarning(dcWebServer) << "Unknown message received. Respond client with 501: Not Implemented.";
HttpReply reply(HttpReply::NotImplemented);
reply.setPayload("501 Not implemented.");
reply.packReply();
writeData(socket, reply.data());
}
@ -260,11 +276,18 @@ void WebServer::onDisconnected()
// clean up
QUuid clientId = m_clientList.key(socket);
m_clientList.remove(clientId);
m_incompleteRequests.remove(socket);
socket->deleteLater();
emit clientDisconnected(clientId);
}
void WebServer::onError(QAbstractSocket::SocketError error)
{
QTcpSocket* socket = qobject_cast<QTcpSocket *>(sender());
qWarning(dcWebServer) << "Client socket error" << socket->peerAddress() << error << socket->errorString();
}
bool WebServer::startServer()
{
if (!m_server->listen(QHostAddress::Any, m_port)) {

View File

@ -25,12 +25,11 @@
#include <QHash>
#include <QDir>
#include <QUuid>
#include <QTcpSocket>
#include "transportinterface.h"
class QTcpServer;
class QTcpSocket;
class HttpRequest;
class HttpReply;
@ -56,6 +55,7 @@ private:
QTcpServer *m_server;
QHash<QUuid, QTcpSocket *> m_clientList;
QHash<QTcpSocket *, HttpRequest> m_incompleteRequests;
bool m_enabled;
qint16 m_port;
@ -74,6 +74,7 @@ private slots:
void onNewConnection();
void readClient();
void onDisconnected();
void onError(QAbstractSocket::SocketError error);
public slots:
bool startServer() override;

View File

@ -32,6 +32,7 @@
#include <QNetworkReply>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QHttpPart>
#include <QMetaType>
using namespace guhserver;
@ -43,10 +44,18 @@ class TestRestDevices: public GuhTestBase
private slots:
void getConfiguredDevices();
void addConfiguredDevice_data();
void addConfiguredDevice();
void executeAction_data();
void executeAction();
void getStateValue_data();
void getStateValue();
private:
// for debugging
void printResponse(QNetworkReply *reply);
void printResponse(QNetworkReply *reply, const QByteArray &data);
};
@ -55,32 +64,286 @@ void TestRestDevices::getConfiguredDevices()
QNetworkAccessManager *nam = new QNetworkAccessManager(this);
QSignalSpy clientSpy(nam, SIGNAL(finished(QNetworkReply*)));
// Get all devices
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "text/json");
request.setUrl(QUrl("http://localhost:3000/api/v1/devices"));
QNetworkReply *reply;
reply = nam->get(request);
clientSpy.wait(200);
clientSpy.wait();
QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver");
QByteArray data = reply->readAll();
reply->deleteLater();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
QCOMPARE(error.error, QJsonParseError::NoError);
QVariantList deviceList = jsonDoc.toVariant().toList();
QCOMPARE(deviceList.count(), 3);
QVERIFY2(deviceList.count() >= 2, "not enought devices.");
// Get each of thouse devices individualy
foreach (const QVariant &device, deviceList) {
QVariantMap deviceMap = device.toMap();
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "text/json");
request.setUrl(QUrl(QString("http://localhost:3000/api/v1/devices/%1").arg(deviceMap.value("id").toString())));
clientSpy.clear();
QNetworkReply *reply = nam->get(request);
clientSpy.wait();
QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver");
jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
QCOMPARE(error.error, QJsonParseError::NoError);
reply->deleteLater();
}
nam->deleteLater();
}
void TestRestDevices::addConfiguredDevice_data()
{
QTest::addColumn<DeviceClassId>("deviceClassId");
QTest::addColumn<QVariantList>("deviceParams");
QTest::addColumn<int>("expectedStatusCode");
QVariantMap nameParam;
nameParam.insert("name", "name");
nameParam.insert("value", "Test Mockdevice");
QVariantMap httpportParam;
httpportParam.insert("name", "httpport");
httpportParam.insert("value", m_mockDevice1Port - 1);
QVariantMap asyncParam;
asyncParam.insert("name", "async");
asyncParam.insert("value", true);
QVariantMap notAsyncParam;
notAsyncParam.insert("name", "async");
notAsyncParam.insert("value", false);
QVariantMap notBrokenParam;
notBrokenParam.insert("name", "broken");
notBrokenParam.insert("value", false);
QVariantMap brokenParam;
brokenParam.insert("name", "broken");
brokenParam.insert("value", true);
QVariantList deviceParams;
deviceParams.clear(); deviceParams << nameParam << httpportParam << notAsyncParam << notBrokenParam;
QTest::newRow("User, JustAdd") << mockDeviceClassId << deviceParams << 200;
deviceParams.clear(); deviceParams << nameParam << httpportParam << asyncParam << notBrokenParam;
QTest::newRow("User, JustAdd, Async") << mockDeviceClassId << deviceParams << 200;
QTest::newRow("Invalid DeviceClassId") << DeviceClassId::createDeviceClassId() << deviceParams << 500;
deviceParams.clear(); deviceParams << nameParam << httpportParam << brokenParam;
QTest::newRow("Setup failure") << mockDeviceClassId << deviceParams << 500;
deviceParams.clear(); deviceParams << nameParam << httpportParam << asyncParam << brokenParam;
QTest::newRow("Setup failure, Async") << mockDeviceClassId << deviceParams << 500;
QVariantList invalidDeviceParams;
QTest::newRow("User, JustAdd, missing params") << mockDeviceClassId << invalidDeviceParams << 500;
QVariantMap fakeparam;
fakeparam.insert("name", "tropptth");
invalidDeviceParams.append(fakeparam);
QTest::newRow("User, JustAdd, invalid param") << mockDeviceClassId << invalidDeviceParams << 500;
fakeparam.insert("value", "buhuu");
invalidDeviceParams.clear();
invalidDeviceParams.append(fakeparam);
QTest::newRow("User, JustAdd, wrong param") << mockDeviceClassId << invalidDeviceParams << 500;
}
void TestRestDevices::addConfiguredDevice()
{
QFETCH(DeviceClassId, deviceClassId);
QFETCH(QVariantList, deviceParams);
QFETCH(int, expectedStatusCode);
QVariantMap params;
params.insert("deviceClassId", deviceClassId);
params.insert("deviceParams", deviceParams);
QNetworkAccessManager *nam = new QNetworkAccessManager();
QSignalSpy clientSpy(nam, SIGNAL(finished(QNetworkReply*)));
// Get all devices
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(QUrl("http://localhost:3000/api/v1/devices"));
QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact);
qDebug() << "sending" << payload;
QNetworkReply *reply = nam->post(request, payload);
clientSpy.wait();
QCOMPARE(clientSpy.count(), 1);
QByteArray data = reply->readAll();
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QCOMPARE(statusCode, expectedStatusCode);
reply->deleteLater();
if (expectedStatusCode == 200) {
// remove added device
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
QCOMPARE(error.error, QJsonParseError::NoError);
QVariantMap response = jsonDoc.toVariant().toMap();
DeviceId deviceId = DeviceId(response.value("deviceId").toString());
request.setUrl(QUrl(QString("http://localhost:3000/api/v1/devices/%1").arg(deviceId.toString())));
clientSpy.clear();
reply = nam->deleteResource(request);
clientSpy.wait();
QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver");
statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
QCOMPARE(statusCode, 200);
}
nam->deleteLater();
}
void TestRestDevices::executeAction_data()
{
QTest::addColumn<DeviceId>("deviceId");
QTest::addColumn<ActionTypeId>("actionTypeId");
QTest::addColumn<QVariantList>("actionParams");
QTest::addColumn<int>("expectedStatusCode");
QVariantList params;
QVariantMap param1;
param1.insert("name", "mockActionParam1");
param1.insert("value", 5);
params.append(param1);
QVariantMap param2;
param2.insert("name", "mockActionParam2");
param2.insert("value", true);
params.append(param2);
QTest::newRow("valid action") << m_mockDeviceId << mockActionIdWithParams << params << 200;
QTest::newRow("invalid deviceId") << DeviceId::createDeviceId() << mockActionIdWithParams << params << 404;
QTest::newRow("invalid actionTypeId") << m_mockDeviceId << ActionTypeId::createActionTypeId() << params << 404;
QTest::newRow("missing params") << m_mockDeviceId << mockActionIdWithParams << QVariantList() << 500;
QTest::newRow("async action") << m_mockDeviceId << mockActionIdAsync << QVariantList() << 200;
QTest::newRow("broken action") << m_mockDeviceId << mockActionIdFailing << QVariantList() << 500;
QTest::newRow("async broken action") << m_mockDeviceId << mockActionIdAsyncFailing << QVariantList() << 500;
}
void TestRestDevices::executeAction()
{
QFETCH(DeviceId, deviceId);
QFETCH(ActionTypeId, actionTypeId);
QFETCH(QVariantList, actionParams);
QFETCH(int, expectedStatusCode);
// execute action
QNetworkAccessManager nam;
QSignalSpy spy(&nam, SIGNAL(finished(QNetworkReply*)));
QVariantMap payloadMap;
payloadMap.insert("params", actionParams);
QNetworkRequest request(QUrl(QString("http://localhost:3000/api/v1/devices/%1/execute/%2").arg(deviceId.toString()).arg(actionTypeId.toString())));
spy.clear();
QNetworkReply *reply = nam.post(request, QJsonDocument::fromVariant(payloadMap).toJson(QJsonDocument::Compact));
spy.wait();
QCOMPARE(spy.count(), 1);
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QCOMPARE(statusCode, expectedStatusCode);
reply->deleteLater();
// Fetch action execution history from mock device
spy.clear();
request = QNetworkRequest(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockDevice1Port)));
reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
QByteArray data = reply->readAll();
reply->deleteLater();
// cleanup for the next run
spy.clear();
request.setUrl(QUrl(QString("http://localhost:%1/clearactionhistory").arg(m_mockDevice1Port)));
reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
reply->deleteLater();
spy.clear();
request.setUrl(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockDevice1Port)));
reply = nam.get(request);
spy.wait();
QCOMPARE(spy.count(), 1);
reply->deleteLater();
data = reply->readAll();
}
void TestRestDevices::getStateValue_data()
{
QList<Device*> devices = GuhCore::instance()->findConfiguredDevices(mockDeviceClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
QTest::addColumn<DeviceId>("deviceId");
QTest::addColumn<StateTypeId>("stateTypeId");
QTest::addColumn<int>("expectedStatusCode");
QTest::newRow("existing state") << device->id() << mockIntStateId << 200;
QTest::newRow("all states") << device->id() << StateTypeId() << 200;
QTest::newRow("invalid device") << DeviceId::createDeviceId() << mockIntStateId << 404;
QTest::newRow("invalid statetype") << device->id() << StateTypeId::createStateTypeId() << 404;
}
void TestRestDevices::getStateValue()
{
QFETCH(DeviceId, deviceId);
QFETCH(StateTypeId, stateTypeId);
QFETCH(int, expectedStatusCode);
QNetworkAccessManager *nam = new QNetworkAccessManager();
QSignalSpy clientSpy(nam, SIGNAL(finished(QNetworkReply*)));
QNetworkRequest request;
if (!stateTypeId.isNull()) {
request.setUrl(QUrl(QString("http://localhost:3000/api/v1/devices/%1/states/%2").arg(deviceId.toString()).arg(stateTypeId.toString())));
} else {
// Get all states
request.setUrl(QUrl(QString("http://localhost:3000/api/v1/devices/%1/states").arg(deviceId.toString())));
}
qDebug() << request.url();
QNetworkReply *reply = nam->get(request);
clientSpy.wait();
QCOMPARE(clientSpy.count(), 1);
QByteArray data = reply->readAll();
qDebug() << data;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QCOMPARE(statusCode, expectedStatusCode);
reply->deleteLater();
}
void TestRestDevices::printResponse(QNetworkReply *reply)
void TestRestDevices::printResponse(QNetworkReply *reply, const QByteArray &data)
{
qDebug() << "-------------------------------";
qDebug() << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
foreach (const QNetworkReply::RawHeaderPair &headerPair, reply->rawHeaderPairs()) {
qDebug() << headerPair.first << ":" << headerPair.second;
}
qDebug() << "-------------------------------";
qDebug() << reply->readAll();
qDebug() << data;
qDebug() << "-------------------------------";
}
#include "testrestdevices.moc"

View File

@ -32,6 +32,7 @@
#include <QNetworkReply>
#include <QCoreApplication>
#include <QMetaType>
#include <QByteArray>
using namespace guhserver;
@ -42,9 +43,14 @@ class TestWebserver: public GuhTestBase
private slots:
void httpVersion();
void multiPackageMessage();
void checkAllowedMethodCall_data();
void checkAllowedMethodCall();
void badRequests_data();
void badRequests();
void getFiles_data();
void getFiles();
@ -63,7 +69,11 @@ void TestWebserver::httpVersion()
QSignalSpy clientSpy(socket, SIGNAL(readyRead()));
socket->write("GET /hello/guh HTTP/1.0\r\n\r\n");
QByteArray requestData;
requestData.append("GET /hello/guh HTTP/1.0\r\n");
requestData.append("User-Agent: guh webserver test\r\n\r\n");
socket->write(requestData);
bool filesWritten = socket->waitForBytesWritten(500);
QVERIFY2(filesWritten, "could not write to webserver.");
@ -86,6 +96,59 @@ void TestWebserver::httpVersion()
socket->deleteLater();
}
void TestWebserver::multiPackageMessage()
{
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()));
QByteArray requestData;
requestData.append("PUT / HTTP/1.1\r\n");
requestData.append("User-Agent: webserver test\r\n");
requestData.append("Content-Length: 42\r\n");
requestData.append("\r\n");
requestData.append("This message");
socket->write(requestData);
bool filesWritten = socket->waitForBytesWritten();
QVERIFY2(filesWritten, "could not write to webserver.");
socket->write(QByteArray(" was sent"));
filesWritten = socket->waitForBytesWritten();
QVERIFY2(filesWritten, "could not write to webserver.");
socket->write(QByteArray(" in four TCP"));
filesWritten = socket->waitForBytesWritten();
QVERIFY2(filesWritten, "could not write to webserver.");
socket->write(QByteArray("packages. "));
filesWritten = socket->waitForBytesWritten();
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, 501);
socket->close();
socket->deleteLater();
}
void TestWebserver::checkAllowedMethodCall_data()
{
QTest::addColumn<QString>("method");
@ -141,6 +204,60 @@ void TestWebserver::checkAllowedMethodCall()
reply->deleteLater();
}
void TestWebserver::badRequests_data()
{
QTest::addColumn<QByteArray>("request");
QTest::addColumn<int>("expectedStatusCode");
QByteArray wrongContentLength;
wrongContentLength.append("PUT / HTTP/1.1\r\n");
wrongContentLength.append("User-Agent: webserver test\r\n");
wrongContentLength.append("Content-Length: 1\r\n");
wrongContentLength.append("\r\n");
wrongContentLength.append("longer content than told in the header");
QTest::newRow("wrong content length") << wrongContentLength << 400;
}
void TestWebserver::badRequests()
{
QFETCH(QByteArray, request);
QFETCH(int, expectedStatusCode);
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(request);
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, expectedStatusCode);
socket->close();
socket->deleteLater();
}
void TestWebserver::getFiles_data()
{
QTest::addColumn<QString>("query");
@ -164,13 +281,12 @@ void TestWebserver::getFiles()
request.setUrl(QUrl("http://localhost:3000" + query));
QNetworkReply *reply = nam->get(request);
clientSpy.wait(200);
clientSpy.wait();
QVERIFY2(clientSpy.count() == 1, "expected exactly 1 response from webserver");
printResponse(reply);
bool ok = false;
qDebug() << reply->readAll();
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok);
QVERIFY2(ok, "Could not convert statuscode from response to int");
QCOMPARE(statusCode, expectedStatusCode);