Skip to content

Commit

Permalink
Complete implementation of XEP-0215 (External Service Discovery)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ri0n committed Apr 25, 2024
1 parent ffaeb34 commit ab2f925
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 35 deletions.
5 changes: 3 additions & 2 deletions src/xmpp/xmpp-im/stundisco.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
195 changes: 175 additions & 20 deletions src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalService>();
if (s->parse(el)) {
if (s->parse(el, !creds_.isEmpty(), false)) {
services_.append(s);
}
}
Expand All @@ -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<ExternalService>();
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"));
Expand All @@ -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()) {
Expand All @@ -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;
}
Expand All @@ -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
{
Expand Down Expand Up @@ -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.
}
}
}
Expand All @@ -217,12 +327,57 @@ ExternalServiceList ExternalServiceDiscovery::cachedServices(const QStringList &
}

void ExternalServiceDiscovery::credentials(QObject *ctx, ServicesCallback &&callback,
const QSet<ExternalServiceId> &ids)
const QSet<ExternalServiceId> &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"
34 changes: 21 additions & 13 deletions src/xmpp/xmpp-im/xmpp_externalservicediscovery.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#define XMPP_EXTERNALSERVICEDISCOVERY_H

#include "xmpp_task.h"
#include "xmpp_xdata.h"

#include <QDateTime>
#include <QDeadlineTimer>
Expand All @@ -40,19 +41,22 @@ struct ExternalService {
using Ptr = std::shared_ptr<ExternalService>;
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 {
Expand Down Expand Up @@ -122,21 +126,25 @@ 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.
* Otherwise `restricted` flag has to be set and the credentials are requested when they are really needed.
* 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<ExternalServiceId> &ids);
void credentials(QObject *ctx, ServicesCallback &&callback, const QSet<ExternalServiceId> &ids,
std::chrono::minutes minTtl = std::chrono::minutes(1));
signals:
// server push signals only
void serviceAdded(const ExternalServiceList &);
void serviceDeleted(ExternalServiceList &);
void serviceModified(ExternalServiceList &);

private:
ExternalServiceList::iterator findCachedService(const ExternalServiceId &id = {});

Client *client_;
QPointer<JT_ExternalServiceDiscovery> currentTask = nullptr; // for all services (no type)
ExternalServiceList services_;
Expand Down

0 comments on commit ab2f925

Please sign in to comment.