// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppIncomingClient.h"

#include "QXmppBindIq.h"
#include "QXmppConstants_p.h"
#include "QXmppMessage.h"
#include "QXmppPasswordChecker.h"
#include "QXmppSasl_p.h"
#include "QXmppSessionIq.h"
#include "QXmppStartTlsPacket.h"
#include "QXmppStreamFeatures.h"
#include "QXmppUtils.h"

#include <QDomElement>
#include <QHostAddress>
#include <QSslKey>
#include <QSslSocket>
#include <QTimer>

class QXmppIncomingClientPrivate
{
public:
    QXmppIncomingClientPrivate(QXmppIncomingClient *qq);
    QTimer *idleTimer;

    QString domain;
    QString jid;
    QString resource;
    QXmppPasswordChecker *passwordChecker;
    QXmppSaslServer *saslServer;

    void checkCredentials(const QByteArray &response);
    QString origin() const;

private:
    QXmppIncomingClient *q;
};

QXmppIncomingClientPrivate::QXmppIncomingClientPrivate(QXmppIncomingClient *qq)
    : idleTimer(nullptr), passwordChecker(nullptr), saslServer(nullptr), q(qq)
{
}

void QXmppIncomingClientPrivate::checkCredentials(const QByteArray &response)
{
    QXmppPasswordRequest request;
    request.setDomain(domain);
    request.setUsername(saslServer->username());

    if (saslServer->mechanism() == "PLAIN") {
        request.setPassword(saslServer->password());

        QXmppPasswordReply *reply = passwordChecker->checkPassword(request);
        reply->setParent(q);
        reply->setProperty("__sasl_raw", response);
        QObject::connect(reply, &QXmppPasswordReply::finished,
                         q, &QXmppIncomingClient::onPasswordReply);
    } else if (saslServer->mechanism() == "DIGEST-MD5") {
        QXmppPasswordReply *reply = passwordChecker->getDigest(request);
        reply->setParent(q);
        reply->setProperty("__sasl_raw", response);
        QObject::connect(reply, &QXmppPasswordReply::finished,
                         q, &QXmppIncomingClient::onDigestReply);
    }
}

QString QXmppIncomingClientPrivate::origin() const
{
    QSslSocket *socket = q->socket();
    if (socket) {
        return socket->peerAddress().toString() + " " + QString::number(socket->peerPort());
    } else {
        return "<unknown>";
    }
}

/// Constructs a new incoming client stream.
///
/// \param socket The socket for the XMPP stream.
/// \param domain The local domain.
/// \param parent The parent QObject for the stream (optional).
///

QXmppIncomingClient::QXmppIncomingClient(QSslSocket *socket, const QString &domain, QObject *parent)
    : QXmppStream(parent)
{

    d = new QXmppIncomingClientPrivate(this);
    d->domain = domain;

    if (socket) {
        connect(socket, &QAbstractSocket::disconnected,
                this, &QXmppIncomingClient::onSocketDisconnected);

        setSocket(socket);
    }

    info(QString("Incoming client connection from %1").arg(d->origin()));

    // create inactivity timer
    d->idleTimer = new QTimer(this);
    d->idleTimer->setSingleShot(true);
    connect(d->idleTimer, &QTimer::timeout,
            this, &QXmppIncomingClient::onTimeout);
}

/// Destroys the current stream.
///

QXmppIncomingClient::~QXmppIncomingClient()
{
    delete d;
}

/// Returns true if the socket is connected, the client is authenticated
/// and a resource is bound.
///

bool QXmppIncomingClient::isConnected() const
{
    return QXmppStream::isConnected() &&
        !d->jid.isEmpty() &&
        !d->resource.isEmpty();
}

/// Returns the client's JID.
///

QString QXmppIncomingClient::jid() const
{
    return d->jid;
}

/// Sets the number of seconds after which a client will be disconnected
/// for inactivity.

void QXmppIncomingClient::setInactivityTimeout(int secs)
{
    d->idleTimer->stop();
    d->idleTimer->setInterval(secs * 1000);
    if (d->idleTimer->interval()) {
        d->idleTimer->start();
    }
}

/// Sets the password checker used to verify client credentials.
///
/// \param checker
///

void QXmppIncomingClient::setPasswordChecker(QXmppPasswordChecker *checker)
{
    d->passwordChecker = checker;
}

