This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
powersync-core/plugins/mock/httpdaemon.cpp
Michael Zanetti 562e7aa89d update how id names are generated
include the deviceClass/plugin name in all defines to avoid collisions
between deviceClasses within the same file. So far this hasn't
really been an issue because using idName we could set random ids.

Now interfaces dictate the names, so having multiple deviceClasses
in one file and both implementing the same interface would clash.

This also should improve readability in the plugins code as we won't
have ids like: "bridgeConnected" and "connected" available which
easily causes the developer to accidentally use "connected" where
instead "bridgeConnected" should be used (I actually found some
bugs like this while updating plugins for this). The new style
would force those states to be named like e.g. "bridgeConnected"
and "lightConnected" which are not as easy to mix up.
2019-04-08 13:55:15 +02:00

219 lines
8.0 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2015 Simon Stürz <simon.stuerz@guh.io> *
* Copyright (C) 2014 Michael Zanetti <michael_zanetti@gmx.net> *
* *
* This file is part of guh. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "httpdaemon.h"
#include "plugin/device.h"
#include "plugin/deviceclass.h"
#include "plugin/deviceplugin.h"
#include "types/statetype.h"
#include "extern-plugininfo.h"
#include <QTcpSocket>
#include <QDebug>
#include <QDateTime>
#include <QUrlQuery>
#include <QRegExp>
#include <QStringList>
HttpDaemon::HttpDaemon(Device *device, DevicePlugin *parent):
QTcpServer(parent), disabled(false), m_plugin(parent), m_device(device)
{
listen(QHostAddress::Any, device->paramValue(mockHttpportParamTypeId).toInt());
}
HttpDaemon::~HttpDaemon()
{
close();
}
void HttpDaemon::incomingConnection(qintptr socket)
{
if (disabled)
return;
// When a new client connects, the server constructs a QTcpSocket and all
// communication with the client is done over this QTcpSocket. QTcpSocket
// works asynchronously, this means that all the communication is done
// in the two slots readClient() and discardClient().
QTcpSocket* s = new QTcpSocket(this);
connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
s->setSocketDescriptor(socket);
}
void HttpDaemon::actionExecuted(const ActionTypeId &actionTypeId)
{
m_actionList.append(qMakePair<ActionTypeId, QDateTime>(actionTypeId, QDateTime::currentDateTime()));
}
void HttpDaemon::readClient()
{
if (disabled)
return;
// This slot is called when the client sent data to the server. The
// server looks if it was a get request and sends a very simple HTML
// document back.
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine()) {
QByteArray data = socket->readLine();
QStringList tokens = QString(data).split(QRegExp("[ \r\n][ \r\n]*"));
QUrl url("http://foo.bar" + tokens[1]);
QUrlQuery query(url);
if (url.path() == "/setstate") {
qCDebug(dcMockDevice) << "Set state value" << query.queryItems().first().second;
emit setState(StateTypeId(query.queryItems().first().first), QVariant(query.queryItems().first().second));
} else if (url.path() == "/generateevent") {
emit triggerEvent(EventTypeId(query.queryItemValue("eventtypeid")));
} else if (url.path() == "/actionhistory") {
qCDebug(dcMockDevice) << "Get action history called";
QTextStream os(socket);
os.setAutoDetectUnicode(true);
os << generateHeader();
for (int i = 0; i < m_actionList.count(); ++i) {
os << m_actionList.at(i).first.toString() << '\n';
qCDebug(dcMockDevice) << " " << m_actionList.at(i).first.toString();
}
socket->close();
return;
} else if (url.path() == "/clearactionhistory") {
qCDebug(dcMockDevice) << "Clear action history";
m_actionList.clear();
} else if (url.path() == "/disappear") {
qCDebug(dcMockDevice) << "Should disappear";
emit disappear();
}
if (tokens[0] == "GET") {
QTextStream os(socket);
os.setAutoDetectUnicode(true);
os << generateWebPage();
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState)
delete socket;
}
}
}
void HttpDaemon::discardClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
socket->deleteLater();
}
QString HttpDaemon::generateHeader()
{
QString contentHeader(
"HTTP/1.0 200 Ok\r\n"
"Content-Type: text/html; charset=\"utf-8\"\r\n"
"\r\n"
);
return contentHeader;
}
QString HttpDaemon::generateWebPage()
{
DeviceClass deviceClass = m_plugin->supportedDevices().first();
QString body = QString(
"<html>"
"<body>"
"<h1>Mock device Controller</h1>\n"
"<hr>"
"<h2>Device Information</h2>"
"Name: %1<br>"
"ID: %2<br>"
"DeviceClass ID: %3<br>").arg(m_device->name()).arg(m_device->id().toString()).arg(deviceClass.id().toString());
body.append("<hr>");
body.append("<h2>States</h2>");
body.append("<table>");
for (int i = 0; i < deviceClass.stateTypes().count(); ++i) {
body.append("<tr>");
body.append("<form action=\"/setstate\" method=\"get\">");
const StateType &stateType = deviceClass.stateTypes().at(i);
body.append("<td>" + stateType.name() + "</td>");
body.append(QString("<td><input type='input'' name='%1' value='%2'></td>").arg(stateType.id().toString()).arg(m_device->states().at(i).value().toString()));
body.append("<td><input type=submit value='Set State'/></td>");
body.append("</form>");
body.append("</tr>");
}
body.append("</table>");
body.append("<hr>");
body.append("<h2>Events</h2>");
body.append("<table>");
for (int i = 0; i < deviceClass.eventTypes().count(); ++i) {
const EventType &eventType = deviceClass.eventTypes().at(i);
body.append(QString(
"<tr>"
"<form action=\"/generateevent\" method=\"get\">"
"<td>%1<input type='hidden' name='eventtypeid' value='%2'/></td>"
"<td>").arg(eventType.name()).arg(eventType.id().toString()));
if (!eventType.name().endsWith(" changed")) {
body.append("<input type='submit' value='Generate'/>");
}
body.append("</td>"
"</form>"
"</tr>"
);
}
body.append("</table>");
body.append("<hr>");
body.append("<h2>Actions</h2>");
body.append("<table border=2px>");
body.append("<tr><td>Name</td><td>Type ID</td><td>Timestamp</td></tr>");
for (int i = 0; i < m_actionList.count(); ++i) {
ActionTypeId actionTypeId = ActionTypeId(m_actionList.at(i).first);
QDateTime timestamp = m_actionList.at(i).second;
QString actionName;
foreach (const ActionType &at, deviceClass.actionTypes()) {
if (at.id() == actionTypeId) {
actionName = at.name();
break;
}
}
body.append(QString(
"<tr>"
"<td>%1</td>"
"<td>%2</td>"
"<td>%3</td>"
"</tr>"
).arg(actionName).arg(actionTypeId.toString()).arg(timestamp.toString()));
}
body.append("</table>");
body.append("</body></html>\n");
return generateHeader() + body;
}