// SPDX-FileCopyrightText: 2023 Tibor Csötönyi // SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later #include "QXmppJingleMessageInitiationManager.h" #include "QXmppClient.h" #include "QXmppConstants_p.h" #include "QXmppMessage.h" #include "QXmppPromise.h" #include "QXmppUtils.h" #include #include using namespace QXmpp; using Jmi = QXmppJingleMessageInitiation; using JmiManager = QXmppJingleMessageInitiationManager; using JmiElement = QXmppJingleMessageInitiationElement; using JmiType = JmiElement::Type; class QXmppJingleMessageInitiationPrivate { public: QXmppJingleMessageInitiationPrivate(JmiManager *manager) : manager(manager) { } QXmppTask request(JmiElement &&jmiElement); QXmppJingleMessageInitiationManager *manager; QString id; QString callPartnerJid; bool isProceeded { false }; }; /// /// \brief Creates a Jingle Message Initiation request based on given type. /// \param type The request type (proceed, accept, reject, retract, finish). /// QXmppTask QXmppJingleMessageInitiationPrivate::request(JmiElement &&jmiElement) { jmiElement.setId(id); return manager->sendMessage(jmiElement, callPartnerJid); } /// /// \class QXmppJingleMessageInitiation /// /// \brief The QXmppJingleMessageInitiation class holds information about the JMI element in the /// current context. /// /// \since QXmpp 1.6 /// /// /// \brief Constructs a Jingle Message Initiation object. /// QXmppJingleMessageInitiation::QXmppJingleMessageInitiation(QXmppJingleMessageInitiationManager *manager) : d(new QXmppJingleMessageInitiationPrivate(manager)) { } QXmppJingleMessageInitiation::~QXmppJingleMessageInitiation() = default; /// /// Creates a JMI element of type "ringing" and sends a request containing the element. /// QXmppTask QXmppJingleMessageInitiation::ring() { QXmppJingleMessageInitiationElement jmiElement; jmiElement.setType(JmiType::Ringing); return d->request(std::move(jmiElement)); } /// /// Creates a JMI element of type "proceed" and sends a request containing the element. /// QXmppTask QXmppJingleMessageInitiation::proceed() { QXmppJingleMessageInitiationElement jmiElement; jmiElement.setType(JmiType::Proceed); return d->request(std::move(jmiElement)); } /// /// Creates a JMI element of type "reject" and sends a request containing the element. /// The default reason tag/type will be "busy" with text "Busy". /// /// \param reason Reason object for reject element /// \param containsTieBreak Whether the reject element contains a tie-break tag or not /// QXmppTask QXmppJingleMessageInitiation::reject(std::optional reason, bool containsTieBreak) { JmiElement jmiElement; jmiElement.setType(JmiType::Reject); if (!reason) { reason = QXmppJingleReason(); reason->setType(QXmppJingleReason::Busy); reason->setText(QStringLiteral("Busy")); } jmiElement.setReason(reason); jmiElement.setContainsTieBreak(containsTieBreak); return d->request(std::move(jmiElement)); } /// /// Creates a JMI element of type "retract" and sends a request containing the element. /// The default reason tag/type will be "cancel" with text "Retracted". /// /// \param reason Reason object for retract element /// \param containsTieBreak Whether the retract element contains a tie-break tag or not /// QXmppTask QXmppJingleMessageInitiation::retract(std::optional reason, bool containsTieBreak) { JmiElement jmiElement; jmiElement.setType(JmiType::Retract); if (!reason) { reason = QXmppJingleReason(); reason->setType(QXmppJingleReason::Cancel); reason->setText(QStringLiteral("Retracted")); } jmiElement.setReason(reason); jmiElement.setContainsTieBreak(containsTieBreak); return d->request(std::move(jmiElement)); } /// /// Creates a JMI element of type "finish" and sends a request containing the element. /// The default reason type/tag will be "success" with text "Success". /// /// \param reason Reason object for finish element /// \param migratedTo JMI id the session has been migrated to /// QXmppTask QXmppJingleMessageInitiation::finish(std::optional reason, const QString &migratedTo) { JmiElement jmiElement; jmiElement.setType(JmiType::Finish); if (!reason) { reason = QXmppJingleReason(); reason->setType(QXmppJingleReason::Success); reason->setText(QStringLiteral("Success")); } jmiElement.setReason(reason); jmiElement.setMigratedTo(migratedTo); return d->request(std::move(jmiElement)); } /// /// Returns the JMI ID. /// QString QXmppJingleMessageInitiation::id() const { return d->id; } /// /// Sets the JMI ID. /// void QXmppJingleMessageInitiation::setId(const QString &id) { d->id = id; } /// /// Sets the call partner's bare JID. /// /// Normally, the JMI ID would be sufficient in order to differentiate the JMIs. /// However, attackers pretending to be the call partner can be mitigated by caching the call /// partner's JID. /// /// \param callPartnerJid bare JID of the call partner /// void QXmppJingleMessageInitiation::setCallPartnerJid(const QString &callPartnerJid) { d->callPartnerJid = callPartnerJid; } /// /// Returns the call partner's bare JID. /// /// \return the call partner's bare JID. /// QString QXmppJingleMessageInitiation::callPartnerJid() const { return d->callPartnerJid; } /// /// Returns the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been /// proceeded. /// bool QXmppJingleMessageInitiation::isProceeded() const { return d->isProceeded; } /// /// Sets the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been /// proceeded. /// void QXmppJingleMessageInitiation::setIsProceeded(bool isProceeded) { d->isProceeded = isProceeded; } /// /// \fn QXmppJingleMessageInitiation::ringing() /// /// Emitted when a propose request was accepted and the device starts ringing. /// /// /// \fn QXmppJingleMessageInitiation::proceeded(const QString &, const QString &) /// /// Emitted when a propose request was successfully processed and accepted. /// /// \param id belonging JMI id /// \param callPartnerResource resource of the call partner about to be called /// /// /// \fn QXmppJingleMessageInitiation::closed(const Result &) /// /// Emitted when a call was ended either through rejection, retraction, finish or an error. /// /// \param result close reason /// class QXmppJingleMessageInitiationManagerPrivate { public: QVector> jmis; }; /// /// \typedef QXmppJingleMessageInitiationManager::ProposeResult /// /// Contains JMI object or an error if sending the propose message failed. /// /// /// \class QXmppJingleMessageInitiationManager /// /// \brief The QXmppJingleMessageInitiationManager class makes it possible to retrieve /// Jingle Message Initiation elements as defined by \xep{0353, Jingle Message Initiation}. /// /// \since QXmpp 1.6 /// QXmppJingleMessageInitiationManager::QXmppJingleMessageInitiationManager() : d(std::make_unique()) { } QXmppJingleMessageInitiationManager::~QXmppJingleMessageInitiationManager() = default; /// \cond QStringList QXmppJingleMessageInitiationManager::discoveryFeatures() const { return { ns_jingle_message_initiation }; } /// \endcond /// /// Creates a proposal JMI element and passes it as a message. /// QXmppTask QXmppJingleMessageInitiationManager::propose(const QString &callPartnerJid, const QXmppJingleDescription &description) { QXmppPromise promise; QXmppJingleMessageInitiationElement jmiElement; jmiElement.setType(JmiType::Propose); jmiElement.setId(QXmppUtils::generateStanzaUuid()); jmiElement.setDescription(description); sendMessage(jmiElement, callPartnerJid).then(this, [this, promise, callPartnerJid](SendResult result) mutable { if (auto error = std::get_if(&result)) { warning(u"Error sending Jingle Message Initiation proposal: " % error->description); promise.finish(*error); } else { promise.finish(addJmi(callPartnerJid)); } }); return promise.task(); } /// \cond bool QXmppJingleMessageInitiationManager::handleMessage(const QXmppMessage &message) { // JMI messages must be of type "chat" and contain a hint. if (message.type() != QXmppMessage::Chat || !message.hasHint(QXmppMessage::Store)) { return false; } // Only continue if the message contains a JMI element. if (auto jmiElement = message.jingleMessageInitiationElement()) { return handleJmiElement(std::move(*jmiElement), message.from()); } return false; } void QXmppJingleMessageInitiationManager::setClient(QXmppClient *client) { QXmppClientExtension::setClient(client); } /// \endcond /// /// Lets the client send a message to user with given callPartnerJid containing the JMI element. /// /// \param jmiElement the JMI element to be passed /// \param callPartnerJid bare JID of the call partner /// QXmppTask QXmppJingleMessageInitiationManager::sendMessage(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid) { QXmppMessage message; message.setTo(callPartnerJid); message.addHint(QXmppMessage::Store); message.setJingleMessageInitiationElement(jmiElement); return client()->send(std::move(message)); } /// /// Removes a JMI object from the JMIs vector. /// /// \param jmi object to be removed /// void QXmppJingleMessageInitiationManager::clear(const std::shared_ptr &jmi) { d->jmis.erase( std::remove_if( d->jmis.begin(), d->jmis.end(), [&jmi](const auto &storedJmi) { return jmi->id() == storedJmi->id() && jmi->callPartnerJid() == storedJmi->callPartnerJid(); }), d->jmis.end()); } /// /// Removes all JMI objects from the JMI vector. /// void QXmppJingleMessageInitiationManager::clearAll() { d->jmis.clear(); } bool QXmppJingleMessageInitiationManager::handleJmiElement(QXmppJingleMessageInitiationElement &&jmiElement, const QString &senderJid) { auto jmiElementId = jmiElement.id(); auto callPartnerJid = QXmppUtils::jidToBareJid(senderJid); // Check if there's already a JMI object with jmiElementId and callPartnerJid in JMIs vector. // That means that a JMI has already been created with given (J)IDs. auto itr = std::find_if(d->jmis.begin(), d->jmis.end(), [&jmiElementId, &callPartnerJid](const auto &jmi) { return jmi->id() == jmiElementId && jmi->callPartnerJid() == callPartnerJid; }); if (itr != d->jmis.end()) { return handleExistingJmi(*itr, jmiElement, QXmppUtils::jidToResource(senderJid)); } if (jmiElement.type() == JmiType::Propose) { return handleProposeJmiElement(jmiElement, callPartnerJid); } return false; } /// /// Handles a JMI object which already exists in the JMIs vector. /// /// \param existingJmi JMI object to be handled /// \param jmiElement JMI element to be processed with the JMI object /// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.) /// \return success (true) or failure /// bool QXmppJingleMessageInitiationManager::handleExistingJmi(const std::shared_ptr &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource) { switch (jmiElement.type()) { case JmiType::Ringing: Q_EMIT existingJmi->ringing(); return true; case JmiType::Proceed: Q_EMIT existingJmi->proceeded(jmiElement.id(), callPartnerResource); existingJmi->setIsProceeded(true); return true; case JmiType::Reject: Q_EMIT existingJmi->closed( Jmi::Rejected { jmiElement.reason(), jmiElement.containsTieBreak() }); return true; case JmiType::Retract: Q_EMIT existingJmi->closed( Jmi::Retracted { jmiElement.reason(), jmiElement.containsTieBreak() }); return true; case JmiType::Finish: existingJmi->finish(jmiElement.reason(), jmiElement.migratedTo()); Q_EMIT existingJmi->closed( Jmi::Finished { jmiElement.reason(), jmiElement.migratedTo() }); return true; default: return false; } } /// /// Handles a propose JMI element. /// /// \param jmiElement to be handled /// \param callPartnerJid bare JID of the call partner /// \return success (true) or failure /// bool QXmppJingleMessageInitiationManager::handleProposeJmiElement(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid) { // Check if there's already a JMI object with provided callPartnerJid in JMIs vector. // That means that a propose has already been sent. auto itr = std::find_if( d->jmis.cbegin(), d->jmis.cend(), [&callPartnerJid](const auto &jmi) { return jmi->callPartnerJid() == callPartnerJid; }); // Tie break case or usual JMI proposal? if (itr != d->jmis.end()) { return handleTieBreak(*itr, jmiElement, callPartnerJid); } Q_EMIT proposed(addJmi(callPartnerJid), jmiElement.id(), jmiElement.description()); return true; } /// /// Handles a tie break case as defined in https://xmpp.org/extensions/xep-0353.html#tie-breaking. /// \param existingJmi existing JMI object to be handled /// \param jmiElement JMI element to be processed with existing JMI object /// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.) /// \return success (true) or failure /// bool QXmppJingleMessageInitiationManager::handleTieBreak(const std::shared_ptr &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource) { // Tie break -> session is set to be expired QXmppJingleReason reason; reason.setType(QXmppJingleReason::Expired); // Existing (proceeded) or non-existing session? if (existingJmi->isProceeded()) { return handleExistingSession(existingJmi, jmiElement.id()); } // Tie break in propose state (no existing session) - two parties try calling each other // at the same time, the proposal with the lower ID overrules the other one. return handleNonExistingSession(existingJmi, jmiElement.id(), callPartnerResource); } /// /// Device switch: session already exists and will be migrated to new device with id jmiElementId. /// /// \param existingJmi Current JMI object /// \param jmiElementId New (counterpart's) session JMI element ID /// bool QXmppJingleMessageInitiationManager::handleExistingSession(const std::shared_ptr &existingJmi, const QString &jmiElementId) { // Old session will be finished with reason "Expired". QXmppJingleReason reason; reason.setType(QXmppJingleReason::Expired); reason.setText(QStringLiteral("Session migrated")); // Tell the old session to be finished. Q_EMIT existingJmi->closed(Jmi::Finished { reason, jmiElementId }); existingJmi->finish(reason, jmiElementId).then(this, [this, existingJmi, jmiElementId](SendResult result) { if (auto *error = std::get_if(&result)) { Q_EMIT existingJmi->closed(*error); } else { // Then, proceed (accept) the new proposal and set the JMI ID // to the ID of the received JMI element. existingJmi->setId(jmiElementId); existingJmi->proceed().then(this, [existingJmi](SendResult result) { if (auto *error = std::get_if(&result)) { Q_EMIT existingJmi->closed(*error); } else { // The session is now closed as it is finished. existingJmi->setIsProceeded(true); } }); } }); return true; } /// /// \brief Tie break in propose state (no existing session) - two parties try calling each other /// at the same time, the proposal with the lower ID overrules the other one. /// /// \param existingJmi Current JMI object /// \param jmiElementId Counterpart's JMI element ID /// bool QXmppJingleMessageInitiationManager::handleNonExistingSession(const std::shared_ptr &existingJmi, const QString &jmiElementId, const QString &callPartnerResource) { QXmppJingleReason reason; reason.setType(QXmppJingleReason::Expired); reason.setText(QStringLiteral("Tie-Break")); if (QUuid::fromString(existingJmi->id()) < QUuid::fromString(jmiElementId)) { // Jingle message initiator with lower ID rejects the other proposal. existingJmi->setId(jmiElementId); existingJmi->reject(std::move(reason), true).then(this, [existingJmi](auto result) { if (auto *error = std::get_if(&result)) { Q_EMIT existingJmi->closed(*error); } }); } else { // Jingle message initiator with higher ID retracts its proposal. existingJmi->retract(std::move(reason), true).then(this, [this, existingJmi, jmiElementId, callPartnerResource](auto result) { if (auto error = std::get_if(&result)) { Q_EMIT existingJmi->closed(*error); } else { // Afterwards, JMI ID is changed to lower ID. existingJmi->setId(jmiElementId); // Finally, the call is being accepted. existingJmi->proceed().then(this, [existingJmi, jmiElementId, callPartnerResource](SendResult result) { if (auto *error = std::get_if(&result)) { Q_EMIT existingJmi->closed(*error); } else { existingJmi->setIsProceeded(true); Q_EMIT existingJmi->proceeded(jmiElementId, callPartnerResource); } }); } }); } return true; } /// /// Adds a JMI object to the JMIs vector and sets the bare JID of the call partner in the JMI object. /// \param callPartnerJid bare JID of the call partner /// \return The newly created JMI /// std::shared_ptr QXmppJingleMessageInitiationManager::addJmi(const QString &callPartnerJid) { auto jmi { std::make_shared(this) }; jmi->setCallPartnerJid(callPartnerJid); d->jmis.append(jmi); return jmi; } /// /// Returns the JMIs vector. /// const QVector> &QXmppJingleMessageInitiationManager::jmis() const { return d->jmis; } /// /// \fn QXmppJingleMessageInitiationManager::proposed(const std::shared_ptr &, const QString &, const QXmppJingleDescription &) /// /// Emitted when a call has been proposed. /// /// \param jmi Jingle Message Initiation object of proposed session /// \param id JMI element id /// \param description JMI element's description containing media type (i.e., audio, video) ///