From ab2f925bf8b78f3f31ee618685bb89825f7f9a7c Mon Sep 17 00:00:00 2001 From: Sergei Ilinykh Date: Fri, 26 Apr 2024 02:45:21 +0300 Subject: [PATCH] Complete implementation of XEP-0215 (External Service Discovery) --- src/xmpp/xmpp-im/stundisco.cpp | 5 +- .../xmpp-im/xmpp_externalservicediscovery.cpp | 195 ++++++++++++++++-- .../xmpp-im/xmpp_externalservicediscovery.h | 34 +-- 3 files changed, 199 insertions(+), 35 deletions(-) diff --git a/src/xmpp/xmpp-im/stundisco.cpp b/src/xmpp/xmpp-im/stundisco.cpp index 0ef2405c..21ce1f81 100644 --- a/src/xmpp/xmpp-im/stundisco.cpp +++ b/src/xmpp/xmpp-im/stundisco.cpp @@ -87,7 +87,7 @@ class StunDiscoMonitor : public AbstractStunDisco { needResolve.append(s); else s->addresses.append(addr); - if (l->restricted && s->password.isEmpty()) { + if (l->needsNewCreds()) { needCreds.append(s); } else if (!s->addresses.isEmpty()) { emit serviceAdded(s); @@ -179,13 +179,14 @@ class StunDiscoMonitor : public AbstractStunDisco { QString etype = extType(s); auto it = std::find_if(resolved.begin(), resolved.end(), [&s, &etype](ExternalService::Ptr const &r) { - return s->host == r->host && etype == r->type && s->port == r->port; + return s->host == r->host && etype == r->type && (r->port == 0 || s->port == r->port); }); if (it == resolved.end()) { s->expires = QDeadlineTimer(); // expired timer qDebug("no creds from server for %s:%hu %s", qPrintable(s->host), s->port, qPrintable(etype)); continue; // failed to get creds? weird } + s->expires = (*it)->expires; s->username = (*it)->username; s->password = (*it)->password; } diff --git a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp index c5354e41..96f09e57 100644 --- a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp +++ b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp @@ -74,7 +74,7 @@ bool JT_ExternalServiceDiscovery::take(const QDomElement &x) for (auto el = query.firstChildElement(serviceTag); !el.isNull(); el = el.nextSiblingElement(serviceTag)) { // services_.append(ExternalService {}); auto s = std::make_shared(); - if (s->parse(el)) { + if (s->parse(el, !creds_.isEmpty(), false)) { services_.append(s); } } @@ -86,7 +86,43 @@ bool JT_ExternalServiceDiscovery::take(const QDomElement &x) return true; } -bool ExternalService::parse(QDomElement &el) +//---------------------------------------------------------------------------- +// JT_PushMessage +//---------------------------------------------------------------------------- +class JT_PushExternalService : public Task { + + Q_OBJECT + + ExternalServiceList services_; + +public: + using Task::Task; + bool take(const QDomElement &e) + { + if (e.tagName() != QStringLiteral("iq") || e.attribute("type") != QStringLiteral("set")) + return false; + auto query = e.firstChildElement(QLatin1String("services")); + if (query.isNull() || query.namespaceURI() != QLatin1String("urn:xmpp:extdisco:2")) { + return false; + } + QString serviceTag { QStringLiteral("service") }; + for (auto el = query.firstChildElement(serviceTag); !el.isNull(); el = el.nextSiblingElement(serviceTag)) { + // services_.append(ExternalService {}); + auto s = std::make_shared(); + if (s->parse(el, false, true)) { + services_.append(s); + } + } + emit received(services_); + + return true; + } + +signals: + void received(const ExternalServiceList &); +}; + +bool ExternalService::parse(QDomElement &el, bool isCreds, bool isPush) { QString actionOpt = el.attribute(QLatin1String("action")); QString expiresOpt = el.attribute(QLatin1String("expires")); @@ -104,7 +140,7 @@ bool ExternalService::parse(QDomElement &el) return false; port = portReq.toUShort(&ok); - if (!ok) + if (!ok && !portReq.isEmpty()) return false; if (!expiresOpt.isEmpty()) { @@ -116,25 +152,38 @@ bool ExternalService::parse(QDomElement &el) if (expires.hasExpired()) qInfo("Server returned already expired service %s expired at %s UTC", qPrintable(*this), qPrintable(expiresOpt)); - } else { - expires = QDeadlineTimer(QDeadlineTimer::Forever); - } + } // else never expires - if (actionOpt.isEmpty() || actionOpt == QLatin1String("add")) - action = Action::Add; - else if (actionOpt == QLatin1String("modify")) - action = Action::Modify; - else if (actionOpt == QLatin1String("delete")) - action = Action::Delete; - else - return false; + if (isCreds) { + return true; // just host/type/username/password/expires and optional port + } + restricted = !username.isEmpty() || !password.isEmpty(); if (!restrictedOpt.isEmpty()) { if (restrictedOpt == QLatin1String("true") || restrictedOpt == QLatin1String("1")) restricted = true; else if (restrictedOpt != QLatin1String("false") && restrictedOpt != QLatin1String("0")) return false; } + if (restricted && username.isEmpty() && password.isEmpty() && expiresOpt.isEmpty()) { + expires = QDeadlineTimer(); // restricted but creds invalid. make expired + } + + auto formEl = childElementsByTagNameNS(el, "jabber:x:data", "x").item(0).toElement(); + if (!formEl.isNull()) { + form.fromXml(formEl); + } + + if (isPush) { + if (actionOpt.isEmpty() || actionOpt == QLatin1String("add")) + action = Action::Add; + else if (actionOpt == QLatin1String("modify")) + action = Action::Modify; + else if (actionOpt == QLatin1String("delete")) + action = Action::Delete; + else + return false; + } return true; } @@ -145,7 +194,59 @@ ExternalService::operator QString() const .arg(name, host, QString::number(port), type, transport); } -ExternalServiceDiscovery::ExternalServiceDiscovery(Client *client) : client_(client) { } +bool ExternalService::needsNewCreds(std::chrono::minutes minTtl) const +{ + return restricted || !(expires.isForever() || expires.remainingTimeAsDuration() > minTtl); +} + +ExternalServiceDiscovery::ExternalServiceDiscovery(Client *client) : client_(client) +{ + JT_PushExternalService *push = new JT_PushExternalService(client->rootTask()); + connect(push, &JT_PushExternalService::received, this, [this](const ExternalServiceList &services) { + ExternalServiceList deleted; + ExternalServiceList modified; + ExternalServiceList added; + for (auto const &service : services) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + switch (service->action) { + case ExternalService::Add: // weird.. + modified << service; + **cachedServiceIt = *service; + break; + case ExternalService::Modify: + modified << service; + **cachedServiceIt = *service; + break; + case ExternalService::Delete: + deleted << *cachedServiceIt; + services_.erase(cachedServiceIt); + break; + } + } else { + switch (service->action) { + case ExternalService::Add: + case ExternalService::Modify: // weird.. + added << service; + services_ << service; + break; + case ExternalService::Delete: + // we never knew it + break; + } + } + } + if (!added.empty()) { + emit serviceAdded(added); + } + if (!modified.empty()) { + emit serviceModified(modified); + } + if (!deleted.empty()) { + emit serviceDeleted(deleted); + } + }); +} bool ExternalServiceDiscovery::isSupported() const { @@ -194,11 +295,20 @@ void ExternalServiceDiscovery::services(QObject *ctx, ServicesCallback &&callbac } else { auto task = new JT_ExternalServiceDiscovery(client_->rootTask()); auto type = types[0]; - connect(task, &Task::finished, ctx, [task, type, cb = std::move(callback)]() { cb(task->services()); }); + connect(task, &Task::finished, ctx, [task, type, cb = std::move(callback), this]() { + for (auto const &service : std::as_const(task->services())) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + **cachedServiceIt = *service; + } // else we can't add to the cache coz it can make the cache incomplete. see + // comment below. + } + cb(task->services()); + }); task->getServices(type); task->go(true); - // in fact we can improve caching even more if start remembering specific repviously requested types, - // even if the result was negative. + // in fact we can improve caching even more if start remembering specific pveviously + // requested types, even if the result was negative. } } } @@ -217,12 +327,57 @@ ExternalServiceList ExternalServiceDiscovery::cachedServices(const QStringList & } void ExternalServiceDiscovery::credentials(QObject *ctx, ServicesCallback &&callback, - const QSet &ids) + const QSet &ids, std::chrono::minutes minTtl) { + bool cacheValid = true; + ExternalServiceList ret; + for (auto const &id : ids) { + auto cachedServiceIt = findCachedService(id); + if (cachedServiceIt != services_.end()) { + auto const &s = **cachedServiceIt; + if (s.username.isEmpty() || s.password.isEmpty() + || !(s.expires.isForever() || s.expires.remainingTimeAsDuration() > minTtl)) { + cacheValid = false; + break; + } + ret << *cachedServiceIt; + } + } + if (cacheValid) { + callback(ret); + return; + } + auto task = new JT_ExternalServiceDiscovery(client_->rootTask()); - connect(task, &Task::finished, ctx ? ctx : this, [task, cb = std::move(callback)]() { cb(task->services()); }); + connect(task, &Task::finished, ctx ? ctx : this, [task, cb = std::move(callback), this]() { + ExternalServiceList ret; + for (auto const &service : std::as_const(task->services())) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + auto &cache = **cachedServiceIt; + cache.username = service->username; + cache.password = service->password; + cache.expires = service->expires; + ret << *cachedServiceIt; + } else { + qDebug("credentials request returned creds not previously cached service. adding to " + "the result as is."); + ret << service; + } + } + cb(ret); + }); task->getCredentials(ids); task->go(true); } +ExternalServiceList::iterator ExternalServiceDiscovery::findCachedService(const ExternalServiceId &id) +{ + return std::find_if(services_.begin(), services_.end(), [&id](auto const &s) { + return s->type == id.type && s->host == id.host && (id.port == 0 || s->port == id.port); + }); +} + } // namespace XMPP + +#include "xmpp_externalservicediscovery.moc" diff --git a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h index 563603c2..a31bd8e2 100644 --- a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h +++ b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h @@ -21,6 +21,7 @@ #define XMPP_EXTERNALSERVICEDISCOVERY_H #include "xmpp_task.h" +#include "xmpp_xdata.h" #include #include @@ -40,19 +41,22 @@ struct ExternalService { using Ptr = std::shared_ptr; enum Action { Add, Delete, Modify }; - Action action = Action::Add; - QDeadlineTimer expires; // optional - QString host; // required - QString name; // optional - QString password; // optional - std::uint16_t port; // required - bool restricted = false; // optional - QString transport; // optional - QString type; // required - QString username; // optional - - bool parse(QDomElement &el); + Action action = Action::Add; // required only for pushes + QDeadlineTimer expires = QDeadlineTimer::Forever; // optional + QString host; // required + QString name; // optional + QString password; // optional + std::uint16_t port; // required + bool restricted = false; // optional + QString transport; // optional + QString type; // required + QString username; // optional + XData form; // optional + + bool parse(QDomElement &el, bool isCreds, bool isPush); operator QString() const; + + bool needsNewCreds(std::chrono::minutes minTtl = std::chrono::minutes(1)) const; }; struct ExternalServiceId { @@ -122,6 +126,7 @@ class ExternalServiceDiscovery : public QObject { * @param ctx - if ctx dies, the request will be aborted * @param callback - callback to call when ready * @param ids - identifier of services + * @param minTtl - if service expires in less than minTtl it will be re-requested * * The credentials won't be cached since it's assumed if crdentials are returned with services request then * the creds are constant values until the service is expired. @@ -129,7 +134,8 @@ class ExternalServiceDiscovery : public QObject { * Most likely with `restricted` flag those are going to be temporal credentials. Even so the caller may cache them * on its own risk. */ - void credentials(QObject *ctx, ServicesCallback &&callback, const QSet &ids); + void credentials(QObject *ctx, ServicesCallback &&callback, const QSet &ids, + std::chrono::minutes minTtl = std::chrono::minutes(1)); signals: // server push signals only void serviceAdded(const ExternalServiceList &); @@ -137,6 +143,8 @@ class ExternalServiceDiscovery : public QObject { void serviceModified(ExternalServiceList &); private: + ExternalServiceList::iterator findCachedService(const ExternalServiceId &id = {}); + Client *client_; QPointer currentTask = nullptr; // for all services (no type) ExternalServiceList services_;