respond to disco#info queries

This commit is contained in:
Daniel Gultsch 2023-01-25 19:23:07 +01:00
parent 57d264d72e
commit e073f22ec0
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
9 changed files with 222 additions and 82 deletions

View file

@ -44,7 +44,7 @@ public class Element {
}
public void addExtensions(final Collection<? extends Extension> extensions) {
for(final Extension extension : extensions) {
for (final Extension extension : extensions) {
addExtension(extension);
}
}

View file

@ -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<String> NAMESPACES = Arrays.asList(
Version.FT_3.namespace,
Version.FT_4.namespace,
Version.FT_5.namespace
);
public static List<String> 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;

View file

@ -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<String> features;
public final Identity identity;
public ServiceDescription(List<String> features, Identity identity) {
this.features = features;
this.identity = identity;
}
public InfoQuery asInfoQuery() {
final var infoQuery = new InfoQuery();
final Collection<Feature> 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;
}
}
}

View file

@ -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) {

View file

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

View file

@ -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<EntityCapabilities.Hash, InfoQuery> outgoingCapsHash = new HashMap<>();
private final Map<EntityCapabilities.Hash, ServiceDescription> 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);
}
}

View file

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

View file

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

View file

@ -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"
+ " <identity category='client' name='Exodus 0.9.1' type='pc'/>\n"
+ " <feature var='http://jabber.org/protocol/caps'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#info'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#items'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#info'/>\n"
+ " <feature var='http://jabber.org/protocol/muc'/>\n"
+ " </query>";
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);
}
}