/// \cond
void QXmppIncomingClient::handleStream(const QDomElement &streamElement)
{
    if (d->idleTimer->interval()) {
        d->idleTimer->start();
    }
    if (d->saslServer != nullptr) {
        delete d->saslServer;
        d->saslServer = nullptr;
    }

    // start stream
    const QByteArray sessionId = QXmppUtils::generateStanzaHash().toLatin1();
    QString response = QString("<?xml version='1.0'?><stream:stream"
                               " xmlns=\"%1\" xmlns:stream=\"%2\""
                               " id=\"%3\" from=\"%4\" version=\"1.0\" xml:lang=\"en\">")
                           .arg(
                               ns_client,
                               ns_stream,
                               sessionId,
                               d->domain.toLatin1());
    sendData(response.toUtf8());

    // check requested domain
    if (streamElement.attribute("to") != d->domain) {
        QString response = QString("<stream:error>"
                                   "<host-unknown xmlns=\"urn:ietf:params:xml:ns:xmpp-streams\"/>"
                                   "<text xmlns=\"urn:ietf:params:xml:ns:xmpp-streams\">"
                                   "This server does not serve %1"
                                   "</text>"
                                   "</stream:error>")
                               .arg(streamElement.attribute("to"));
        sendData(response.toUtf8());
        disconnectFromHost();
        return;
    }

    // send stream features
    QXmppStreamFeatures features;
    if (socket() && !socket()->isEncrypted() && !socket()->localCertificate().isNull() && !socket()->privateKey().isNull()) {
        features.setTlsMode(QXmppStreamFeatures::Enabled);
    }
    if (!d->jid.isEmpty()) {
        features.setBindMode(QXmppStreamFeatures::Required);
        features.setSessionMode(QXmppStreamFeatures::Enabled);
    } else if (d->passwordChecker) {
        QStringList mechanisms;
        mechanisms << "PLAIN";
        if (d->passwordChecker->hasGetPassword()) {
            mechanisms << "DIGEST-MD5";
        }
        features.setAuthMechanisms(mechanisms);
    }
    sendPacket(features);
}

void QXmppIncomingClient::handleStanza(const QDomElement &nodeRecv)
{
    const QString ns = nodeRecv.namespaceURI();

    if (d->idleTimer->interval()) {
        d->idleTimer->start();
    }

    if (QXmppStartTlsPacket::isStartTlsPacket(nodeRecv, QXmppStartTlsPacket::StartTls)) {
        sendPacket(QXmppStartTlsPacket(QXmppStartTlsPacket::Proceed));
        socket()->flush();
        socket()->startServerEncryption();
        return;
    } else if (ns == ns_sasl) {
        if (!d->passwordChecker) {
            warning("Cannot perform authentication, no password checker");
            sendPacket(QXmppSaslFailure("temporary-auth-failure"));
            disconnectFromHost();
            return;
        }

        if (nodeRecv.tagName() == QLatin1String("auth")) {
            QXmppSaslAuth auth;
            auth.parse(nodeRecv);

            d->saslServer = QXmppSaslServer::create(auth.mechanism(), this);
            if (!d->saslServer) {
                sendPacket(QXmppSaslFailure("invalid-mechanism"));
                disconnectFromHost();
                return;
            }

            d->saslServer->setRealm(d->domain.toUtf8());

            QByteArray challenge;
            QXmppSaslServer::Response result = d->saslServer->respond(auth.value(), challenge);

            if (result == QXmppSaslServer::InputNeeded) {
                // check credentials
                d->checkCredentials(auth.value());
            } else if (result == QXmppSaslServer::Challenge) {
                sendPacket(QXmppSaslChallenge(challenge));
            } else {
                // FIXME: what condition?
                sendPacket(QXmppSaslFailure());
                disconnectFromHost();
                return;
            }
        } else if (nodeRecv.tagName() == QLatin1String("response")) {
            QXmppSaslResponse response;
            response.parse(nodeRecv);

            if (!d->saslServer) {
                warning("SASL response received, but no mechanism selected");
                sendPacket(QXmppSaslFailure());
                disconnectFromHost();
                return;
            }

            QByteArray challenge;
            QXmppSaslServer::Response result = d->saslServer->respond(response.value(), challenge);
            if (result == QXmppSaslServer::InputNeeded) {
                // check credentials
                d->checkCredentials(response.value());
            } else if (result == QXmppSaslServer::Succeeded) {
                // authentication succeeded
                d->jid = QString("%1@%2").arg(d->saslServer->username(), d->domain);
                info(QString("Authentication succeeded for '%1' from %2").arg(d->jid, d->origin()));
                Q_EMIT updateCounter("incoming-client.auth.success");
                sendPacket(QXmppSaslSuccess());
                handleStart();
            } else {
                // FIXME: what condition?
                sendPacket(QXmppSaslFailure());
                disconnectFromHost();
            }
        }
    } else if (ns == ns_client) {
        if (nodeRecv.tagName() == QLatin1String("iq")) {
            const QString type = nodeRecv.attribute("type");
            if (QXmppBindIq::isBindIq(nodeRecv) && type == QLatin1String("set")) {
                QXmppBindIq bindSet;
                bindSet.parse(nodeRecv);
                d->resource = bindSet.resource().trimmed();
                if (d->resource.isEmpty()) {
                    d->resource = QXmppUtils::generateStanzaHash();
                }
                d->jid = QString("%1/%2").arg(QXmppUtils::jidToBareJid(d->jid), d->resource);

                QXmppBindIq bindResult;
                bindResult.setType(QXmppIq::Result);
                bindResult.setId(bindSet.id());
                bindResult.setJid(d->jid);
                sendPacket(bindResult);

                // bound
                Q_EMIT connected();
                return;
            } else if (QXmppSessionIq::isSessionIq(nodeRecv) && type == QLatin1String("set")) {
                QXmppSessionIq sessionSet;
                sessionSet.parse(nodeRecv);

                QXmppIq sessionResult;
                sessionResult.setType(QXmppIq::Result);
                sessionResult.setId(sessionSet.id());
                sessionResult.setTo(d->jid);
                sendPacket(sessionResult);
                return;
            }
        }

        // check the sender is legitimate
        const QString from = nodeRecv.attribute("from");
        if (!from.isEmpty() && from != d->jid && from != QXmppUtils::jidToBareJid(d->jid)) {
            warning(QString("Received a stanza from unexpected JID %1").arg(from));
            return;
        }

        // process unhandled stanzas
        if (nodeRecv.tagName() == QLatin1String("iq") ||
            nodeRecv.tagName() == QLatin1String("message") ||
            nodeRecv.tagName() == QLatin1String("presence")) {
            QDomElement nodeFull(nodeRecv);

            // if the sender is empty, set it to the appropriate JID
            if (nodeFull.attribute("from").isEmpty()) {
                if (nodeFull.tagName() == QLatin1String("presence") &&
                    (nodeFull.attribute("type") == QLatin1String("subscribe") ||
                     nodeFull.attribute("type") == QLatin1String("subscribed"))) {
                    nodeFull.setAttribute("from", QXmppUtils::jidToBareJid(d->jid));
                } else {
                    nodeFull.setAttribute("from", d->jid);
                }
            }

            // if the recipient is empty, set it to the local domain
            if (nodeFull.attribute("to").isEmpty()) {
                nodeFull.setAttribute("to", d->domain);
            }

            // emit stanza for processing by server
            Q_EMIT elementReceived(nodeFull);
        }
    }
}
/// \endcond

