diff options
| -rw-r--r-- | .github/workflows/tests.yml | 12 | ||||
| -rw-r--r-- | src/base/QXmppMessage.cpp | 2 | ||||
| -rw-r--r-- | src/base/QXmppStream.cpp | 18 | ||||
| -rw-r--r-- | src/base/QXmppStream.h | 2 | ||||
| -rw-r--r-- | src/client/QXmppCarbonManagerV2.cpp | 5 | ||||
| -rw-r--r-- | src/client/QXmppIqHandling.h | 10 | ||||
| -rw-r--r-- | src/client/QXmppMamManager.cpp | 230 | ||||
| -rw-r--r-- | src/client/QXmppOutgoingClient.cpp | 8 | ||||
| -rw-r--r-- | src/omemo/QXmppOmemoManager.cpp | 36 | ||||
| -rw-r--r-- | src/omemo/QXmppOmemoManager_p.cpp | 61 | ||||
| -rw-r--r-- | src/omemo/QXmppOmemoManager_p.h | 1 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | tests/qxmppentitytimemanager/tst_qxmppentitytimemanager.cpp | 68 | ||||
| -rw-r--r-- | tests/qxmppmessage/tst_qxmppmessage.cpp | 2 | ||||
| -rw-r--r-- | tests/qxmppversionmanager/tst_qxmppversionmanager.cpp | 68 |
15 files changed, 398 insertions, 127 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb81a3ee..da4ba743 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,6 +49,18 @@ jobs: - uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Install Qt + uses: jurplel/install-qt-action@v2 + - name: Run tests + run: | + ${env:PATH} += ";D:/a/qxmpp/qxmpp/src/Debug" + cmake . && cmake --build . + # ctest + # ctest --rerun-failed --output-on-failure xmllint: runs-on: ubuntu-latest steps: diff --git a/src/base/QXmppMessage.cpp b/src/base/QXmppMessage.cpp index c0923d22..9527f5ed 100644 --- a/src/base/QXmppMessage.cpp +++ b/src/base/QXmppMessage.cpp @@ -1659,7 +1659,7 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode writer->writeStartElement(QStringLiteral("encryption")); writer->writeDefaultNamespace(ns_eme); writer->writeAttribute(QStringLiteral("namespace"), d->encryptionMethod); - helperToXmlAddAttribute(writer, QStringLiteral("name"), d->encryptionName); + helperToXmlAddAttribute(writer, QStringLiteral("name"), encryptionName()); writer->writeEndElement(); } diff --git a/src/base/QXmppStream.cpp b/src/base/QXmppStream.cpp index 0ea87cbd..7c49c762 100644 --- a/src/base/QXmppStream.cpp +++ b/src/base/QXmppStream.cpp @@ -197,7 +197,7 @@ QXmppTask<QXmpp::SendResult> QXmppStream::send(QXmppPacket &&packet, bool &writt /// /// \since QXmpp 1.5 /// -QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq) +QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq, const QString &to) { using namespace QXmpp; @@ -212,7 +212,7 @@ QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq) iq.setId(QXmppUtils::generateStanzaUuid()); } - return sendIq(QXmppPacket(iq), iq.id(), iq.to()); + return sendIq(QXmppPacket(iq), iq.id(), to); } /// @@ -466,10 +466,18 @@ bool QXmppStream::handleIqResponse(const QDomElement &stanza) return false; } - if (auto itr = d->runningIqs.find(stanza.attribute(QStringLiteral("id"))); + const auto id = stanza.attribute(QStringLiteral("id")); + if (auto itr = d->runningIqs.find(id); itr != d->runningIqs.end()) { - if (stanza.attribute("from") != itr.value().jid) { - warning(QStringLiteral("Received IQ response to one of our requests from wrong sender. Ignoring.")); + const auto expectedFrom = itr.value().jid; + // Check that the sender of the response matches the recipient of the request. + // Stanzas coming from the server on behalf of the user's account must have no "from" + // attribute or have it set to the user's bare JID. + // If 'from' is empty, the IQ has been sent by the server. In this case we don't need to + // do the check as we trust the server anyways. + if (const auto from = stanza.attribute("from"); !from.isEmpty() && from != expectedFrom) { + warning(QStringLiteral("Ignored received IQ response to request '%1' because of wrong sender '%2' instead of expected sender '%3'") + .arg(id, from, expectedFrom)); return false; } diff --git a/src/base/QXmppStream.h b/src/base/QXmppStream.h index 55dc7967..a7584366 100644 --- a/src/base/QXmppStream.h +++ b/src/base/QXmppStream.h @@ -47,7 +47,7 @@ public: QXmppTask<QXmpp::SendResult> send(QXmppPacket &&); using IqResult = std::variant<QDomElement, QXmppError>; - QXmppTask<IqResult> sendIq(QXmppIq &&); + QXmppTask<IqResult> sendIq(QXmppIq &&, const QString &to); QXmppTask<IqResult> sendIq(QXmppPacket &&, const QString &id, const QString &to); void cancelOngoingIqs(); bool hasIqId(const QString &id) const; diff --git a/src/client/QXmppCarbonManagerV2.cpp b/src/client/QXmppCarbonManagerV2.cpp index 7959cab3..97831731 100644 --- a/src/client/QXmppCarbonManagerV2.cpp +++ b/src/client/QXmppCarbonManagerV2.cpp @@ -18,10 +18,9 @@ using namespace QXmpp::Private; class CarbonEnableIq : public QXmppIq { public: - CarbonEnableIq(const QString &jid) + CarbonEnableIq() : QXmppIq() { - setTo(jid); setType(QXmppIq::Set); } @@ -165,7 +164,7 @@ void QXmppCarbonManagerV2::enableCarbons() return; } - client()->sendIq(CarbonEnableIq(client()->configuration().jidBare())).then(this, [this](QXmppClient::IqResult domResult) { + client()->sendIq(CarbonEnableIq()).then(this, [this](QXmppClient::IqResult domResult) { if (auto err = parseIq(std::move(domResult))) { warning("Could not enable message carbons: " % err->description); } else { diff --git a/src/client/QXmppIqHandling.h b/src/client/QXmppIqHandling.h index 7535bc30..63eba306 100644 --- a/src/client/QXmppIqHandling.h +++ b/src/client/QXmppIqHandling.h @@ -95,12 +95,10 @@ namespace Private { iq.parse(element); iq.setE2eeMetadata(e2eeMetadata); - processHandleIqResult( - client, - iq.id(), - iq.from(), - e2eeMetadata, - invokeIqHandler(std::forward<Handler>(handler), std::move(iq))); + auto id = iq.id(), from = iq.from(); + + processHandleIqResult(client, id, from, e2eeMetadata, + invokeIqHandler(std::forward<Handler>(handler), std::move(iq))); return true; } return false; diff --git a/src/client/QXmppMamManager.cpp b/src/client/QXmppMamManager.cpp index c39a3411..fe0735c7 100644 --- a/src/client/QXmppMamManager.cpp +++ b/src/client/QXmppMamManager.cpp @@ -9,29 +9,98 @@ #include "QXmppConstants_p.h" #include "QXmppDataForm.h" #include "QXmppE2eeExtension.h" -#include "QXmppFutureUtils_p.h" #include "QXmppMamIq.h" #include "QXmppMessage.h" +#include "QXmppPromise.h" #include "QXmppUtils.h" #include <unordered_map> #include <QDomElement> +using namespace QXmpp; using namespace QXmpp::Private; +template<typename T, typename Converter> +auto transform(const T &input, Converter convert) +{ + using Output = std::decay_t<decltype(convert(*input.begin()))>; + QVector<Output> output; + output.reserve(input.size()); + std::transform(input.begin(), input.end(), std::back_inserter(output), std::move(convert)); + return output; +} + +template<typename T> +auto sum(const T &c) +{ + return std::accumulate(c.begin(), c.end(), 0); +} + +struct MamMessage +{ + QDomElement element; + std::optional<QDateTime> delay; +}; + +enum EncryptedType { Unencrypted, + Encrypted }; + +QXmppMessage parseMamMessage(const MamMessage &mamMessage, EncryptedType encrypted) +{ + QXmppMessage m; + m.parse(mamMessage.element, encrypted == Encrypted ? ScePublic : SceAll); + if (mamMessage.delay) { + m.setStamp(*mamMessage.delay); + } + return m; +} + +std::optional<std::tuple<MamMessage, QString>> parseMamMessageResult(const QDomElement &messageEl) +{ + auto resultElement = messageEl.firstChildElement("result"); + if (resultElement.isNull() || resultElement.namespaceURI() != ns_mam) { + return {}; + } + + auto forwardedElement = resultElement.firstChildElement("forwarded"); + if (forwardedElement.isNull() || forwardedElement.namespaceURI() != ns_forwarding) { + return {}; + } + + auto queryId = resultElement.attribute("queryid"); + + auto messageElement = forwardedElement.firstChildElement("message"); + if (messageElement.isNull()) { + return {}; + } + + auto parseDelay = [](const auto &forwardedEl) -> std::optional<QDateTime> { + auto delayEl = forwardedEl.firstChildElement("delay"); + if (!delayEl.isNull() && delayEl.namespaceURI() == ns_delayed_delivery) { + return QXmppUtils::datetimeFromString(delayEl.attribute("stamp")); + } + return {}; + }; + + return { { MamMessage { messageElement, parseDelay(forwardedElement) }, queryId } }; +} + struct RetrieveRequestState { QXmppPromise<QXmppMamManager::RetrieveResult> promise; QXmppMamResultIq iq; - QVector<QXmppMessage> messages; + QVector<MamMessage> messages; + QVector<QXmppMessage> processedMessages; + uint runningDecryptionJobs = 0; void finish() { + Q_ASSERT(messages.count() == processedMessages.count()); promise.finish( QXmppMamManager::RetrievedMessages { std::move(iq), - std::move(messages) }); + std::move(processedMessages) }); } }; @@ -87,28 +156,8 @@ QStringList QXmppMamManager::discoveryFeatures() const bool QXmppMamManager::handleStanza(const QDomElement &element) { if (element.tagName() == "message") { - QDomElement resultElement = element.firstChildElement("result"); - if (!resultElement.isNull() && resultElement.namespaceURI() == ns_mam) { - QDomElement forwardedElement = resultElement.firstChildElement("forwarded"); - QString queryId = resultElement.attribute("queryid"); - - if (forwardedElement.isNull() || forwardedElement.namespaceURI() != ns_forwarding) { - return false; - } - - auto messageElement = forwardedElement.firstChildElement("message"); - auto delayElement = forwardedElement.firstChildElement("delay"); - - if (messageElement.isNull()) { - return false; - } - - QXmppMessage message; - message.parse(messageElement); - if (!delayElement.isNull() && delayElement.namespaceURI() == ns_delayed_delivery) { - const QString stamp = delayElement.attribute("stamp"); - message.setStamp(QXmppUtils::datetimeFromString(stamp)); - } + if (auto result = parseMamMessageResult(element)) { + auto &[message, queryId] = *result; auto itr = d->ongoingRequests.find(queryId.toStdString()); if (itr != d->ongoingRequests.end()) { @@ -116,7 +165,7 @@ bool QXmppMamManager::handleStanza(const QDomElement &element) itr->second.messages.append(std::move(message)); } else { // signal-based API - Q_EMIT archivedMessageReceived(queryId, message); + Q_EMIT archivedMessageReceived(queryId, parseMamMessage(message, Unencrypted)); } return true; } @@ -240,74 +289,97 @@ QString QXmppMamManager::retrieveArchivedMessages(const QString &to, QXmppTask<QXmppMamManager::RetrieveResult> QXmppMamManager::retrieveMessages(const QString &to, const QString &node, const QString &jid, const QDateTime &start, const QDateTime &end, const QXmppResultSetQuery &resultSetQuery) { auto queryIq = buildRequest(to, node, jid, start, end, resultSetQuery); + auto queryId = queryIq.queryId(); + + auto [itr, inserted] = d->ongoingRequests.insert({ queryIq.queryId().toStdString(), RetrieveRequestState() }); + Q_ASSERT(inserted); - auto [itr, _] = d->ongoingRequests.insert({ queryIq.queryId().toStdString(), RetrieveRequestState() }); + // create task here; promise could finish immediately after client()->sendIq() + auto task = itr->second.promise.task(); // retrieve messages - client()->sendIq(std::move(queryIq)).then(this, [this, queryId = queryIq.queryId()](QXmppClient::IqResult result) { + client()->sendIq(std::move(queryIq)).then(this, [this, queryId](QXmppClient::IqResult result) { auto itr = d->ongoingRequests.find(queryId.toStdString()); if (itr == d->ongoingRequests.end()) { return; } + auto &state = itr->second; - if (std::holds_alternative<QDomElement>(result)) { - auto &iq = itr->second.iq; - iq.parse(std::get<QDomElement>(result)); + // handle IQ sending errors + if (std::holds_alternative<QXmppError>(result)) { + state.promise.finish(std::get<QXmppError>(result)); + d->ongoingRequests.erase(itr); + return; + } - if (iq.type() == QXmppIq::Error) { - itr->second.promise.finish(QXmppError { iq.error().text(), iq.error() }); - d->ongoingRequests.erase(itr); - return; - } + // parse IQ + auto &iq = state.iq; + iq.parse(std::get<QDomElement>(result)); - // decrypt encrypted messages - if (auto *e2eeExt = client()->encryptionExtension()) { - auto &messages = itr->second.messages; - auto running = std::make_shared<uint>(0); - // handle case when no message is encrypted - auto hasEncryptedMessages = false; + // handle MAM error result IQ + if (iq.type() == QXmppIq::Error) { + state.promise.finish(QXmppError { iq.error().text(), iq.error() }); + d->ongoingRequests.erase(itr); + return; + } - for (auto i = 0; i < messages.size(); i++) { - if (!e2eeExt->isEncrypted(messages.at(i))) { - continue; - } - hasEncryptedMessages = true; - - auto message = messages.at(i); - (*running)++; - e2eeExt->decryptMessage(std::move(message)).then(this, [this, i, running, queryId](auto result) { - (*running)--; - auto itr = d->ongoingRequests.find(queryId.toStdString()); - if (itr == d->ongoingRequests.end()) { - return; - } - - if (std::holds_alternative<QXmppMessage>(result)) { - itr->second.messages[i] = std::get<QXmppMessage>(std::move(result)); - } else { - warning(QStringLiteral("Error decrypting message.")); - } - if (*running == 0) { - itr->second.finish(); - d->ongoingRequests.erase(itr); - } - }); + // decrypt encrypted messages + if (auto *e2eeExt = client()->encryptionExtension()) { + // initialize processed messages (we need random access because + // decryptMessage() may finish in random order) + state.processedMessages.resize(state.messages.size()); + + // check for encrypted messages (once) + auto messagesEncrypted = transform(state.messages, [&](const auto &m) { + return e2eeExt->isEncrypted(m.element); + }); + auto encryptedCount = sum(messagesEncrypted); + + // We can't do this on the fly (with ++ and --) in the for loop + // because some decryptMessage() jobs could finish instantly + state.runningDecryptionJobs = encryptedCount; + + for (auto i = 0; i < state.messages.size(); i++) { + if (!messagesEncrypted[i]) { + continue; } - if (!hasEncryptedMessages) { - // finish here, no decryptMessage callback will do it - itr->second.finish(); - d->ongoingRequests.erase(itr); - } - } else { - itr->second.finish(); - d->ongoingRequests.erase(itr); + e2eeExt->decryptMessage(parseMamMessage(state.messages.at(i), Encrypted)).then(this, [this, i, queryId](auto result) { + auto itr = d->ongoingRequests.find(queryId.toStdString()); + Q_ASSERT(itr != d->ongoingRequests.end()); + + auto &state = itr->second; + + // store decrypted message, fallback to encrypted message + if (std::holds_alternative<QXmppMessage>(result)) { + state.processedMessages[i] = std::get<QXmppMessage>(std::move(result)); + } else { + warning(QStringLiteral("Error decrypting message.")); + state.processedMessages[i] = parseMamMessage(state.messages[i], Unencrypted); + } + + // finish promise if this was the last job + state.runningDecryptionJobs--; + if (state.runningDecryptionJobs == 0) { + state.finish(); + d->ongoingRequests.erase(itr); + } + }); + } + + // finishing the promise is done after decryptMessage() + if (encryptedCount > 0) { + return; } - } else { - itr->second.promise.finish(std::get<QXmppError>(result)); - d->ongoingRequests.erase(itr); } + + // for the case without decryption, finish here + state.processedMessages = transform(state.messages, [](const auto &m) { + return parseMamMessage(m, Unencrypted); + }); + state.finish(); + d->ongoingRequests.erase(itr); }); - return itr->second.promise.task(); + return task; } diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp index 046bdcf2..19895938 100644 --- a/src/client/QXmppOutgoingClient.cpp +++ b/src/client/QXmppOutgoingClient.cpp @@ -324,11 +324,9 @@ bool QXmppOutgoingClient::isStreamResumed() const /// QXmppTask<QXmppStream::IqResult> QXmppOutgoingClient::sendIq(QXmppIq &&iq) { - // always set a to address (the QXmppStream needs this for matching) - if (iq.to().isEmpty()) { - iq.setTo(d->config.domain()); - } - return QXmppStream::sendIq(std::move(iq)); + // If 'to' is empty the user's bare JID is meant implicitly (see RFC6120, section 10.3.3.). + auto to = iq.to(); + return QXmppStream::sendIq(std::move(iq), to.isEmpty() ? d->config.jidBare() : to); } void QXmppOutgoingClient::_q_socketDisconnected() diff --git a/src/omemo/QXmppOmemoManager.cpp b/src/omemo/QXmppOmemoManager.cpp index 0aab152d..a3ad12bb 100644 --- a/src/omemo/QXmppOmemoManager.cpp +++ b/src/omemo/QXmppOmemoManager.cpp @@ -340,7 +340,7 @@ QXmppOmemoManager::~QXmppOmemoManager() = default; /// /// This should be called after starting the client and before the login. /// It must only be called after \c setUp() has been called once for the user -/// during one of the past login session. +/// during one of the past login sessions. /// It does not need to be called if setUp() has been called during the current /// login session. /// @@ -1267,29 +1267,33 @@ bool Manager::handlePubSubEvent(const QDomElement &element, const QString &pubSu switch (event.eventType()) { // Items have been published. case QXmppPubSubEventBase::Items: { - const auto items = event.items(); - // Only process items if the event notification contains one. - // That is necessary because PubSub allows publishing without - // items leading to notification-only events. - if (!items.isEmpty()) { - const auto &deviceListItem = items.constFirst(); - if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { - d->updateDevices(pubSubService, event.items().constFirst()); + // That is necessary because PubSub allows publishing without items leading to + // notification-only events. + if (const auto &items = event.items(); !items.isEmpty()) { + // Since the usage of the item ID \c QXmppPubSubManager::Current is only RECOMMENDED + // by \xep{0060, Publish-Subscribe} (PubSub) but not obligatory, an appropriate + // contact device list is determined. + // In case of the own device list node, it is sctrictly processed as a recommended + // singleton item and changed to fit that if needed. + const auto isOwnDeviceListNode = d->ownBareJid() == pubSubService; + if (isOwnDeviceListNode) { + const auto &deviceListItem = items.constFirst(); + if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { + d->updateDevices(pubSubService, event.items().constFirst()); + } else { + d->handleIrregularDeviceListChanges(pubSubService); + } } else { - d->handleIrregularDeviceListChanges(pubSubService); + d->updateContactDevices(pubSubService, items); } } break; } - // Items have been retracted. + // Specific items are deleted. case QXmppPubSubEventBase::Retract: { - // Specific items are deleted. - const auto &retractedItem = event.retractIds().constFirst(); - if (retractedItem == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { - d->handleIrregularDeviceListChanges(pubSubService); - } + d->handleIrregularDeviceListChanges(pubSubService); } // All items are deleted. case QXmppPubSubEventBase::Purge: diff --git a/src/omemo/QXmppOmemoManager_p.cpp b/src/omemo/QXmppOmemoManager_p.cpp index 1c86d80b..bcfd8303 100644 --- a/src/omemo/QXmppOmemoManager_p.cpp +++ b/src/omemo/QXmppOmemoManager_p.cpp @@ -920,7 +920,7 @@ bool ManagerPrivate::updatePreKeyPairs(uint32_t count) deviceBundle.addPublicPreKey(preKeyId, serializedPublicPreKey); } - this->preKeyPairs.insert(serializedPreKeyPairs); + preKeyPairs.insert(serializedPreKeyPairs); omemoStorage->addPreKeyPairs(serializedPreKeyPairs); ownDevice.latestPreKeyId = latestPreKeyId - 1 + count; @@ -2648,6 +2648,36 @@ void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Func } // +// Updates all locally stored devices of a contact. +// +// \param deviceOwnerJid bare JID of the devices' owner +// \param deviceListItems PEP items that may contain a device list +// +// \returns a found device list item +// +std::optional<QXmppOmemoDeviceListItem> QXmppOmemoManagerPrivate::updateContactDevices(const QString &deviceOwnerJid, const QVector<QXmppOmemoDeviceListItem> &deviceListItems) +{ + if (deviceListItems.size() > 1) { + const auto itr = std::find_if(deviceListItems.cbegin(), deviceListItems.cend(), [=](const QXmppOmemoDeviceListItem &item) { + return item.id() == QXmppPubSubManager::Current; + }); + + if (itr != deviceListItems.cend()) { + updateDevices(deviceOwnerJid, *itr); + return *itr; + } else { + warning("Device list for JID '" % deviceOwnerJid % "' could not be updated because the node contains more than one item but none with the singleton node's specific ID '" % QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current) % "'"); + handleIrregularDeviceListChanges(deviceOwnerJid); + return {}; + } + } + + const auto &item = deviceListItems.constFirst(); + updateDevices(deviceOwnerJid, item); + return item; +} + +// // Updates all locally stored devices by a passed device list item. // // \param deviceOwnerJid bare JID of the devices' owner @@ -2783,7 +2813,7 @@ void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOme // Publish an own correct device list if the PEP service's one is incorrect // and the devices are already set up locally. if (isOwnDeviceListIncorrect) { - if (!this->devices.isEmpty()) { + if (!devices.isEmpty()) { publishDeviceListItem(true, [=](bool isPublished) { if (!isPublished) { warning("Own device list item could not be published in order to correct the PEP service's one"); @@ -2795,7 +2825,7 @@ void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOme // // Corrects the own device list on the PEP service by the locally stored -// devices or set a contact device to be removed locally in the future. +// devices or sets a contact device to be removed locally in the future. // // \param deviceOwnerJid bare JID of the devices' owner // @@ -2810,7 +2840,7 @@ void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwner auto future = pubSubManager->deleteOwnPepNode(ns_omemo_2_devices); future.then(q, [=](QXmppPubSubManager::Result result) { if (const auto error = std::get_if<QXmppError>(&result)) { - warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid % + warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid % "' could not be deleted in order to recover from an inconsistent node: " % errorToString(*error)); } else { @@ -2845,7 +2875,7 @@ void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwner } }); } else { - auto &ownerDevices = this->devices[deviceOwnerJid]; + auto &ownerDevices = devices[deviceOwnerJid]; // Set a timestamp for locally stored contact devices being removed // later if their device list item is removed, if their device list node @@ -3055,16 +3085,27 @@ QXmppTask<bool> ManagerPrivate::changeDeviceLabel(const QString &deviceLabel) // QXmppTask<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> ManagerPrivate::requestDeviceList(const QString &jid) { - auto future = pubSubManager->requestItem<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices, QXmppPubSubManager::Current); - future.then(q, [this, jid](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable { + QXmppPromise<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> interface; + + // Since the usage of the item ID \c QXmppPubSubManager::Current is only RECOMMENDED by + // \xep{0060, Publish-Subscribe} (PubSub) but not obligatory, all items are requested even if + // the node should contain only one item. + auto future = pubSubManager->requestItems<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices); + future.then(q, [this, interface, jid](QXmppPubSubManager::ItemsResult<QXmppOmemoDeviceListItem> result) mutable { if (const auto error = std::get_if<QXmppError>(&result)) { warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error)); + interface.finish(*error); + } else if (const auto &items = std::get<QXmppPubSubManager::Items<QXmppOmemoDeviceListItem>>(result).items; items.isEmpty()) { + const auto errorMessage = "Device list for JID '" % jid % "' could not be retrieved because the node does not contain any item"; + warning(errorMessage); + interface.finish(QXmppError { errorMessage }); + } else if (const auto item = updateContactDevices(jid, items); item) { + interface.finish(*item); } else { - const auto &item = std::get<QXmppOmemoDeviceListItem>(result); - updateDevices(jid, item); + interface.finish(QXmppError { "Device list for JID '" % jid % "' could not be retrieved because the node does not contain an appropriate item" }); } }); - return future; + return interface.task(); } // diff --git a/src/omemo/QXmppOmemoManager_p.h b/src/omemo/QXmppOmemoManager_p.h index 96f10f94..0792bdf2 100644 --- a/src/omemo/QXmppOmemoManager_p.h +++ b/src/omemo/QXmppOmemoManager_p.h @@ -290,6 +290,7 @@ public: QXmppOmemoDeviceListItem deviceListItem(bool addOwnDevice = true); template<typename Function> void updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation); + std::optional<QXmppOmemoDeviceListItem> updateContactDevices(const QString &deviceOwnerJid, const QVector<QXmppOmemoDeviceListItem> &deviceListItems); void updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem); void handleIrregularDeviceListChanges(const QString &deviceOwnerJid); template<typename Function> diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e7d304ef..e599ec47 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ add_simple_test(qxmppdataform) add_simple_test(qxmppdiscoveryiq) add_simple_test(qxmppdiscoverymanager TestClient.h) add_simple_test(qxmppentitytimeiq) +add_simple_test(qxmppentitytimemanager TestClient.h) add_simple_test(qxmppexternalservicediscoveryiq) add_simple_test(qxmppexternalservicediscoverymanager TestClient.h) add_simple_test(qxmpphttpuploadiq) @@ -79,6 +80,7 @@ add_simple_test(qxmppusertunemanager TestClient.h) add_simple_test(qxmppvcardiq) add_simple_test(qxmppvcardmanager) add_simple_test(qxmppversioniq) +add_simple_test(qxmppversionmanager TestClient.h) if(WITH_QCA) add_simple_test(qxmppfileencryption) diff --git a/tests/qxmppentitytimemanager/tst_qxmppentitytimemanager.cpp b/tests/qxmppentitytimemanager/tst_qxmppentitytimemanager.cpp new file mode 100644 index 00000000..a76a628d --- /dev/null +++ b/tests/qxmppentitytimemanager/tst_qxmppentitytimemanager.cpp @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn <lnj@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppEntityTimeIq.h" +#include "QXmppEntityTimeManager.h" + +#include "TestClient.h" + +Q_DECLARE_METATYPE(QXmppEntityTimeIq) + +class tst_QXmppEntityTimeManager : public QObject +{ + Q_OBJECT + Q_SLOT void initTestCase(); + Q_SLOT void testSendRequest(); + Q_SLOT void testHandleRequest(); +}; + +void tst_QXmppEntityTimeManager::initTestCase() +{ + qRegisterMetaType<QXmppEntityTimeIq>(); +} + +void tst_QXmppEntityTimeManager::testSendRequest() +{ + TestClient test; + auto *manager = test.addNewExtension<QXmppEntityTimeManager>(); + + QSignalSpy spy(manager, &QXmppEntityTimeManager::timeReceived); + + manager->requestTime("juliet@capulet.com/balcony"); + test.expect("<iq id='qxmpp1' to='juliet@capulet.com/balcony' type='get'><time xmlns='urn:xmpp:time'/></iq>"); + manager->handleStanza(xmlToDom(R"(<iq id='qxmpp1' to='romeo@montague.net/orchard' from='juliet@capulet.com/balcony' type='result'> + <time xmlns='urn:xmpp:time'> + <tzo>-06:00</tzo> + <utc>2006-12-19T17:58:35Z</utc> + </time> +</iq>)")); + + QCOMPARE(spy.size(), 1); + auto time = spy.at(0).at(0).value<QXmppEntityTimeIq>(); + QCOMPARE(time.utc(), QDateTime({2006, 12, 19}, {17, 58, 35}, Qt::UTC)); + QCOMPARE(time.tzo(), -6 * 60 * 60); +} + +void tst_QXmppEntityTimeManager::testHandleRequest() +{ + TestClient test; + test.configuration().setJid("juliet@capulet.com/balcony"); + + auto *manager = test.addNewExtension<QXmppEntityTimeManager>(); + + manager->handleStanza(xmlToDom(R"(<iq type='get' from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='time_1'> + <time xmlns='urn:xmpp:time'/> +</iq>)")); + + auto packet = xmlToDom(test.takePacket()); + QVERIFY(QXmppEntityTimeIq::isEntityTimeIq(packet)); + QXmppEntityTimeIq resp; + resp.parse(packet); + + QCOMPARE(resp.id(), QStringLiteral("time_1")); + QCOMPARE(resp.type(), QXmppIq::Result); +} + +QTEST_MAIN(tst_QXmppEntityTimeManager) +#include "tst_qxmppentitytimemanager.moc" diff --git a/tests/qxmppmessage/tst_qxmppmessage.cpp b/tests/qxmppmessage/tst_qxmppmessage.cpp index deca437e..98ca5670 100644 --- a/tests/qxmppmessage/tst_qxmppmessage.cpp +++ b/tests/qxmppmessage/tst_qxmppmessage.cpp @@ -783,7 +783,7 @@ void tst_QXmppMessage::testEme() // test standard encryption: OMEMO const QByteArray xmlOmemo( "<message to=\"foo@example.com/QXmpp\" from=\"bar@example.com/QXmpp\" type=\"normal\">" - "<encryption xmlns=\"urn:xmpp:eme:0\" namespace=\"eu.siacs.conversations.axolotl\"/>" + "<encryption xmlns=\"urn:xmpp:eme:0\" namespace=\"eu.siacs.conversations.axolotl\" name=\"OMEMO\"/>" "<body>This message is encrypted with OMEMO, but your client doesn't seem to support that.</body>" "</message>"); diff --git a/tests/qxmppversionmanager/tst_qxmppversionmanager.cpp b/tests/qxmppversionmanager/tst_qxmppversionmanager.cpp new file mode 100644 index 00000000..b28d0de2 --- /dev/null +++ b/tests/qxmppversionmanager/tst_qxmppversionmanager.cpp @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn <lnj@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppVersionManager.h" +#include "QXmppVersionIq.h" + +#include "TestClient.h" + +Q_DECLARE_METATYPE(QXmppVersionIq); + +class tst_QXmppVersionManager : public QObject +{ + Q_OBJECT + Q_SLOT void initTestCase(); + Q_SLOT void testSendRequest(); + Q_SLOT void testHandleRequest(); +}; + +void tst_QXmppVersionManager::initTestCase() +{ + qRegisterMetaType<QXmppVersionIq>(); +} + +void tst_QXmppVersionManager::testSendRequest() +{ + TestClient test; + auto *verManager = test.addNewExtension<QXmppVersionManager>(); + + QSignalSpy spy(verManager, &QXmppVersionManager::versionReceived); + + auto id = verManager->requestVersion("juliet@capulet.com/balcony"); + test.expect("<iq id='qxmpp1' to='juliet@capulet.com/balcony' type='get'><query xmlns='jabber:iq:version'/></iq>"); + verManager->handleStanza(xmlToDom(R"(<iq type='result' from='juliet@capulet.com/balcony' id='qxmpp1'> + <query xmlns='jabber:iq:version'> + <name>Exodus</name> + <version>0.7.0.4</version> + <os>Windows-XP 5.01.2600</os> + </query> +</iq>)")); + + QCOMPARE(spy.size(), 1); + auto version = spy.at(0).at(0).value<QXmppVersionIq>(); + QCOMPARE(version.name(), QStringLiteral("Exodus")); + QCOMPARE(version.version(), QStringLiteral("0.7.0.4")); + QCOMPARE(version.os(), QStringLiteral("Windows-XP 5.01.2600")); +} + +void tst_QXmppVersionManager::testHandleRequest() +{ + TestClient test; + test.configuration().setJid("juliet@capulet.com/balcony"); + + auto *verManager = test.addNewExtension<QXmppVersionManager>(); + verManager->setClientName("Exodus"); + verManager->setClientVersion("0.7.0.4"); + verManager->setClientOs("Windows-XP 5.01.2600"); + + verManager->handleStanza(xmlToDom(R"(<iq type='get' from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='version_1'> + <query xmlns='jabber:iq:version'/> +</iq>)")); + test.expect(R"(<iq id='version_1' to='romeo@montague.net/orchard' type='result'>)" + "<query xmlns='jabber:iq:version'><name>Exodus</name><os>Windows-XP 5.01.2600</os><version>0.7.0.4</version>" + "</query></iq>"); +} + +QTEST_MAIN(tst_QXmppVersionManager) +#include "tst_qxmppversionmanager.moc" |
