Ctrl K
ComunidadGuías…

controller.cpp

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#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++;
    }
}