void QXmppIncomingClient::onDigestReply()
{
    auto *reply = qobject_cast<QXmppPasswordReply *>(sender());
    if (!reply) {
        return;
    }
    reply->deleteLater();

    if (reply->error() == QXmppPasswordReply::TemporaryError) {
        warning(QString("Temporary authentication failure for '%1' from %2").arg(d->saslServer->username(), d->origin()));
        Q_EMIT updateCounter("incoming-client.auth.temporary-auth-failure");
        sendPacket(QXmppSaslFailure("temporary-auth-failure"));
        disconnectFromHost();
        return;
    }

    QByteArray challenge;
    d->saslServer->setPasswordDigest(reply->digest());

    QXmppSaslServer::Response result = d->saslServer->respond(reply->property("__sasl_raw").toByteArray(), challenge);
    if (result != QXmppSaslServer::Challenge) {
        warning(QString("Authentication failed for '%1' from %2").arg(d->saslServer->username(), d->origin()));
        Q_EMIT updateCounter("incoming-client.auth.not-authorized");
        sendPacket(QXmppSaslFailure("not-authorized"));
        disconnectFromHost();
        return;
    }

    // send new challenge
    sendPacket(QXmppSaslChallenge(challenge));
}

void QXmppIncomingClient::onPasswordReply()
{
    auto *reply = qobject_cast<QXmppPasswordReply *>(sender());
    if (!reply) {
        return;
    }
    reply->deleteLater();

    const QString jid = QString("%1@%2").arg(d->saslServer->username(), d->domain);
    switch (reply->error()) {
    case QXmppPasswordReply::NoError:
        d->jid = jid;
        info(QString("Authentication succeeded for '%1' from %2").arg(d->jid, d->origin()));
        Q_EMIT updateCounter("incoming-client.auth.success");
        sendPacket(QXmppSaslSuccess());
        handleStart();
        break;
    case QXmppPasswordReply::AuthorizationError:
        warning(QString("Authentication failed for '%1' from %2").arg(jid, d->origin()));
        Q_EMIT updateCounter("incoming-client.auth.not-authorized");
        sendPacket(QXmppSaslFailure("not-authorized"));
        disconnectFromHost();
        break;
    case QXmppPasswordReply::TemporaryError:
        warning(QString("Temporary authentication failure for '%1' from %2").arg(jid, d->origin()));
        Q_EMIT updateCounter("incoming-client.auth.temporary-auth-failure");
        sendPacket(QXmppSaslFailure("temporary-auth-failure"));
        disconnectFromHost();
        break;
    }
}

void QXmppIncomingClient::onSocketDisconnected()
{
    info(QString("Socket disconnected for '%1' from %2").arg(d->jid, d->origin()));
    Q_EMIT disconnected();
}

void QXmppIncomingClient::onTimeout()
{
    warning(QString("Idle timeout for '%1' from %2").arg(d->jid, d->origin()));
    disconnectFromHost();

    // make sure disconnected() gets emitted no matter what
    QTimer::singleShot(30, this, &QXmppStream::disconnected);
}
