From e073f22ec0a07e2f6a6afc98781067239f409dfd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 25 Jan 2023 19:23:07 +0100 Subject: [PATCH] respond to disco#info queries --- .../eu/siacs/conversations/xml/Element.java | 2 +- .../stanzas/FileTransferDescription.java | 31 ++++--- .../android/xmpp/ServiceDescription.java | 49 ++++++++++ .../android/xmpp/XmppConnection.java | 28 +++++- .../android/xmpp/manager/DiscoManager.java | 93 ++++++++++++++----- .../android/xmpp/manager/PresenceManager.java | 27 +++--- .../android/xmpp/processor/IqProcessor.java | 42 +++------ .../xmpp/processor/MessageProcessor.java | 2 + .../android/xmpp/EntityCapabilitiesTest.java | 30 +++++- 9 files changed, 222 insertions(+), 82 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/ServiceDescription.java diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 9bf5ea54b..70b7f5cba 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -44,7 +44,7 @@ public class Element { } public void addExtensions(final Collection extensions) { - for(final Extension extension : extensions) { + for (final Extension extension : extensions) { addExtension(extension); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java index a7a7e3177..04e5e6da9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java @@ -1,23 +1,17 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; - -import java.util.Arrays; -import java.util.List; - import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import java.util.Arrays; +import java.util.List; public class FileTransferDescription extends GenericDescription { - public static List NAMESPACES = Arrays.asList( - Version.FT_3.namespace, - Version.FT_4.namespace, - Version.FT_5.namespace - ); - + public static List NAMESPACES = + Arrays.asList(Version.FT_3.namespace, Version.FT_4.namespace, Version.FT_5.namespace); private FileTransferDescription(String name, String namespace) { super(name, namespace); @@ -46,8 +40,10 @@ public class FileTransferDescription extends GenericDescription { } } - public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) { - final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace()); + public static FileTransferDescription of( + DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) { + final FileTransferDescription description = + new FileTransferDescription("description", version.getNamespace()); final Element fileElement; if (version == Version.FT_3) { Element offer = description.addChild("offer"); @@ -64,9 +60,14 @@ public class FileTransferDescription extends GenericDescription { } public static FileTransferDescription upgrade(final Element element) { - Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); - Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace"); - final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace()); + Preconditions.checkArgument( + "description".equals(element.getName()), + "Name of provided element is not description"); + Preconditions.checkArgument( + NAMESPACES.contains(element.getNamespace()), + "Element does not match a file transfer namespace"); + final FileTransferDescription description = + new FileTransferDescription("description", element.getNamespace()); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); return description; diff --git a/src/main/java/im/conversations/android/xmpp/ServiceDescription.java b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java new file mode 100644 index 000000000..283a0a615 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java @@ -0,0 +1,49 @@ +package im.conversations.android.xmpp; + +import com.google.common.collect.Collections2; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.util.Collection; +import java.util.List; + +public class ServiceDescription { + public final List features; + public final Identity identity; + + public ServiceDescription(List features, Identity identity) { + this.features = features; + this.identity = identity; + } + + public InfoQuery asInfoQuery() { + final var infoQuery = new InfoQuery(); + final Collection features = + Collections2.transform( + this.features, + sf -> { + final var feature = new Feature(); + feature.setVar(sf); + return feature; + }); + infoQuery.addExtensions(features); + final var identity = + infoQuery.addExtension( + new im.conversations.android.xmpp.model.disco.info.Identity()); + identity.setIdentityName(this.identity.name); + identity.setCategory(this.identity.category); + identity.setType(this.identity.type); + return infoQuery; + } + + public static class Identity { + public final String name; + public final String category; + public final String type; + + public Identity(String name, String category, String type) { + this.name = name; + this.category = category; + this.type = type; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index 10d8cc99e..e3735ea17 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -57,9 +57,12 @@ import im.conversations.android.xml.TagWriter; import im.conversations.android.xmpp.manager.AbstractManager; import im.conversations.android.xmpp.manager.CarbonsManager; import im.conversations.android.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.StreamElement; import im.conversations.android.xmpp.model.csi.Active; import im.conversations.android.xmpp.model.csi.Inactive; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; import im.conversations.android.xmpp.model.ping.Ping; import im.conversations.android.xmpp.model.register.Register; import im.conversations.android.xmpp.model.sm.Ack; @@ -2042,6 +2045,29 @@ public class XmppConnection implements Runnable { return packet.getId(); } + public void sendResultFor(final Iq request, final Extension... extensions) { + final var from = request.getFrom(); + final var id = request.getId(); + final var response = new Iq(Iq.Type.RESULT); + response.setTo(from); + response.setId(id); + for (final Extension extension : extensions) { + response.addExtension(extension); + } + this.sendPacket(response); + } + + public void sendErrorFor(final Iq request, final Condition condition) { + final var from = request.getFrom(); + final var id = request.getId(); + final var response = new Iq(Iq.Type.ERROR); + response.setTo(from); + response.setId(id); + final Error error = response.addExtension(new Error()); + error.setCondition(condition); + this.sendPacket(response); + } + public void sendMessagePacket(final Message packet) { this.sendPacket(packet); } @@ -2325,7 +2351,7 @@ public class XmppConnection implements Runnable { } } - private static class StateChangingError extends Error { + private static class StateChangingError extends java.lang.Error { private final ConnectionState state; public StateChangingError(ConnectionState state) { diff --git a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java index f2e3224fe..325ccea65 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -10,7 +10,6 @@ import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; - import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.R; import eu.siacs.conversations.xml.Namespace; @@ -18,20 +17,25 @@ import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.ServiceDescription; import im.conversations.android.xmpp.XmppConnection; -import im.conversations.android.xmpp.model.disco.info.Feature; -import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.Hash; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.items.Item; import im.conversations.android.xmpp.model.disco.items.ItemsQuery; +import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class DiscoManager extends AbstractManager { + private static final Logger LOGGER = LoggerFactory.getLogger(DiscoManager.class); + public static final String CAPABILITY_NODE = "http://conversations.im"; private static final Collection FEATURES_BASE = @@ -205,12 +209,11 @@ public class DiscoManager extends AbstractManager { return hasFeature(getAccount().address.getDomain(), feature); } - public InfoQuery getInfo() { - return getInfo(false); + public ServiceDescription getServiceDescription() { + return getServiceDescription(false); } - private InfoQuery getInfo(final boolean privacyMode) { - final var infoQuery = new InfoQuery(); + private ServiceDescription getServiceDescription(final boolean privacyMode) { final ImmutableList.Builder stringFeatureBuilder = ImmutableList.builder(); stringFeatureBuilder.addAll(FEATURES_BASE); stringFeatureBuilder.addAll( @@ -218,21 +221,9 @@ public class DiscoManager extends AbstractManager { if (!privacyMode) { stringFeatureBuilder.addAll(FEATURES_AV_CALLS); } - final var stringFeatures = stringFeatureBuilder.build(); - final Collection features = - Collections2.transform( - stringFeatures, - sf -> { - final var feature = new Feature(); - feature.setVar(sf); - return feature; - }); - infoQuery.addExtensions(features); - final var identity = infoQuery.addExtension(new Identity()); - identity.setIdentityName(getIdentityName()); - identity.setCategory("client"); - identity.setType(getIdentityType()); - return infoQuery; + return new ServiceDescription( + stringFeatureBuilder.build(), + new ServiceDescription.Identity(getIdentityName(), "client", getIdentityType())); } String getIdentityVersion() { @@ -252,4 +243,62 @@ public class DiscoManager extends AbstractManager { return "phone"; } } + + public void handleInfoQuery(final Iq request) { + final var infoQueryRequest = request.getExtension(InfoQuery.class); + final var nodeRequest = infoQueryRequest.getNode(); + LOGGER.warn("{} requested disco info for node {}", request.getFrom(), nodeRequest); + final ServiceDescription serviceDescription; + if (Strings.isNullOrEmpty(nodeRequest)) { + serviceDescription = getServiceDescription(); + } else { + final var hash = buildHashFromNode(nodeRequest); + final var cachedServiceDescription = + hash != null + ? getManager(PresenceManager.class).getCachedServiceDescription(hash) + : null; + if (cachedServiceDescription != null) { + serviceDescription = cachedServiceDescription; + } else { + LOGGER.warn("No disco info was cached for node {}", nodeRequest); + connection.sendErrorFor(request, new Condition.ItemNotFound()); + return; + } + } + final var infoQuery = serviceDescription.asInfoQuery(); + infoQuery.setNode(nodeRequest); + connection.sendResultFor(request, infoQuery); + } + + public static EntityCapabilities.Hash buildHashFromNode(final String node) { + final var capsPrefix = CAPABILITY_NODE + "#"; + final var caps2Prefix = Namespace.ENTITY_CAPABILITIES_2 + "#"; + if (node.startsWith(capsPrefix)) { + final String hash = node.substring(capsPrefix.length()); + if (Strings.isNullOrEmpty(hash)) { + return null; + } + if (BaseEncoding.base64().canDecode(hash)) { + return EntityCapabilities.EntityCapsHash.of(hash); + } + } else if (node.startsWith(caps2Prefix)) { + final String caps = node.substring(caps2Prefix.length()); + if (Strings.isNullOrEmpty(caps)) { + return null; + } + final int separator = caps.lastIndexOf('.'); + if (separator < 0) { + return null; + } + final Hash.Algorithm algorithm = Hash.Algorithm.tryParse(caps.substring(0, separator)); + final String hash = caps.substring(separator + 1); + if (algorithm == null || Strings.isNullOrEmpty(hash)) { + return null; + } + if (BaseEncoding.base64().canDecode(hash)) { + return EntityCapabilities2.EntityCaps2Hash.of(algorithm, hash); + } + } + return null; + } } diff --git a/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java b/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java index 296f57c8f..5fabb6012 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java @@ -1,37 +1,36 @@ package im.conversations.android.xmpp.manager; import android.content.Context; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.ServiceDescription; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.capabilties.Capabilities; import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities; -import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Presence; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PresenceManager extends AbstractManager { private static final Logger LOGGER = LoggerFactory.getLogger(PresenceManager.class); - private final Map outgoingCapsHash = new HashMap<>(); + private final Map serviceDescriptions = + new HashMap<>(); public PresenceManager(Context context, XmppConnection connection) { super(context, connection); } public void sendPresence() { - final var infoQuery = getManager(DiscoManager.class).getInfo(); + final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription(); + final var infoQuery = serviceDiscoveryFeatures.asInfoQuery(); final var capsHash = EntityCapabilities.hash(infoQuery); final var caps2Hash = EntityCapabilities2.hash(infoQuery); - outgoingCapsHash.put(capsHash, infoQuery); - outgoingCapsHash.put(caps2Hash, infoQuery); + serviceDescriptions.put(capsHash, serviceDiscoveryFeatures); + serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures); final var capabilities = new Capabilities(); capabilities.setHash(caps2Hash); final var legacyCapabilities = new LegacyCapabilities(); @@ -45,4 +44,8 @@ public class PresenceManager extends AbstractManager { connection.sendPresencePacket(presence); } + + public ServiceDescription getCachedServiceDescription(final EntityCapabilities.Hash hash) { + return this.serviceDescriptions.get(hash); + } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java index 1e0a7609d..88680bb68 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java @@ -4,12 +4,12 @@ import android.content.Context; import com.google.common.base.Preconditions; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.manager.BlockingManager; +import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.manager.RosterManager; -import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.blocking.Block; import im.conversations.android.xmpp.model.blocking.Unblock; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.error.Condition; -import im.conversations.android.xmpp.model.error.Error; import im.conversations.android.xmpp.model.ping.Ping; import im.conversations.android.xmpp.model.roster.Query; import im.conversations.android.xmpp.model.stanza.Iq; @@ -34,54 +34,36 @@ public class IqProcessor extends XmppConnection.Delegate implements Consumer && connection.fromAccount(packet) && packet.hasExtension(Query.class)) { getManager(RosterManager.class).handlePush(packet.getExtension(Query.class)); - sendResultFor(packet); + connection.sendResultFor(packet); return; } if (type == Iq.Type.SET && connection.fromAccount(packet) && packet.hasExtension(Block.class)) { getManager(BlockingManager.class).handlePush(packet.getExtension(Block.class)); - sendResultFor(packet); + connection.sendResultFor(packet); return; } if (type == Iq.Type.SET && connection.fromAccount(packet) && packet.hasExtension(Unblock.class)) { getManager(BlockingManager.class).handlePush(packet.getExtension(Unblock.class)); - sendResultFor(packet); + connection.sendResultFor(packet); return; } if (type == Iq.Type.GET && packet.hasExtension(Ping.class)) { LOGGER.debug("Responding to ping from {}", packet.getFrom()); - sendResultFor(packet); + connection.sendResultFor(packet); + return; + } + + if (type == Iq.Type.GET && packet.hasExtension(InfoQuery.class)) { + getManager(DiscoManager.class).handleInfoQuery(packet); return; } final var extensionIds = packet.getExtensionIds(); LOGGER.info("Could not handle {}. Sending feature-not-implemented", extensionIds); - sendErrorFor(packet, new Condition.FeatureNotImplemented()); - } - - public void sendResultFor(final Iq request, final Extension... extensions) { - final var from = request.getFrom(); - final var id = request.getId(); - final var response = new Iq(Iq.Type.RESULT); - response.setTo(from); - response.setId(id); - for (final Extension extension : extensions) { - response.addExtension(extension); - } - connection.sendIqPacket(response, null); - } - - public void sendErrorFor(final Iq request, final Condition condition) { - final var from = request.getFrom(); - final var id = request.getId(); - final var response = new Iq(Iq.Type.ERROR); - response.setTo(from); - response.setId(id); - final Error error = response.addExtension(new Error()); - error.setCondition(condition); - connection.sendIqPacket(response, null); + connection.sendErrorFor(packet, new Condition.FeatureNotImplemented()); } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java index 38fddecf6..cc55ee3d1 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java @@ -43,6 +43,8 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume LOGGER.info("'{}' from {}", body, message.getFrom()); } + LOGGER.info("Message received {}", message.getExtensionIds()); + // TODO process receipt requests (184 + 333) // TODO collect Extensions that require transformation (everything that will end up in the diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java index 0cfb23c4b..50fe9727d 100644 --- a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -2,9 +2,11 @@ package im.conversations.android.xmpp; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.XmlElementReader; +import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -26,8 +28,8 @@ public class EntityCapabilitiesTest { + " node='http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0='>\n" + " \n" + " \n" - + " \n" + " \n" + + " \n" + " \n" + " "; final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); @@ -182,4 +184,30 @@ public class EntityCapabilitiesTest { final String var = EntityCapabilities2.hash(info).encoded(); Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var); } + + @Test + public void parseCaps2Node() { + final var caps = + DiscoManager.buildHashFromNode( + "urn:xmpp:caps#sha-256.u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY="); + assertThat(caps, instanceOf(EntityCapabilities2.EntityCaps2Hash.class)); + } + + @Test + public void parseCaps2NodeMissingHash() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#sha-256."); + assertNull(caps); + } + + @Test + public void parseCaps2NodeInvalid() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#-"); + assertNull(caps); + } + + @Test + public void parseCaps2NodeUnknownAlgo() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#test.test"); + assertNull(caps); + } }