controller.cpp
Rama master · réplica de lectura del repositorio original.
#include <QDir>
#include "controller.h"
#include "logger.h"
Controller::Controller(const QString &configFile) : HOMEd(SERVICE_VERSION, configFile), m_database(new Database(getConfig(), this)), m_tcpServer(new QTcpServer(this)), m_webSocket(new QWebSocketServer("HOMEd", QWebSocketServer::NonSecureMode, this)), m_timer(new QTimer(this)), m_commands(QMetaEnum::fromType <Command> ())
{
m_frontend = getConfig()->value("server/frontend", basePath().append("share/homed-web")).toString();
m_title = getConfig()->value("server/title", "HOMEd").toString();
m_username = getConfig()->value("server/username").toString();
m_password = getConfig()->value("server/password").toString();
m_guest = getConfig()->value("server/guest").toString();
m_debug = getConfig()->value("server/debug", false).toBool();
m_auth = m_username.isEmpty() || m_password.isEmpty() ? false : true;
m_retained = {"device", "expose", "service", "status"};
m_types = {"css", "js", "json", "png", "svg", "woff2"};
connect(m_tcpServer, &QTcpServer::newConnection, this, &Controller::socketConnected);
connect(m_webSocket, &QWebSocketServer::newConnection, this, &Controller::clientConnected);
connect(m_timer, &QTimer::timeout, this, &Controller::pingClients);
if (!m_tcpServer->listen(QHostAddress(getConfig()->value("server/address", "0.0.0.0").toString()), static_cast <quint16> (getConfig()->value("server/port", 8086).toInt())))
{
logWarning << "Failed to start server, error:" << m_tcpServer->errorString();
return;
}
m_timer->start(10000);
m_database->init();
}
QString Controller::includeList(const QString &path, const QString &type, const QString &pattern)
{
QList <QString> list = QDir(QString(m_frontend).append(path)).entryList({QString("*.").append(type)}, QDir::Files, QDir::Name), items;
for (int i = 0; i < list.count(); i++)
items.append(pattern.arg(QString(path).mid(1).append('/').append(list.at(i))));
return items.join(QString("\n").append(QString(8, 0x20)));
}
QString Controller::ticketToken(const QString &ticket)
{
auto it = m_tickets.find(ticket);
QString token;
if (it != m_tickets.end())
{
if (it.value().expire >= QDateTime::currentSecsSinceEpoch())
token = it.value().token;
m_tickets.erase(it);
}
return token;
}
void Controller::httpResponse(QTcpSocket *socket, quint16 code, const QMap <QString, QString> &headers, const QByteArray &response)
{
QByteArray data;
switch (code)
{
case 200: data = "HTTP/1.1 200 OK"; break;
case 301: data = "HTTP/1.1 301 Moved Permanently"; break;
case 404: data = "HTTP/1.1 404 Not Found"; break;
case 405: data = "HTTP/1.1 405 Method Not Allowed"; break;
case 500: data = "HTTP/1.1 500 Internal Server Error"; break;
}
for (auto it = headers.begin(); it != headers.end(); it++)
data.append(QString("\r\n%1: %2").arg(it.key(), it.value()).toUtf8());
socket->write(data.append("\r\nConnection: close\r\n\r\n").append(response));
socket->close();
}
void Controller::fileResponse(QTcpSocket *socket, const QString &fileName)
{
QFile file(QString(m_frontend).append(fileName));
QMap <QString, QString> headers;
QByteArray type, data;
if (!file.exists())
{
httpResponse(socket, 404);
return;
}
if (!file.open(QFile::ReadOnly))
{
httpResponse(socket, 500);
return;
}
switch (m_types.indexOf(fileName.mid(fileName.lastIndexOf('.') + 1)))
{
case 0: type = "text/css"; break; // css
case 1: type = "text/javascript"; break; // js
case 2: type = "application/json"; break; // json
case 3: type = "image/png"; break; // png
case 4: type = "image/svg+xml"; break; // svg
case 5: type = "font/woff2"; break; // woff2
default: type = "text/html"; break;
}
data = file.readAll();
if (fileName == "/index.html" || fileName == "/login.html")
{
headers.insert("Cache-Control", "no-cache, no-store");
data = (fileName == "/index.html" ? QString(data).arg(m_title, includeList("/plugin/css", "css", "<link rel=\"stylesheet\" href=\"%1\">"), includeList("/plugin/js", "js", "<script src=\"%1\"></script>"), SERVICE_VERSION, m_auth ? "<span id=\"logout\"><i class=\"mdi-logout\"></i> LOGOUT</span>" : QString()).remove(QRegExp("\n[ ]+(?=\n)")) : QString(data).arg(m_title)).toUtf8();
}
headers.insert("Content-Type", type);
headers.insert("Content-Length", QString::number(data.length()));
httpResponse(socket, 200, headers, data);
file.close();
}
void Controller::quit(void)
{
m_webSocket->close();
for (auto it = m_clients.begin(); it != m_clients.end(); it++)
it.key()->deleteLater();
HOMEd::quit();
}
void Controller::mqttConnected(void)
{
for (auto it = m_clients.begin(); it != m_clients.end(); it++)
for (int i = 0; i < it.value().count(); i++)
mqttSubscribe(mqttTopic(it.value().at(i)));
if (m_database->passive())
{
logWarning << "Running in passive mode";
return;
}
mqttSubscribe(mqttTopic("command/web"));
m_database->store();
mqttPublishService();
}
void Controller::mqttDisconnected(void)
{
m_messages.clear();
}
void Controller::mqttReceived(const QByteArray &message, const QMqttTopicName &topic)
{
QString subTopic = topic.name().replace(0, mqttTopic().length(), QString());
QJsonObject json = QJsonDocument::fromJson(message).object();
if (subTopic == "command/web")
{
switch (static_cast <Command> (m_commands.keyToValue(json.value("action").toString().toUtf8().constData())))
{
case Command::restartService:
{
logWarning << "Restart request received...";
mqttPublish(topic.name(), QJsonObject(), true);
QCoreApplication::exit(EXIT_RESTART);
break;
}
case Command::updateDashboards:
{
m_database->updateDasboards(json.value("data").toArray());
m_database->store(true);
break;
}
case Command::updateIcons:
{
m_database->updateIcons(json.value("data").toObject());
m_database->store(true);
break;
}
case Command::updateNames:
{
m_database->updateNames(json.value("data").toObject());
m_database->store(true);
break;
}
}
}
if (m_retained.contains(subTopic.split('/').value(0)))
m_messages.insert(subTopic, message);
for (auto it = m_clients.begin(); it != m_clients.end(); it++)
{
for (int i = 0; i < it.value().count(); i++)
{
const QString &item = it.value().at(i);
if (item.endsWith('#') ? !subTopic.startsWith(item.mid(0, item.indexOf("#"))) : subTopic != item)
continue;
it.key()->sendTextMessage(QJsonDocument({{"topic", subTopic}, {"message", json.isEmpty() ? QJsonValue::Null : QJsonValue(json)}}).toJson(QJsonDocument::Compact));
break;
}
}
}
void Controller::socketConnected(void)
{
QTcpSocket *socket = m_tcpServer->nextPendingConnection();
connect(socket, &QTcpSocket::disconnected, this, &Controller::socketDisconnected);
connect(socket, &QTcpSocket::readyRead, this, &Controller::readyRead);
m_sockets.append(socket);
}
void Controller::socketDisconnected(void)
{
QTcpSocket *socket = reinterpret_cast <QTcpSocket*> (sender());
m_sockets.removeAll(socket);
socket->deleteLater();
}
void Controller::readyRead(void)
{
QTcpSocket *socket = reinterpret_cast <QTcpSocket*> (sender());
QByteArray request = socket->peek(socket->bytesAvailable());
QList <QString> list = QString(request).split("\r\n\r\n"), head = list.value(0).split("\r\n"), target = head.value(0).split(0x20), cookieList, itemList;
QByteArray method = target.value(0).toUtf8(), url = target.value(1).toUtf8(), content = list.value(1).toUtf8();
QMap <QString, QString> headers, cookies, items;
bool upgrade = false, guest = false;
disconnect(socket, &QTcpSocket::readyRead, this, &Controller::readyRead);
logDebug(m_debug) << "Request" << head.value(0) << "received from" << socket->peerAddress().toString();
for (int i = 1; i < head.count(); i++)
{
int index = head.at(i).indexOf(':');
if (index < 0)
continue;
headers.insert(head.at(i).mid(0, index).trimmed().toLower(), head.at(i).mid(index + 1).trimmed());
logDebug(m_debug) << "Header received:" << head.at(i);
}
cookieList = headers.value("cookie").split(';');
for (int i = 0; i < cookieList.count(); i++)
{
QList <QString> cookie = cookieList.at(i).split('=');
if (cookie.count() < 2)
continue;
cookies.insert(cookie.value(0).trimmed(), cookie.value(1).trimmed());
logDebug(m_debug) << "Cookie received:" << cookieList.at(i);
}
if (method == "POST")
{
socket->read(request.length());
while (content.length() < headers.value("content-length").toInt() && socket->waitForReadyRead(REQUEST_TIMEOUT))
content.append(socket->readAll());
}
itemList = QString(method == "GET" && url.contains('?') ? url.mid(url.indexOf('?') + 1) : content).split('&');
for (int i = 0; i < itemList.count(); i++)
{
QList <QString> item = itemList.at(i).split('=');
if (item.count() < 2)
continue;
items.insert(item.value(0), QUrl::fromPercentEncoding(item.value(1).toUtf8()));
logDebug(m_debug) << "Data received:" << itemList.at(i);
}
url = url.mid(0, url.indexOf('?'));
upgrade = headers.value("upgrade") == "websocket";
if (m_auth)
{
QString address = !headers.value("x-real-ip").isEmpty() ? headers.value("x-real-ip") : !headers.value("x-forwarded-for").isEmpty() ? headers.value("x-forwarded-for").split(',').value(0).trimmed() : socket->peerAddress().toString(), token = upgrade && items.contains("ticket") ? ticketToken(items.value("ticket")) : cookies.value("homed-auth-token");
if (address.startsWith("::ffff:"))
address = address.mid(7);
if (token != m_database->adminToken() && token != m_database->guestToken() && (upgrade || (url != "/manifest.json" && !url.startsWith("/css/") && !url.startsWith("/font/") && !url.startsWith("/img/"))))
{
if (method == "POST")
{
QString username = items.value("username"), password = items.value("password");
if (username == m_username && password == m_password)
{
logInfo << "User" << username << "successfully logged in, address:" << address;
token = m_database->adminToken();
}
else if (!m_guest.isEmpty() && username == "guest" && password == m_guest)
{
logInfo << "Guest user successfully logged in, address:" << address;
token = m_database->guestToken();
}
if (token == m_database->adminToken() || token == m_database->guestToken())
{
if (items.value("ticket") == "true")
{
QByteArray ticket = m_database->randomData(32).toHex(), data = QJsonDocument(QJsonObject {{"ticket", QString(ticket)}}).toJson(QJsonDocument::Compact);
httpResponse(socket, 200, {{"Cache-Control", "no-cache, no-store"}, {"Content-Type", "application/json"}, {"Content-Length", QString::number(data.length())}}, data);
m_tickets.insert(ticket, {token, QDateTime::currentSecsSinceEpoch() + TICKET_MAX_AGE});
}
else
httpResponse(socket, 301, {{"Location", QString(headers.value("x-ingress-path")).append('/')}, {"Cache-Control", "no-cache, no-store"}, {"Set-Cookie", QString("homed-auth-token=%1; path=/; max-age=%2").arg(token).arg(COOKIE_MAX_AGE)}});
return;
}
logWarning << "User" << (username.isEmpty() ? "(empty)" : username) << "login failed, address:" << address;
}
fileResponse(socket, "/login.html");
return;
}
guest = token != m_database->adminToken();
}
if (url == "/logout")
{
httpResponse(socket, 301, {{"Location", QString(headers.value("x-ingress-path")).append('/')}, {"Cache-Control", "no-cache, no-store"}, {"Set-Cookie", "homed-auth-token=deleted; path=/; max-age=0"}});
if (guest || items.value("session") != "all")
return;
for (auto it = m_clients.begin(); it != m_clients.end(); it++)
it.key()->deleteLater();
m_database->resetAdminToken();
m_database->resetGuestToken();
m_database->store(true);
return;
}
if (method != "GET")
{
httpResponse(socket, 405);
return;
}
if (upgrade)
{
socket->setProperty("guest", guest);
m_webSocket->handleConnection(socket);
m_sockets.removeAll(socket);
return;
}
fileResponse(socket, url != "/" ? url : "/index.html");
}
void Controller::clientConnected(void)
{
QWebSocket *client = m_webSocket->nextPendingConnection();
connect(client, &QWebSocket::disconnected, this, &Controller::clientDisconnected);
connect(client, &QWebSocket::textMessageReceived, this, &Controller::textMessageReceived);
if (mqttStatus())
client->sendTextMessage(QJsonDocument({{"topic", "setup"}, {"message", QJsonObject {{"guest", client->parent() ? client->parent()->property("guest").toBool() : false}}}}).toJson(QJsonDocument::Compact));
else
client->sendTextMessage(QJsonDocument({{"topic", "error"}, {"message", "mqtt disconnected"}}).toJson(QJsonDocument::Compact));
m_clients.insert(client, QList <QString> ());
}
void Controller::clientDisconnected(void)
{
QWebSocket *client = reinterpret_cast <QWebSocket*> (sender());
m_clients.remove(client);
client->deleteLater();
}
void Controller::textMessageReceived(const QString &message)
{
auto it = m_clients.find(reinterpret_cast <QWebSocket*> (sender()));
QJsonObject json = QJsonDocument::fromJson(message.toUtf8()).object();
QString action = json.value("action").toString(), subTopic = json.value("topic").toString();
if (it == m_clients.end() || subTopic.isEmpty())
return;
if (action == "subscribe")
{
QWebSocket* client = it.key();
if (!it.value().contains(subTopic))
it.value().append(subTopic);
for (auto it = m_messages.begin(); it != m_messages.end(); it++)
{
if (subTopic.endsWith('#') ? !it.key().startsWith(subTopic.mid(0, subTopic.indexOf("#"))) : it.key() != subTopic)
continue;
client->sendTextMessage(QJsonDocument({{"topic", it.key()}, {"message", QJsonDocument::fromJson(it.value()).object()}}).toJson(QJsonDocument::Compact));
}
mqttSubscribe(mqttTopic(subTopic));
}
else if (action == "publish")
{
QJsonObject message = json.value("message").toObject();
QString command = message.value("action").toString();
if ((it.key()->parent() ? it.key()->parent()->property("guest").toBool() : false) && (subTopic.startsWith("command/") ? !command.startsWith("get") && command != "callScene" : !subTopic.startsWith("td/")))
{
it.key()->sendTextMessage(QJsonDocument({{"topic", "error"}, {"message", "access denied"}}).toJson(QJsonDocument::Compact));
return;
}
mqttPublish(mqttTopic(subTopic), message);
}
else if (action == "unsubscribe")
it.value().removeAll(subTopic);
}
void Controller::pingClients(void)
{
qint64 now = QDateTime::currentSecsSinceEpoch();
for (auto it = m_clients.begin(); it != m_clients.end(); it++)
it.key()->ping();
for (auto it = m_tickets.begin(); it != m_tickets.end(); NULL)
{
if (now > it.value().expire)
{
it = m_tickets.erase(it);
continue;
}
it++;
}
}