From 43a82e504bfa5a3ae0ddb7a72284a4fee6d34625 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 17 Jan 2023 13:09:03 +0100 Subject: [PATCH] parse caps from presence --- .../eu/siacs/conversations/xml/Namespace.java | 24 ++++++++--- .../android/database/dao/DiscoDao.java | 26 ++++++++++++ .../android/xmpp/EntityCapabilities.java | 4 ++ .../android/xmpp/EntityCapabilities2.java | 37 ++++------------- .../android/xmpp/Extensions.java | 5 ++- .../android/xmpp/XmppConnection.java | 2 +- .../android/xmpp/manager/DiscoManager.java | 10 +++++ .../android/xmpp/model/Hash.java | 41 +++++++++++++++++++ .../xmpp/model/capabilties/Capabilities.java | 35 ++++++++++++++++ .../model/capabilties/LegacyCapabilities.java | 35 ++++++++++++++++ .../xmpp/processor/PresenceProcessor.java | 36 ++++++++++++++-- 11 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/Hash.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index b614251bd..2c5eaaac0 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -5,6 +5,13 @@ public final class Namespace { public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; + + public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; + + public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; + + public static final String HASHES = "urn:xmpp:hashes:2"; + public static final String BLOCKING = "urn:xmpp:blocking"; public static final String ROSTER = "jabber:iq:roster"; public static final String REGISTER = "jabber:iq:register"; @@ -26,7 +33,8 @@ public final class Namespace { public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String NICK = "http://jabber.org/protocol/nick"; - public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; + public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = + "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; public static final String BIND2 = "urn:xmpp:bind:0"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; @@ -36,7 +44,7 @@ public final class Namespace { public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_DATA = "urn:xmpp:avatar:data"; - public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; + public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; @@ -51,9 +59,12 @@ public final class Namespace { public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; - public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; - public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; - public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0"; + public static final String JINGLE_RTP_HEADER_EXTENSIONS = + "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = + "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = + "urn:xmpp:jingle:apps:rtp:ssma:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; @@ -64,6 +75,7 @@ public final class Namespace { public static final String INVITE = "urn:xmpp:invite"; public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; - public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + public static final String OMEMO_DTLS_SRTP_VERIFICATION = + "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; } diff --git a/src/main/java/im/conversations/android/database/dao/DiscoDao.java b/src/main/java/im/conversations/android/database/dao/DiscoDao.java index e14b48796..1c17e6b05 100644 --- a/src/main/java/im/conversations/android/database/dao/DiscoDao.java +++ b/src/main/java/im/conversations/android/database/dao/DiscoDao.java @@ -15,6 +15,8 @@ import im.conversations.android.database.entity.DiscoFeatureEntity; import im.conversations.android.database.entity.DiscoIdentityEntity; import im.conversations.android.database.entity.DiscoItemEntity; import im.conversations.android.database.model.Account; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.EntityCapabilities2; import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.data.Field; import im.conversations.android.xmpp.model.data.Value; @@ -59,6 +61,27 @@ public abstract class DiscoDao { insertDiscoItems(entities); } + @Transaction + public boolean set( + final Account account, + final Jid address, + final String node, + final EntityCapabilities.Hash capsHash) { + final Long existingDiscoId; + if (capsHash instanceof EntityCapabilities2.EntityCaps2Hash) { + existingDiscoId = getDiscoId(account.id, capsHash.hash); + } else if (capsHash instanceof EntityCapabilities.EntityCapsHash) { + existingDiscoId = getDiscoIdByCapsHash(account.id, capsHash.hash); + } else { + existingDiscoId = null; + } + if (existingDiscoId == null) { + return false; + } + insert(DiscoItemWithDiscoId.of(account.id, address, node, existingDiscoId)); + return true; + } + @Transaction public void set( final Account account, @@ -100,6 +123,9 @@ public abstract class DiscoDao { @Query("SELECT id FROM disco WHERE accountId=:accountId AND caps2HashSha256=:caps2HashSha256") protected abstract Long getDiscoId(final long accountId, final byte[] caps2HashSha256); + @Query("SELECT id FROM disco WHERE accountId=:accountId AND capsHash=:capsHash") + protected abstract Long getDiscoIdByCapsHash(final long accountId, final byte[] capsHash); + public static class DiscoItemWithParent { public long accountId; public Jid address; diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java index ae9fcc3a7..1f1dbcfba 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java @@ -106,5 +106,9 @@ public final class EntityCapabilities { protected EntityCapsHash(byte[] hash) { super(hash); } + + public static EntityCapsHash of(final String encoded) { + return new EntityCapsHash(BaseEncoding.base64().decode(encoded)); + } } } diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java index 1f03e08b1..9942c9c5c 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -1,15 +1,14 @@ package im.conversations.android.xmpp; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.Ordering; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.Bytes; +import im.conversations.android.xmpp.model.Hash; import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.data.Field; import im.conversations.android.xmpp.model.data.Value; @@ -29,17 +28,17 @@ public class EntityCapabilities2 { private static final char FILE_SEPARATOR = 0x1c; public static EntityCaps2Hash hash(final InfoQuery info) { - return hash(Algorithm.SHA_256, info); + return hash(Hash.Algorithm.SHA_256, info); } - public static EntityCaps2Hash hash(final Algorithm algorithm, final InfoQuery info) { + public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) { final String result = algorithm(info); final var hashFunction = toHashFunction(algorithm); return new EntityCaps2Hash( algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes()); } - private static HashFunction toHashFunction(final Algorithm algorithm) { + private static HashFunction toHashFunction(final Hash.Algorithm algorithm) { switch (algorithm) { case SHA_1: return Hashing.sha1(); @@ -149,33 +148,15 @@ public class EntityCapabilities2 { public static class EntityCaps2Hash extends EntityCapabilities.Hash { - public final Algorithm algorithm; + public final Hash.Algorithm algorithm; - protected EntityCaps2Hash(final Algorithm algorithm, byte[] hash) { + protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) { super(hash); this.algorithm = algorithm; } - } - public enum Algorithm { - SHA_1, - SHA_256, - SHA_512; - - public static Algorithm tryParse(@Nullable final String name) { - try { - return valueOf( - CaseFormat.LOWER_HYPHEN.to( - CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name))); - } catch (final IllegalArgumentException e) { - return null; - } - } - - @NonNull - @Override - public String toString() { - return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) { + return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded)); } } } diff --git a/src/main/java/im/conversations/android/xmpp/Extensions.java b/src/main/java/im/conversations/android/xmpp/Extensions.java index 6e7f7ac9e..eff8eb155 100644 --- a/src/main/java/im/conversations/android/xmpp/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/Extensions.java @@ -23,6 +23,8 @@ public final class Extensions { im.conversations.android.xmpp.model.blocking.Block.class, im.conversations.android.xmpp.model.blocking.Blocklist.class, im.conversations.android.xmpp.model.blocking.Unblock.class, + im.conversations.android.xmpp.model.capabilties.LegacyCapabilities.class, + im.conversations.android.xmpp.model.capabilties.Capabilities.class, im.conversations.android.xmpp.model.data.Data.class, im.conversations.android.xmpp.model.data.Field.class, im.conversations.android.xmpp.model.data.Value.class, @@ -32,7 +34,8 @@ public final class Extensions { im.conversations.android.xmpp.model.disco.items.Item.class, im.conversations.android.xmpp.model.disco.items.ItemsQuery.class, im.conversations.android.xmpp.model.roster.Query.class, - im.conversations.android.xmpp.model.roster.Item.class); + im.conversations.android.xmpp.model.roster.Item.class, + im.conversations.android.xmpp.model.Hash.class); private static final BiMap> EXTENSION_CLASS_MAP; diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index f45ec3641..75c047f59 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -2826,7 +2826,7 @@ public class XmppConnection implements Runnable { return ConversationsDatabase.getInstance(context); } - public T getManager(Class type) { + protected T getManager(Class type) { return connection.managers.getInstance(type); } } 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 6fa592d36..9e5bb9ff2 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -1,6 +1,7 @@ package im.conversations.android.xmpp.manager; import android.content.Context; +import androidx.annotation.Nullable; import com.google.common.collect.Collections2; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -26,6 +27,15 @@ public class DiscoManager extends AbstractManager { return info(entity, null); } + public ListenableFuture info( + final Jid entity, @Nullable final String node, final EntityCapabilities.Hash hash) { + // TODO construct node with appended hash + if (getDatabase().discoDao().set(getAccount(), entity, node, hash)) { + return Futures.immediateFuture(null); + } + return Futures.transform(info(entity, node), f -> null, MoreExecutors.directExecutor()); + } + public ListenableFuture info(final Jid entity, final String node) { final var iqRequest = new IqPacket(IqPacket.TYPE.GET); iqRequest.setTo(entity); diff --git a/src/main/java/im/conversations/android/xmpp/model/Hash.java b/src/main/java/im/conversations/android/xmpp/model/Hash.java new file mode 100644 index 000000000..9a640e6ef --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/Hash.java @@ -0,0 +1,41 @@ +package im.conversations.android.xmpp.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; + +@XmlElement(namespace = Namespace.HASHES) +public class Hash extends Extension { + public Hash() { + super(Hash.class); + } + + public Algorithm getAlgorithm() { + return Algorithm.tryParse(this.getAttribute("algo")); + } + + public enum Algorithm { + SHA_1, + SHA_256, + SHA_512; + + public static Algorithm tryParse(@Nullable final String name) { + try { + return valueOf( + CaseFormat.LOWER_HYPHEN.to( + CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name))); + } catch (final IllegalArgumentException e) { + return null; + } + } + + @NonNull + @Override + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java new file mode 100644 index 000000000..33b9b18a7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.Hash; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES_2) +public class Capabilities extends Extension { + + public Capabilities() { + super(Capabilities.class); + } + + public EntityCapabilities2.EntityCaps2Hash getHash() { + final Optional sha256Hash = + Iterables.tryFind( + getExtensions(Hash.class), h -> h.getAlgorithm() == Hash.Algorithm.SHA_256); + if (sha256Hash.isPresent()) { + final String content = sha256Hash.get().getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } + if (BaseEncoding.base64().canDecode(content)) { + return EntityCapabilities2.EntityCaps2Hash.of(Hash.Algorithm.SHA_256, content); + } + } + return null; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java new file mode 100644 index 000000000..25affc7e0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES) +public class LegacyCapabilities extends Extension { + + private static final String HASH_ALGORITHM = "sha-1"; + + public LegacyCapabilities() { + super(LegacyCapabilities.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public EntityCapabilities.EntityCapsHash getHash() { + final String hash = getAttribute("hash"); + final String ver = getAttribute("ver"); + if (Strings.isNullOrEmpty(ver) || Strings.isNullOrEmpty(hash)) { + return null; + } + if (HASH_ALGORITHM.equals(hash) && BaseEncoding.base64().canDecode(hash)) { + return EntityCapabilities.EntityCapsHash.of(hash); + } else { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java index bbe4f95c2..553ac2690 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java @@ -2,13 +2,43 @@ package im.conversations.android.xmpp.processor; import android.content.Context; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.XmppConnection; +import im.conversations.android.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.capabilties.Capabilities; +import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities; import java.util.function.Consumer; -public class PresenceProcessor implements Consumer { +public class PresenceProcessor extends XmppConnection.Delegate implements Consumer { - public PresenceProcessor(final Context context, final XmppConnection connection) {} + public PresenceProcessor(final Context context, final XmppConnection connection) { + super(context, connection); + } @Override - public void accept(PresencePacket presencePacket) {} + public void accept(final PresencePacket presencePacket) { + // TODO do this only for contacts? + fetchCapabilities(presencePacket); + } + + private void fetchCapabilities(final PresencePacket presencePacket) { + final var entity = presencePacket.getFrom(); + final String node; + final EntityCapabilities.Hash hash; + final var capabilities = presencePacket.getExtension(Capabilities.class); + final var legacyCapabilities = presencePacket.getExtension(LegacyCapabilities.class); + if (capabilities != null) { + node = null; + hash = capabilities.getHash(); + } else if (legacyCapabilities != null) { + node = legacyCapabilities.getNode(); + hash = legacyCapabilities.getHash(); + } else { + node = null; + hash = null; + } + if (hash != null) { + getManager(DiscoManager.class).info(entity, node, hash); + } + } }