summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/isode/stroke/disco')
-rw-r--r--src/com/isode/stroke/disco/CapsInfoGenerator.java73
-rw-r--r--src/com/isode/stroke/disco/CapsManager.java144
-rw-r--r--src/com/isode/stroke/disco/CapsMemoryStorage.java26
-rw-r--r--src/com/isode/stroke/disco/CapsProvider.java14
-rw-r--r--src/com/isode/stroke/disco/CapsStorage.java12
-rw-r--r--src/com/isode/stroke/disco/ClientDiscoManager.java76
-rw-r--r--src/com/isode/stroke/disco/DiscoInfoResponder.java54
-rw-r--r--src/com/isode/stroke/disco/EntityCapsManager.java93
-rw-r--r--src/com/isode/stroke/disco/EntityCapsProvider.java21
-rw-r--r--src/com/isode/stroke/disco/GetDiscoInfoRequest.java32
10 files changed, 545 insertions, 0 deletions
diff --git a/src/com/isode/stroke/disco/CapsInfoGenerator.java b/src/com/isode/stroke/disco/CapsInfoGenerator.java
new file mode 100644
index 0000000..2bbd843
--- /dev/null
+++ b/src/com/isode/stroke/disco/CapsInfoGenerator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.crypto.CryptoProvider;
+import com.isode.stroke.elements.CapsInfo;
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.elements.Form;
+import com.isode.stroke.elements.FormField;
+import com.isode.stroke.stringcodecs.Base64;
+
+public class CapsInfoGenerator {
+ private String node_;
+ private CryptoProvider crypto_;
+
+ private final static Comparator<FormField> compareFields = new Comparator<FormField>() {
+ @Override
+ public int compare(FormField lhs, FormField rhs) {
+ return lhs.getName().compareTo(rhs.getName());
+ }
+ };
+
+ CapsInfoGenerator(final String node, CryptoProvider crypto) {
+ this.node_ = node;
+ this.crypto_ = crypto;
+ }
+
+ CapsInfo generateCapsInfo(final DiscoInfo discoInfo) {
+ String serializedCaps = "";
+
+ List<DiscoInfo.Identity> identities = discoInfo.getIdentities();
+ Collections.sort(identities);
+ for (final DiscoInfo.Identity identity : identities) {
+ serializedCaps += identity.getCategory() + "/" + identity.getType()
+ + "/" + identity.getLanguage() + "/" + identity.getName()
+ + "<";
+ }
+
+ List<String> features = discoInfo.getFeatures();
+ Collections.sort(features);
+ for (final String feature : features) {
+ serializedCaps += feature + "<";
+ }
+
+ for (Form extension : discoInfo.getExtensions()) {
+ serializedCaps += extension.getFormType() + "<";
+ List<FormField> fields = extension.getFields();
+ Collections.sort(fields, compareFields);
+ for (FormField field : fields) {
+ if ("FORM_TYPE".equals(field.getName())) {
+ continue;
+ }
+ serializedCaps += field.getName() + "<";
+ List<String> values = field.getValues();
+ Collections.sort(values);
+ for (final String value : values) {
+ serializedCaps += value + "<";
+ }
+ }
+ }
+
+ String version = Base64.encode(crypto_ .getSHA1Hash(new ByteArray(serializedCaps)));
+ return new CapsInfo(node_, version, "sha-1");
+ }
+
+}
diff --git a/src/com/isode/stroke/disco/CapsManager.java b/src/com/isode/stroke/disco/CapsManager.java
new file mode 100644
index 0000000..2e4c45b
--- /dev/null
+++ b/src/com/isode/stroke/disco/CapsManager.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.isode.stroke.client.StanzaChannel;
+import com.isode.stroke.crypto.CryptoProvider;
+import com.isode.stroke.elements.CapsInfo;
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.elements.ErrorPayload;
+import com.isode.stroke.elements.Presence;
+import com.isode.stroke.jid.JID;
+import com.isode.stroke.queries.IQRouter;
+import com.isode.stroke.signals.Slot1;
+import com.isode.stroke.signals.Slot2;
+
+public class CapsManager extends CapsProvider {
+
+ private final IQRouter iqRouter;
+ private final CryptoProvider crypto;
+ private final CapsStorage capsStorage;
+ private boolean warnOnInvalidHash;
+ private Set<String> requestedDiscoInfos = new HashSet<String>();
+ private Set<CapsPair> failingCaps = new HashSet<CapsPair>();
+ private Map<String, Set<CapsPair>> fallbacks = new HashMap<String, Set<CapsPair>>();
+
+ private class CapsPair {
+ JID jid;
+ String node;
+
+ CapsPair(JID j, String n) {jid = j; node = n;}
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof CapsPair)) return false;
+ CapsPair o1 = (CapsPair) o;
+ return jid.equals(o1.jid) && node.equals(o1.node);
+ }
+
+ @Override public int hashCode() {return jid.hashCode() * 5 + node.hashCode();}
+ }
+
+ public CapsManager(CapsStorage capsStorage, StanzaChannel stanzaChannel,
+ IQRouter iqRouter, CryptoProvider crypto) {
+ this.iqRouter = iqRouter;
+ this.crypto = crypto;
+ this.capsStorage = capsStorage;
+ this.warnOnInvalidHash = true;
+
+ stanzaChannel.onPresenceReceived.connect(new Slot1<Presence>() {
+ @Override
+ public void call(Presence p1) {
+ handlePresenceReceived(p1);
+ }
+ });
+ stanzaChannel.onAvailableChanged.connect(new Slot1<Boolean>() {
+ @Override
+ public void call(Boolean p1) {
+ handleStanzaChannelAvailableChanged(p1);
+ }
+ });
+ }
+
+ private void handlePresenceReceived(Presence presence) {
+ CapsInfo capsInfo = presence.getPayload(new CapsInfo());
+ if (capsInfo == null || !capsInfo.getHash().equals("sha-1")
+ || presence.getPayload(new ErrorPayload()) != null) {
+ return;
+ }
+ String hash = capsInfo.getVersion();
+ if (capsStorage.getDiscoInfo(hash) != null) {
+ return;
+ }
+ if (failingCaps.contains(new CapsPair(presence.getFrom(), hash))) {
+ return;
+ }
+ if (requestedDiscoInfos.contains(hash)) {
+ Set<CapsPair> fallback = fallbacks.get(hash);
+ if (fallback == null) fallbacks.put(hash, fallback = new HashSet<CapsPair>());
+ fallback.add(new CapsPair(presence.getFrom(), capsInfo.getNode()));
+ return;
+ }
+ requestDiscoInfo(presence.getFrom(), capsInfo.getNode(), hash);
+ }
+
+ private void handleStanzaChannelAvailableChanged(boolean available) {
+ if (available) {
+ failingCaps.clear();
+ fallbacks.clear();
+ requestedDiscoInfos.clear();
+ }
+ }
+
+ private void handleDiscoInfoReceived(final JID from, final String hash, DiscoInfo discoInfo, ErrorPayload error) {
+ requestedDiscoInfos.remove(hash);
+ if (error != null || discoInfo == null
+ || !new CapsInfoGenerator("", crypto).generateCapsInfo(discoInfo).getVersion().equals(hash)) {
+ if (warnOnInvalidHash && error == null && discoInfo != null) {
+// std.cerr << "Warning: Caps from " << from.toString() << " do not verify" << std.endl;
+ }
+ failingCaps.add(new CapsPair(from, hash));
+ Set<CapsPair> i = fallbacks.get(hash);
+ if (i != null && !i.isEmpty()) {
+ CapsPair fallbackAndNode = i.iterator().next();
+ i.remove(fallbackAndNode);
+ requestDiscoInfo(fallbackAndNode.jid, fallbackAndNode.node, hash);
+ }
+ return;
+ }
+ fallbacks.remove(hash);
+ capsStorage.setDiscoInfo(hash, discoInfo);
+ onCapsAvailable.emit(hash);
+ }
+
+ private void requestDiscoInfo(final JID jid, final String node, final String hash) {
+ GetDiscoInfoRequest request = GetDiscoInfoRequest.create(jid, node
+ + "#" + hash, iqRouter);
+ request.onResponse.connect(new Slot2<DiscoInfo, ErrorPayload>() {
+ @Override
+ public void call(DiscoInfo p1, ErrorPayload p2) {
+ handleDiscoInfoReceived(jid, hash, p1, p2);
+ }
+ });
+ requestedDiscoInfos.add(hash);
+ request.send();
+ }
+
+ @Override
+ DiscoInfo getCaps(String hash) {
+ return capsStorage.getDiscoInfo(hash);
+ }
+
+ // Mainly for testing purposes
+ void setWarnOnInvalidHash(boolean b) {
+ warnOnInvalidHash = b;
+ }
+
+}
diff --git a/src/com/isode/stroke/disco/CapsMemoryStorage.java b/src/com/isode/stroke/disco/CapsMemoryStorage.java
new file mode 100644
index 0000000..81e0e77
--- /dev/null
+++ b/src/com/isode/stroke/disco/CapsMemoryStorage.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.isode.stroke.elements.DiscoInfo;
+
+public class CapsMemoryStorage implements CapsStorage {
+ private Map<String, DiscoInfo> caps = new HashMap<String, DiscoInfo>();
+
+ @Override
+ public DiscoInfo getDiscoInfo(String s) {
+ return caps.get(s);
+ }
+
+ @Override
+ public void setDiscoInfo(String s, DiscoInfo d) {
+ caps.put(s, d);
+
+ }
+
+}
diff --git a/src/com/isode/stroke/disco/CapsProvider.java b/src/com/isode/stroke/disco/CapsProvider.java
new file mode 100644
index 0000000..d83eebc
--- /dev/null
+++ b/src/com/isode/stroke/disco/CapsProvider.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.signals.Signal1;
+
+public abstract class CapsProvider {
+ abstract DiscoInfo getCaps(final String hash);
+
+ public final Signal1<String> onCapsAvailable = new Signal1<String>();
+}
diff --git a/src/com/isode/stroke/disco/CapsStorage.java b/src/com/isode/stroke/disco/CapsStorage.java
new file mode 100644
index 0000000..07c1cd7
--- /dev/null
+++ b/src/com/isode/stroke/disco/CapsStorage.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import com.isode.stroke.elements.DiscoInfo;
+
+public interface CapsStorage {
+ DiscoInfo getDiscoInfo(final String s);
+ void setDiscoInfo(final String s, DiscoInfo d);
+}
diff --git a/src/com/isode/stroke/disco/ClientDiscoManager.java b/src/com/isode/stroke/disco/ClientDiscoManager.java
new file mode 100644
index 0000000..3770fbc
--- /dev/null
+++ b/src/com/isode/stroke/disco/ClientDiscoManager.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import com.isode.stroke.crypto.CryptoProvider;
+import com.isode.stroke.elements.CapsInfo;
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.presence.PayloadAddingPresenceSender;
+import com.isode.stroke.presence.PresenceSender;
+import com.isode.stroke.queries.IQRouter;
+
+public class ClientDiscoManager {
+ private PayloadAddingPresenceSender presenceSender;
+ private CryptoProvider crypto;
+ private DiscoInfoResponder discoInfoResponder;
+ private String capsNode;
+ private CapsInfo capsInfo;
+
+ /**
+ * Constructs the manager
+ *
+ * \param iqRouter the router on which requests will be answered \param
+ * presenceSender the presence sender to which all outgoing presence (with
+ * caps information) will be sent.
+ */
+ public ClientDiscoManager(IQRouter iqRouter, PresenceSender presenceSender,
+ CryptoProvider crypto) {
+ this.crypto = crypto;
+ discoInfoResponder = new DiscoInfoResponder(iqRouter);
+ discoInfoResponder.start();
+ this.presenceSender = new PayloadAddingPresenceSender(presenceSender);
+ }
+
+ void delete() {
+ discoInfoResponder.stop();
+ }
+
+ /**
+ * Needs to be called before calling setDiscoInfo().
+ */
+ public void setCapsNode(final String node) {
+ capsNode = node;
+ }
+
+ /**
+ * Sets the capabilities of the client.
+ */
+ public void setDiscoInfo(final DiscoInfo discoInfo) {
+ capsInfo = new CapsInfoGenerator(capsNode, crypto).generateCapsInfo(discoInfo);
+ discoInfoResponder.clearDiscoInfo();
+ discoInfoResponder.setDiscoInfo(discoInfo);
+ discoInfoResponder.setDiscoInfo(
+ capsInfo.getNode() + "#" + capsInfo.getVersion(), discoInfo);
+ presenceSender.setPayload(capsInfo);
+ }
+
+ /**
+ * Returns the presence sender through which all outgoing presence should be
+ * sent. The manager will add the necessary caps information, and forward it
+ * to the presence sender passed at construction time.
+ */
+ public PresenceSender getPresenceSender() {
+ return presenceSender;
+ }
+
+ /**
+ * Called when the client is connected. This resets the presence sender,
+ * such that it assumes initial presence hasn't been sent yet.
+ */
+ void handleConnected() {
+ presenceSender.reset();
+ }
+
+}
diff --git a/src/com/isode/stroke/disco/DiscoInfoResponder.java b/src/com/isode/stroke/disco/DiscoInfoResponder.java
new file mode 100644
index 0000000..c0edca2
--- /dev/null
+++ b/src/com/isode/stroke/disco/DiscoInfoResponder.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.elements.ErrorPayload;
+import com.isode.stroke.jid.JID;
+import com.isode.stroke.queries.GetResponder;
+import com.isode.stroke.queries.IQRouter;
+
+public class DiscoInfoResponder extends GetResponder<DiscoInfo> {
+ private DiscoInfo info_;
+ private Map<String, DiscoInfo> nodeInfo_ = new HashMap<String, DiscoInfo>();
+
+ public DiscoInfoResponder(IQRouter router) {
+ super(new DiscoInfo(), router);
+ }
+
+ void clearDiscoInfo() {
+ info_ = new DiscoInfo();
+ nodeInfo_.clear();
+ }
+
+ void setDiscoInfo(final DiscoInfo info) {
+ info_ = info;
+ }
+
+ void setDiscoInfo(final String node, final DiscoInfo info) {
+ DiscoInfo newInfo = info;
+ newInfo.setNode(node);
+ nodeInfo_.put(node, newInfo);
+ }
+
+ protected boolean handleGetRequest(final JID from, final JID j, final String id, DiscoInfo info) {
+ if (info.getNode().isEmpty()) {
+ sendResponse(from, id, info_);
+ }
+ else {
+ DiscoInfo i = nodeInfo_.get(info.getNode());
+ if (i != null) {
+ sendResponse(from, id, i);
+ }
+ else {
+ sendError(from, id, ErrorPayload.Condition.ItemNotFound, ErrorPayload.Type.Cancel);
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/isode/stroke/disco/EntityCapsManager.java b/src/com/isode/stroke/disco/EntityCapsManager.java
new file mode 100644
index 0000000..a41ec11
--- /dev/null
+++ b/src/com/isode/stroke/disco/EntityCapsManager.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.isode.stroke.client.StanzaChannel;
+import com.isode.stroke.elements.CapsInfo;
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.elements.ErrorPayload;
+import com.isode.stroke.elements.Presence;
+import com.isode.stroke.jid.JID;
+import com.isode.stroke.signals.Slot1;
+
+public class EntityCapsManager extends EntityCapsProvider {
+ private final CapsProvider capsProvider;
+ private final Map<JID, String> caps = new HashMap<JID, String>();
+
+ public EntityCapsManager(CapsProvider capsProvider, StanzaChannel stanzaChannel) {
+ this.capsProvider = capsProvider;
+
+ stanzaChannel.onPresenceReceived.connect(new Slot1<Presence>() {
+ @Override
+ public void call(Presence p1) {
+ handlePresenceReceived(p1);
+ }
+ });
+ stanzaChannel.onAvailableChanged.connect(new Slot1<Boolean>() {
+ @Override
+ public void call(Boolean p1) {
+ handleStanzaChannelAvailableChanged(p1);
+ }
+ });
+ capsProvider.onCapsAvailable.connect(new Slot1<String>() {
+ @Override
+ public void call(String p1) {
+ handleCapsAvailable(p1);
+ }
+ });
+ }
+
+ private void handlePresenceReceived(Presence presence) {
+ JID from = presence.getFrom();
+ if (presence.isAvailable()) {
+ CapsInfo capsInfo = presence.getPayload(new CapsInfo());
+ if (capsInfo == null || !capsInfo.getHash().equals("sha-1") || presence.getPayload(new ErrorPayload()) != null) {
+ return;
+ }
+ String hash = capsInfo.getVersion();
+ String i = caps.get(from);
+ if (!hash.equals(i)) {
+ caps.put(from, hash);
+ DiscoInfo disco = capsProvider.getCaps(hash);
+ if (disco != null || i != null) {
+ onCapsChanged.emit(from);
+ }
+ }
+ }
+ else {
+ if (caps.remove(from) != null) {
+ onCapsChanged.emit(from);
+ }
+ }
+ }
+
+ private void handleStanzaChannelAvailableChanged(boolean available) {
+ if (available) {
+ for (JID i : caps.keySet()) {
+ onCapsChanged.emit(i);
+ }
+ caps.clear();
+ }
+ }
+
+ private void handleCapsAvailable(final String hash) {
+ // TODO: Use Boost.Bimap ?
+ for (JID i : caps.keySet()) {
+ if (caps.get(i).equals(hash)) {
+ onCapsChanged.emit(i);
+ }
+ }
+ }
+
+ public DiscoInfo getCaps(final JID jid) {
+ if (caps.containsKey(jid)) {
+ return capsProvider.getCaps(caps.get(jid));
+ }
+ return new DiscoInfo();
+ }
+}
diff --git a/src/com/isode/stroke/disco/EntityCapsProvider.java b/src/com/isode/stroke/disco/EntityCapsProvider.java
new file mode 100644
index 0000000..fd30173
--- /dev/null
+++ b/src/com/isode/stroke/disco/EntityCapsProvider.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.jid.JID;
+import com.isode.stroke.signals.Signal1;
+
+public abstract class EntityCapsProvider {
+ /**
+ * Returns the service discovery information of the given JID.
+ */
+ public abstract DiscoInfo getCaps(final JID jid);
+
+ /**
+ * Emitted when the capabilities of a JID changes.
+ */
+ public final Signal1<JID> onCapsChanged = new Signal1<JID>();
+}
diff --git a/src/com/isode/stroke/disco/GetDiscoInfoRequest.java b/src/com/isode/stroke/disco/GetDiscoInfoRequest.java
new file mode 100644
index 0000000..f820b2f
--- /dev/null
+++ b/src/com/isode/stroke/disco/GetDiscoInfoRequest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2015, Isode Limited, London, England.
+ * All rights reserved.
+ */
+package com.isode.stroke.disco;
+
+import com.isode.stroke.elements.DiscoInfo;
+import com.isode.stroke.elements.IQ;
+import com.isode.stroke.jid.JID;
+import com.isode.stroke.queries.GenericRequest;
+import com.isode.stroke.queries.IQRouter;
+
+public class GetDiscoInfoRequest extends GenericRequest<DiscoInfo> {
+
+ public static GetDiscoInfoRequest create(final JID jid, IQRouter router) {
+ return new GetDiscoInfoRequest(jid, router);
+ }
+
+ public static GetDiscoInfoRequest create(final JID jid, final String node, IQRouter router) {
+ return new GetDiscoInfoRequest(jid, node, router);
+ }
+
+ private GetDiscoInfoRequest(final JID jid, IQRouter router) {
+ super(IQ.Type.Get, jid, new DiscoInfo(), router);
+ }
+
+ private GetDiscoInfoRequest(final JID jid, final String node, IQRouter router) {
+ this(jid, router);
+ getPayloadGeneric().setNode(node);
+ }
+
+}