diff options
Diffstat (limited to 'src/com/isode/stroke/disco')
-rw-r--r-- | src/com/isode/stroke/disco/CapsInfoGenerator.java | 73 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/CapsManager.java | 144 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/CapsMemoryStorage.java | 26 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/CapsProvider.java | 14 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/CapsStorage.java | 12 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/ClientDiscoManager.java | 76 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/DiscoInfoResponder.java | 54 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/EntityCapsManager.java | 93 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/EntityCapsProvider.java | 21 | ||||
-rw-r--r-- | src/com/isode/stroke/disco/GetDiscoInfoRequest.java | 32 |
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); + } + +} |