parse caps from presence

This commit is contained in:
Daniel Gultsch 2023-01-17 13:09:03 +01:00
parent a2b21d97eb
commit 43a82e504b
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
11 changed files with 216 additions and 39 deletions

View file

@ -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";
}

View file

@ -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;

View file

@ -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));
}
}
}

View file

@ -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));
}
}
}

View file

@ -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<Id, Class<? extends Extension>> EXTENSION_CLASS_MAP;

View file

@ -2826,7 +2826,7 @@ public class XmppConnection implements Runnable {
return ConversationsDatabase.getInstance(context);
}
public <T extends AbstractManager> T getManager(Class<T> type) {
protected <T extends AbstractManager> T getManager(Class<T> type) {
return connection.managers.getInstance(type);
}
}

View file

@ -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<Void> 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<InfoQuery> info(final Jid entity, final String node) {
final var iqRequest = new IqPacket(IqPacket.TYPE.GET);
iqRequest.setTo(entity);

View file

@ -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());
}
}
}

View file

@ -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<Hash> 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;
}
}

View file

@ -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;
}
}
}

View file

@ -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<PresencePacket> {
public class PresenceProcessor extends XmppConnection.Delegate implements Consumer<PresencePacket> {
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);
}
}
}