/*
 * Copyright (C) 2008-2021 The QXmpp developers
 *
 * Author:
 *  Jeremy Lainé
 *
 * Source:
 *  https://github.com/qxmpp-project/qxmpp
 *
 * This file is a part of QXmpp library.
 *
 * 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.
 *
 */

#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()));
                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
                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
            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()));
        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()));
        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()));
        updateCounter("incoming-client.auth.success");
        sendPacket(QXmppSaslSuccess());
        handleStart();
        break;
    case QXmppPasswordReply::AuthorizationError:
        warning(QString("Authentication failed for '%1' from %2").arg(jid, d->origin()));
        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()));
        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()));
    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);
}
