diff --git a/libguh/libguh.pro b/libguh/libguh.pro index 65b48006..aa36991d 100644 --- a/libguh/libguh.pro +++ b/libguh/libguh.pro @@ -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 diff --git a/libguh/network/httpreply.cpp b/server/httpreply.cpp similarity index 95% rename from libguh/network/httpreply.cpp rename to server/httpreply.cpp index bf486b7a..885aed84 100644 --- a/libguh/network/httpreply.cpp +++ b/server/httpreply.cpp @@ -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 #include +#include /*! 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(); } diff --git a/libguh/network/httpreply.h b/server/httpreply.h similarity index 97% rename from libguh/network/httpreply.h rename to server/httpreply.h index a6268f15..8e301c81 100644 --- a/libguh/network/httpreply.h +++ b/server/httpreply.h @@ -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 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(); }; diff --git a/libguh/network/httprequest.cpp b/server/httprequest.cpp similarity index 74% rename from libguh/network/httprequest.cpp rename to server/httprequest.cpp index 137f2e34..5703818b 100644 --- a/libguh/network/httprequest.cpp +++ b/server/httprequest.cpp @@ -24,61 +24,19 @@ #include +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"; diff --git a/libguh/network/httprequest.h b/server/httprequest.h similarity index 93% rename from libguh/network/httprequest.h rename to server/httprequest.h index 3cb6f558..5b7b02c4 100644 --- a/libguh/network/httprequest.h +++ b/server/httprequest.h @@ -37,7 +37,8 @@ public: Unhandled }; - explicit HttpRequest(QByteArray rawData); + HttpRequest(); + HttpRequest(QByteArray rawData); QByteArray rawHeader() const; QHash 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); }; diff --git a/server/jsonrpc/devicehandler.cpp b/server/jsonrpc/devicehandler.cpp index 579cbed2..6421fbb3 100644 --- a/server/jsonrpc/devicehandler.cpp +++ b/server/jsonrpc/devicehandler.cpp @@ -632,7 +632,6 @@ void DeviceHandler::pairingFinished(const PairingTransactionId &pairingTransacti reply->finished(); return; } - m_asynDeviceAdditions.insert(deviceId, reply); } diff --git a/server/jsonrpc/jsontypes.cpp b/server/jsonrpc/jsontypes.cpp index fe8a1c20..16e47c4a 100644 --- a/server/jsonrpc/jsontypes.cpp +++ b/server/jsonrpc/jsontypes.cpp @@ -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 rules) +QVariantList JsonTypes::packRuleDescriptions(const QList &rules) { QVariantList rulesList; foreach (const Rule &rule, rules) { diff --git a/server/jsonrpc/jsontypes.h b/server/jsonrpc/jsontypes.h index 59c24bdf..1ff795ef 100644 --- a/server/jsonrpc/jsontypes.h +++ b/server/jsonrpc/jsontypes.h @@ -169,7 +169,7 @@ public: static QVariantList packDeviceDescriptors(const QList deviceDescriptors); static QVariantList packRuleDescriptions(); - static QVariantList packRuleDescriptions(const QList rules); + static QVariantList packRuleDescriptions(const QList &rules); static QVariantList packActionTypes(const DeviceClass &deviceClass); static QVariantList packStateTypes(const DeviceClass &deviceClass); diff --git a/server/rest/deviceclassesresource.cpp b/server/rest/deviceclassesresource.cpp index 59d75132..e52ccc3b 100644 --- a/server/rest/deviceclassesresource.cpp +++ b/server/rest/deviceclassesresource.cpp @@ -19,7 +19,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "deviceclassesresource.h" -#include "network/httprequest.h" +#include "httprequest.h" #include "guhcore.h" #include @@ -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 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); diff --git a/server/rest/deviceclassesresource.h b/server/rest/deviceclassesresource.h index 80809ceb..07cf4fc8 100644 --- a/server/rest/deviceclassesresource.h +++ b/server/rest/deviceclassesresource.h @@ -26,7 +26,7 @@ #include "jsontypes.h" #include "restresource.h" -#include "network/httpreply.h" +#include "httpreply.h" class HttpRequest; diff --git a/server/rest/devicesresource.cpp b/server/rest/devicesresource.cpp index f0746c17..5fde6feb 100644 --- a/server/rest/devicesresource.cpp +++ b/server/rest/devicesresource.cpp @@ -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 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(); diff --git a/server/rest/devicesresource.h b/server/rest/devicesresource.h index 5bda455c..31193f13 100644 --- a/server/rest/devicesresource.h +++ b/server/rest/devicesresource.h @@ -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 m_asyncActionExecutions; - mutable QHash m_asynDeviceAdditions; + mutable QHash m_asyncDeviceAdditions; mutable QHash m_asyncEditDevice; mutable QHash m_asyncPairingRequests; diff --git a/server/rest/logsresource.cpp b/server/rest/logsresource.cpp index 14790609..5a2a7398 100644 --- a/server/rest/logsresource.cpp +++ b/server/rest/logsresource.cpp @@ -19,7 +19,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "logsresource.h" -#include "network/httprequest.h" +#include "httprequest.h" namespace guhserver { diff --git a/server/rest/logsresource.h b/server/rest/logsresource.h index c582a815..20403ff9 100644 --- a/server/rest/logsresource.h +++ b/server/rest/logsresource.h @@ -26,7 +26,7 @@ #include "jsontypes.h" #include "restresource.h" -#include "network/httpreply.h" +#include "httpreply.h" class HttpRequest; diff --git a/server/rest/pluginsresource.cpp b/server/rest/pluginsresource.cpp index 5d3c624f..6a0298c7 100644 --- a/server/rest/pluginsresource.cpp +++ b/server/rest/pluginsresource.cpp @@ -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 diff --git a/server/rest/pluginsresource.h b/server/rest/pluginsresource.h index 2823a852..3bae2740 100644 --- a/server/rest/pluginsresource.h +++ b/server/rest/pluginsresource.h @@ -26,7 +26,7 @@ #include "jsontypes.h" #include "restresource.h" -#include "network/httpreply.h" +#include "httpreply.h" class HttpRequest; diff --git a/server/rest/restresource.cpp b/server/rest/restresource.cpp index 85440401..edc1c88a 100644 --- a/server/rest/restresource.cpp +++ b/server/rest/restresource.cpp @@ -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; } diff --git a/server/rest/restresource.h b/server/rest/restresource.h index 877fe7ff..c745a365 100644 --- a/server/rest/restresource.h +++ b/server/rest/restresource.h @@ -24,10 +24,9 @@ #include #include -#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, QString> m_descriptions; + QHash m_params; + QHash m_returns; + }; } diff --git a/server/rest/restserver.cpp b/server/rest/restserver.cpp index 4d9363ca..c8d26191 100644 --- a/server/rest/restserver.cpp +++ b/server/rest/restserver.cpp @@ -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 @@ -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(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(); diff --git a/server/rest/rulesresource.cpp b/server/rest/rulesresource.cpp index de5903e9..3958ab09 100644 --- a/server/rest/rulesresource.cpp +++ b/server/rest/rulesresource.cpp @@ -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 ruleIdsList = GuhCore::instance()->findRules(deviceId); + QList 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); +} + } diff --git a/server/rest/rulesresource.h b/server/rest/rulesresource.h index 9a946c07..bca8e97e 100644 --- a/server/rest/rulesresource.h +++ b/server/rest/rulesresource.h @@ -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; }; } diff --git a/server/rest/vendorsresource.cpp b/server/rest/vendorsresource.cpp index d52fd45b..56ca9bdc 100644 --- a/server/rest/vendorsresource.cpp +++ b/server/rest/vendorsresource.cpp @@ -19,7 +19,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "vendorsresource.h" -#include "network/httprequest.h" +#include "httprequest.h" #include "loggingcategories.h" #include "guhcore.h" diff --git a/server/rest/vendorsresource.h b/server/rest/vendorsresource.h index fa6abcd5..14f7b4b0 100644 --- a/server/rest/vendorsresource.h +++ b/server/rest/vendorsresource.h @@ -26,7 +26,7 @@ #include "jsontypes.h" #include "restresource.h" -#include "network/httpreply.h" +#include "httpreply.h" class HttpRequest; diff --git a/server/server.pri b/server/server.pri index 312f8089..849bee7e 100644 --- a/server/server.pri +++ b/server/server.pri @@ -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 \ diff --git a/server/webserver.cpp b/server/webserver.cpp index 97f5f589..78d78b9f 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -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 #include @@ -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(sender()); + qWarning(dcWebServer) << "Client socket error" << socket->peerAddress() << error << socket->errorString(); +} + bool WebServer::startServer() { if (!m_server->listen(QHostAddress::Any, m_port)) { diff --git a/server/webserver.h b/server/webserver.h index 7b04d080..a0137a43 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -25,12 +25,11 @@ #include #include #include - +#include #include "transportinterface.h" class QTcpServer; -class QTcpSocket; class HttpRequest; class HttpReply; @@ -56,6 +55,7 @@ private: QTcpServer *m_server; QHash m_clientList; + QHash 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; diff --git a/tests/auto/restdevices/testrestdevices.cpp b/tests/auto/restdevices/testrestdevices.cpp index 2ae1284d..7fbc3cdd 100644 --- a/tests/auto/restdevices/testrestdevices.cpp +++ b/tests/auto/restdevices/testrestdevices.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include 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"); + QTest::addColumn("deviceParams"); + QTest::addColumn("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"); + QTest::addColumn("actionTypeId"); + QTest::addColumn("actionParams"); + QTest::addColumn("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 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"); + QTest::addColumn("stateTypeId"); + QTest::addColumn("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" diff --git a/tests/auto/webserver/testwebserver.cpp b/tests/auto/webserver/testwebserver.cpp index 71ab774a..501bb084 100644 --- a/tests/auto/webserver/testwebserver.cpp +++ b/tests/auto/webserver/testwebserver.cpp @@ -32,6 +32,7 @@ #include #include #include +#include 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("method"); @@ -141,6 +204,60 @@ void TestWebserver::checkAllowedMethodCall() reply->deleteLater(); } +void TestWebserver::badRequests_data() +{ + QTest::addColumn("request"); + QTest::addColumn("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("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);