mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2025-09-10 18:59:41 +02:00
SMACK-361 Added support for Entity Capabilities.
git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13560 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
parent
1cdb86989a
commit
21be8c55ee
33 changed files with 2395 additions and 88 deletions
713
source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
Normal file
713
source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
Normal file
|
@ -0,0 +1,713 @@
|
|||
/**
|
||||
* Copyright 2009 Jonas Ådahl.
|
||||
* Copyright 2011-2013 Florian Schmaus
|
||||
*
|
||||
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jivesoftware.smackx.entitycaps;
|
||||
|
||||
import org.jivesoftware.smack.Connection;
|
||||
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||
import org.jivesoftware.smack.ConnectionListener;
|
||||
import org.jivesoftware.smack.PacketInterceptor;
|
||||
import org.jivesoftware.smack.PacketListener;
|
||||
import org.jivesoftware.smack.SmackConfiguration;
|
||||
import org.jivesoftware.smack.XMPPConnection;
|
||||
import org.jivesoftware.smack.XMPPException;
|
||||
import org.jivesoftware.smack.packet.IQ;
|
||||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smack.packet.PacketExtension;
|
||||
import org.jivesoftware.smack.packet.Presence;
|
||||
import org.jivesoftware.smack.filter.NotFilter;
|
||||
import org.jivesoftware.smack.filter.PacketFilter;
|
||||
import org.jivesoftware.smack.filter.AndFilter;
|
||||
import org.jivesoftware.smack.filter.PacketTypeFilter;
|
||||
import org.jivesoftware.smack.filter.PacketExtensionFilter;
|
||||
import org.jivesoftware.smack.util.Base64;
|
||||
import org.jivesoftware.smack.util.Cache;
|
||||
import org.jivesoftware.smackx.Form;
|
||||
import org.jivesoftware.smackx.FormField;
|
||||
import org.jivesoftware.smackx.NodeInformationProvider;
|
||||
import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
||||
import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache;
|
||||
import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
|
||||
import org.jivesoftware.smackx.packet.DiscoverInfo;
|
||||
import org.jivesoftware.smackx.packet.DataForm;
|
||||
import org.jivesoftware.smackx.packet.DiscoverInfo.Feature;
|
||||
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
|
||||
import org.jivesoftware.smackx.packet.DiscoverItems.Item;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Keeps track of entity capabilities.
|
||||
*
|
||||
* @author Florian Schmaus
|
||||
*/
|
||||
public class EntityCapsManager {
|
||||
|
||||
public static final String NAMESPACE = "http://jabber.org/protocol/caps";
|
||||
public static final String ELEMENT = "c";
|
||||
|
||||
private static final String ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
|
||||
private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
|
||||
|
||||
protected static EntityCapsPersistentCache persistentCache;
|
||||
|
||||
private static Map<Connection, EntityCapsManager> instances = Collections
|
||||
.synchronizedMap(new WeakHashMap<Connection, EntityCapsManager>());
|
||||
|
||||
/**
|
||||
* Map of (node + '#" + hash algorithm) to DiscoverInfo data
|
||||
*/
|
||||
protected static Map<String, DiscoverInfo> caps = new Cache<String, DiscoverInfo>(1000, -1);
|
||||
|
||||
/**
|
||||
* Map of Full JID -> DiscoverInfo/null. In case of c2s connection the
|
||||
* key is formed as user@server/resource (resource is required) In case of
|
||||
* link-local connection the key is formed as user@host (no resource) In
|
||||
* case of a server or component the key is formed as domain
|
||||
*/
|
||||
protected static Map<String, NodeVerHash> jidCaps = new Cache<String, NodeVerHash>(10000, -1);
|
||||
|
||||
static {
|
||||
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
|
||||
public void connectionCreated(Connection connection) {
|
||||
if (connection instanceof XMPPConnection)
|
||||
new EntityCapsManager(connection);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1");
|
||||
SUPPORTED_HASHES.put("sha-1", sha1MessageDigest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private WeakReference<Connection> weakRefConnection;
|
||||
private ServiceDiscoveryManager sdm;
|
||||
private boolean entityCapsEnabled;
|
||||
private String currentCapsVersion;
|
||||
private boolean presenceSend = false;
|
||||
private Queue<String> lastLocalCapsVersions = new ConcurrentLinkedQueue<String>();
|
||||
|
||||
/**
|
||||
* Add DiscoverInfo to the database.
|
||||
*
|
||||
* @param nodeVer
|
||||
* The node and verification String (e.g.
|
||||
* "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
|
||||
* @param info
|
||||
* DiscoverInfo for the specified node.
|
||||
*/
|
||||
public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
|
||||
caps.put(nodeVer, info);
|
||||
|
||||
if (persistentCache != null)
|
||||
persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Node version (node#ver) of a JID. Returns a String or null if
|
||||
* EntiyCapsManager does not have any information.
|
||||
*
|
||||
* @param user
|
||||
* the user (Full JID)
|
||||
* @return the node version (node#ver) or null
|
||||
*/
|
||||
public static String getNodeVersionByJid(String jid) {
|
||||
NodeVerHash nvh = jidCaps.get(jid);
|
||||
if (nvh != null) {
|
||||
return nvh.nodeVer;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static NodeVerHash getNodeVerHashByJid(String jid) {
|
||||
return jidCaps.get(jid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the discover info given a user name. The discover info is returned if
|
||||
* the user has a node#ver associated with it and the node#ver has a
|
||||
* discover info associated with it.
|
||||
*
|
||||
* @param user
|
||||
* user name (Full JID)
|
||||
* @return the discovered info
|
||||
*/
|
||||
public static DiscoverInfo getDiscoverInfoByUser(String user) {
|
||||
NodeVerHash nvh = jidCaps.get(user);
|
||||
if (nvh == null)
|
||||
return null;
|
||||
|
||||
return getDiscoveryInfoByNodeVer(nvh.nodeVer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve DiscoverInfo for a specific node.
|
||||
*
|
||||
* @param nodeVer
|
||||
* The node name (e.g.
|
||||
* "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
|
||||
* @return The corresponding DiscoverInfo or null if none is known.
|
||||
*/
|
||||
public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
|
||||
DiscoverInfo info = caps.get(nodeVer);
|
||||
if (info != null)
|
||||
info = new DiscoverInfo(info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent cache implementation
|
||||
*
|
||||
* @param cache
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void setPersistentCache(EntityCapsPersistentCache cache) throws IOException {
|
||||
if (persistentCache != null)
|
||||
throw new IllegalStateException("Entity Caps Persistent Cache was already set");
|
||||
persistentCache = cache;
|
||||
persistentCache.replay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum Cache size for the JID to nodeVer Cache
|
||||
*
|
||||
* @param maxCacheSize
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void setJidCapsMaxCacheSize(int maxCacheSize) {
|
||||
((Cache) jidCaps).setMaxCacheSize(maxCacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum Cache size for the nodeVer to DiscoverInfo Cache
|
||||
*
|
||||
* @param maxCacheSize
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void setCapsMaxCacheSize(int maxCacheSize) {
|
||||
((Cache) caps).setMaxCacheSize(maxCacheSize);
|
||||
}
|
||||
|
||||
private EntityCapsManager(Connection connection) {
|
||||
this.weakRefConnection = new WeakReference<Connection>(connection);
|
||||
this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
Connection connection = weakRefConnection.get();
|
||||
instances.put(connection, this);
|
||||
|
||||
connection.addConnectionListener(new ConnectionListener() {
|
||||
public void connectionClosed() {
|
||||
// Unregister this instance since the connection has been closed
|
||||
presenceSend = false;
|
||||
instances.remove(weakRefConnection.get());
|
||||
}
|
||||
|
||||
public void connectionClosedOnError(Exception e) {
|
||||
presenceSend = false;
|
||||
}
|
||||
|
||||
public void reconnectionFailed(Exception e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
public void reconnectingIn(int seconds) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
public void reconnectionSuccessful() {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
// This calculates the local entity caps version
|
||||
updateLocalEntityCaps();
|
||||
|
||||
if (SmackConfiguration.autoEnableEntityCaps())
|
||||
enableEntityCaps();
|
||||
|
||||
PacketFilter packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new PacketExtensionFilter(
|
||||
ELEMENT, NAMESPACE));
|
||||
connection.addPacketListener(new PacketListener() {
|
||||
// Listen for remote presence stanzas with the caps extension
|
||||
// If we receive such a stanza, record the JID and nodeVer
|
||||
@Override
|
||||
public void processPacket(Packet packet) {
|
||||
if (!entityCapsEnabled())
|
||||
return;
|
||||
|
||||
CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT,
|
||||
EntityCapsManager.NAMESPACE);
|
||||
|
||||
String hash = ext.getHash().toLowerCase();
|
||||
if (!SUPPORTED_HASHES.containsKey(hash))
|
||||
return;
|
||||
|
||||
String from = packet.getFrom();
|
||||
String node = ext.getNode();
|
||||
String ver = ext.getVer();
|
||||
|
||||
jidCaps.put(from, new NodeVerHash(node, ver, hash));
|
||||
}
|
||||
|
||||
}, packetFilter);
|
||||
|
||||
packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new NotFilter(new PacketExtensionFilter(
|
||||
ELEMENT, NAMESPACE)));
|
||||
connection.addPacketListener(new PacketListener() {
|
||||
@Override
|
||||
public void processPacket(Packet packet) {
|
||||
// always remove the JID from the map, even if entityCaps are
|
||||
// disabled
|
||||
String from = packet.getFrom();
|
||||
jidCaps.remove(from);
|
||||
}
|
||||
}, packetFilter);
|
||||
|
||||
packetFilter = new PacketTypeFilter(Presence.class);
|
||||
connection.addPacketSendingListener(new PacketListener() {
|
||||
@Override
|
||||
public void processPacket(Packet packet) {
|
||||
presenceSend = true;
|
||||
}
|
||||
}, packetFilter);
|
||||
|
||||
// Intercept presence packages and add caps data when intended.
|
||||
// XEP-0115 specifies that a client SHOULD include entity capabilities
|
||||
// with every presence notification it sends.
|
||||
PacketFilter capsPacketFilter = new PacketTypeFilter(Presence.class);
|
||||
PacketInterceptor packetInterceptor = new PacketInterceptor() {
|
||||
public void interceptPacket(Packet packet) {
|
||||
if (!entityCapsEnabled)
|
||||
return;
|
||||
|
||||
CapsExtension caps = new CapsExtension(ENTITY_NODE, getCapsVersion(), "sha-1");
|
||||
packet.addExtension(caps);
|
||||
}
|
||||
};
|
||||
connection.addPacketInterceptor(packetInterceptor, capsPacketFilter);
|
||||
// It's important to do this as last action. Since it changes the
|
||||
// behavior of the SDM in some ways
|
||||
sdm.setEntityCapsManager(this);
|
||||
}
|
||||
|
||||
public static synchronized EntityCapsManager getInstanceFor(Connection connection) {
|
||||
// For testing purposed forbid EntityCaps for non XMPPConnections
|
||||
// it may work on BOSH connections too
|
||||
if (!(connection instanceof XMPPConnection))
|
||||
return null;
|
||||
|
||||
if (SUPPORTED_HASHES.size() <= 0)
|
||||
return null;
|
||||
|
||||
EntityCapsManager entityCapsManager = instances.get(connection);
|
||||
|
||||
if (entityCapsManager == null) {
|
||||
entityCapsManager = new EntityCapsManager(connection);
|
||||
}
|
||||
|
||||
return entityCapsManager;
|
||||
}
|
||||
|
||||
public void enableEntityCaps() {
|
||||
// Add Entity Capabilities (XEP-0115) feature node.
|
||||
sdm.addFeature(NAMESPACE);
|
||||
updateLocalEntityCaps();
|
||||
entityCapsEnabled = true;
|
||||
}
|
||||
|
||||
public void disableEntityCaps() {
|
||||
entityCapsEnabled = false;
|
||||
sdm.removeFeature(NAMESPACE);
|
||||
}
|
||||
|
||||
public boolean entityCapsEnabled() {
|
||||
return entityCapsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a record telling what entity caps node a user has.
|
||||
*
|
||||
* @param user
|
||||
* the user (Full JID)
|
||||
*/
|
||||
public void removeUserCapsNode(String user) {
|
||||
jidCaps.remove(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our own caps version. The version depends on the enabled features. A
|
||||
* caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
|
||||
*
|
||||
* @return our own caps version
|
||||
*/
|
||||
public String getCapsVersion() {
|
||||
return currentCapsVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local entity's NodeVer (e.g.
|
||||
* "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
|
||||
* )
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getLocalNodeVer() {
|
||||
return ENTITY_NODE + '#' + getCapsVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Entity Caps are supported by a given JID
|
||||
*
|
||||
* @param jid
|
||||
* @return
|
||||
*/
|
||||
public boolean areEntityCapsSupported(String jid) {
|
||||
if (jid == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
DiscoverInfo result = sdm.discoverInfo(jid);
|
||||
return result.containsFeature(NAMESPACE);
|
||||
} catch (XMPPException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Entity Caps are supported by the local service/server
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean areEntityCapsSupportedByServer() {
|
||||
return areEntityCapsSupported(weakRefConnection.get().getServiceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local user Entity Caps information with the data provided
|
||||
*
|
||||
* If we are connected and there was already a presence send, another
|
||||
* presence is send to inform others about your new Entity Caps node string.
|
||||
*
|
||||
* @param discoverInfo
|
||||
* the local users discover info (mostly the service discovery
|
||||
* features)
|
||||
* @param identityType
|
||||
* the local users identity type
|
||||
* @param identityName
|
||||
* the local users identity name
|
||||
* @param extendedInfo
|
||||
* the local users extended info
|
||||
*/
|
||||
public void updateLocalEntityCaps() {
|
||||
Connection connection = weakRefConnection.get();
|
||||
|
||||
DiscoverInfo discoverInfo = new DiscoverInfo();
|
||||
discoverInfo.setType(IQ.Type.RESULT);
|
||||
discoverInfo.setNode(getLocalNodeVer());
|
||||
if (connection != null)
|
||||
discoverInfo.setFrom(connection.getUser());
|
||||
sdm.addDiscoverInfoTo(discoverInfo);
|
||||
|
||||
currentCapsVersion = generateVerificationString(discoverInfo, "sha-1");
|
||||
addDiscoverInfoByNode(ENTITY_NODE + '#' + currentCapsVersion, discoverInfo);
|
||||
if (lastLocalCapsVersions.size() > 10) {
|
||||
String oldCapsVersion = lastLocalCapsVersions.poll();
|
||||
sdm.removeNodeInformationProvider(ENTITY_NODE + '#' + oldCapsVersion);
|
||||
}
|
||||
lastLocalCapsVersions.add(currentCapsVersion);
|
||||
|
||||
caps.put(currentCapsVersion, discoverInfo);
|
||||
if (connection != null)
|
||||
jidCaps.put(connection.getUser(), new NodeVerHash(ENTITY_NODE, currentCapsVersion, "sha-1"));
|
||||
|
||||
sdm.setNodeInformationProvider(ENTITY_NODE + '#' + currentCapsVersion, new NodeInformationProvider() {
|
||||
List<String> features = sdm.getFeaturesList();
|
||||
List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getIdentities());
|
||||
List<PacketExtension> packetExtensions = sdm.getExtendedInfoAsList();
|
||||
|
||||
@Override
|
||||
public List<Item> getNodeItems() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNodeFeatures() {
|
||||
return features;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identity> getNodeIdentities() {
|
||||
return identities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PacketExtension> getNodePacketExtensions() {
|
||||
return packetExtensions;
|
||||
}
|
||||
});
|
||||
|
||||
// Send an empty presence, and let the packet intercepter
|
||||
// add a <c/> node to it.
|
||||
// See http://xmpp.org/extensions/xep-0115.html#advertise
|
||||
// We only send a presence packet if there was already one send
|
||||
// to respect ConnectionConfiguration.isSendPresence()
|
||||
if (connection != null && connection.isAuthenticated() && presenceSend) {
|
||||
Presence presence = new Presence(Presence.Type.available);
|
||||
connection.sendPacket(presence);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
|
||||
* Method
|
||||
*
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
|
||||
* 5.4 Processing Method</a>
|
||||
*
|
||||
* @param capsNode
|
||||
* the caps node (i.e. node#ver)
|
||||
* @param info
|
||||
* @return true if it's valid and should be cache, false if not
|
||||
*/
|
||||
public static boolean verifyDiscvoerInfoVersion(String ver, String hash, DiscoverInfo info) {
|
||||
// step 3.3 check for duplicate identities
|
||||
if (info.containsDuplicateIdentities())
|
||||
return false;
|
||||
|
||||
// step 3.4 check for duplicate features
|
||||
if (info.containsDuplicateFeatures())
|
||||
return false;
|
||||
|
||||
// step 3.5 check for well-formed packet extensions
|
||||
if (verifyPacketExtensions(info))
|
||||
return false;
|
||||
|
||||
String calculatedVer = generateVerificationString(info, hash);
|
||||
|
||||
if (!ver.equals(calculatedVer))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param info
|
||||
* @return true if the packet extensions is ill-formed
|
||||
*/
|
||||
protected static boolean verifyPacketExtensions(DiscoverInfo info) {
|
||||
List<FormField> foundFormTypes = new LinkedList<FormField>();
|
||||
for (Iterator<PacketExtension> i = info.getExtensions().iterator(); i.hasNext();) {
|
||||
PacketExtension pe = i.next();
|
||||
if (pe.getNamespace().equals(Form.NAMESPACE)) {
|
||||
DataForm df = (DataForm) pe;
|
||||
for (Iterator<FormField> it = df.getFields(); it.hasNext();) {
|
||||
FormField f = it.next();
|
||||
if (f.getVariable().equals("FORM_TYPE")) {
|
||||
for (FormField fft : foundFormTypes) {
|
||||
if (f.equals(fft))
|
||||
return true;
|
||||
}
|
||||
foundFormTypes.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a XEP-115 Verification String
|
||||
*
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
|
||||
* Verification String</a>
|
||||
*
|
||||
* @param discoverInfo
|
||||
* @param hash
|
||||
* the used hash function
|
||||
* @return The generated verification String or null if the hash is not
|
||||
* supported
|
||||
*/
|
||||
protected static String generateVerificationString(DiscoverInfo discoverInfo, String hash) {
|
||||
MessageDigest md = SUPPORTED_HASHES.get(hash.toLowerCase());
|
||||
if (md == null)
|
||||
return null;
|
||||
|
||||
DataForm extendedInfo = (DataForm) discoverInfo.getExtension(Form.ELEMENT, Form.NAMESPACE);
|
||||
|
||||
// 1. Initialize an empty string S ('sb' in this method).
|
||||
StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
|
||||
// need thread-safe StringBuffer
|
||||
|
||||
// 2. Sort the service discovery identities by category and then by
|
||||
// type and then by xml:lang
|
||||
// (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
|
||||
// [NAME]. Note that each slash is included even if the LANG or
|
||||
// NAME is not included (in accordance with XEP-0030, the category and
|
||||
// type MUST be included.
|
||||
SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
|
||||
;
|
||||
for (Iterator<DiscoverInfo.Identity> it = discoverInfo.getIdentities(); it.hasNext();)
|
||||
sortedIdentities.add(it.next());
|
||||
|
||||
// 3. For each identity, append the 'category/type/lang/name' to S,
|
||||
// followed by the '<' character.
|
||||
for (Iterator<DiscoverInfo.Identity> it = sortedIdentities.iterator(); it.hasNext();) {
|
||||
DiscoverInfo.Identity identity = it.next();
|
||||
sb.append(identity.getCategory());
|
||||
sb.append("/");
|
||||
sb.append(identity.getType());
|
||||
sb.append("/");
|
||||
sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
|
||||
sb.append("/");
|
||||
sb.append(identity.getName() == null ? "" : identity.getName());
|
||||
sb.append("<");
|
||||
}
|
||||
|
||||
// 4. Sort the supported service discovery features.
|
||||
SortedSet<String> features = new TreeSet<String>();
|
||||
for (Iterator<Feature> it = discoverInfo.getFeatures(); it.hasNext();)
|
||||
features.add(it.next().getVar());
|
||||
|
||||
// 5. For each feature, append the feature to S, followed by the '<'
|
||||
// character
|
||||
for (String f : features) {
|
||||
sb.append(f);
|
||||
sb.append("<");
|
||||
}
|
||||
|
||||
// only use the data form for calculation is it has a hidden FORM_TYPE
|
||||
// field
|
||||
// see XEP-0115 5.4 step 3.6
|
||||
if (extendedInfo != null && extendedInfo.hasHiddenFromTypeField()) {
|
||||
synchronized (extendedInfo) {
|
||||
// 6. If the service discovery information response includes
|
||||
// XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
|
||||
// by the XML character data of the <value/> element).
|
||||
SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
|
||||
public int compare(FormField f1, FormField f2) {
|
||||
return f1.getVariable().compareTo(f2.getVariable());
|
||||
}
|
||||
});
|
||||
|
||||
FormField ft = null;
|
||||
|
||||
for (Iterator<FormField> i = extendedInfo.getFields(); i.hasNext();) {
|
||||
FormField f = i.next();
|
||||
if (!f.getVariable().equals("FORM_TYPE")) {
|
||||
fs.add(f);
|
||||
} else {
|
||||
ft = f;
|
||||
}
|
||||
}
|
||||
|
||||
// Add FORM_TYPE values
|
||||
if (ft != null) {
|
||||
formFieldValuesToCaps(ft.getValues(), sb);
|
||||
}
|
||||
|
||||
// 7. 3. For each field other than FORM_TYPE:
|
||||
// 1. Append the value of the "var" attribute, followed by the
|
||||
// '<' character.
|
||||
// 2. Sort values by the XML character data of the <value/>
|
||||
// element.
|
||||
// 3. For each <value/> element, append the XML character data,
|
||||
// followed by the '<' character.
|
||||
for (FormField f : fs) {
|
||||
sb.append(f.getVariable());
|
||||
sb.append("<");
|
||||
formFieldValuesToCaps(f.getValues(), sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
|
||||
// 3269).
|
||||
// 9. Compute the verification string by hashing S using the algorithm
|
||||
// specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
|
||||
// 3174).
|
||||
// The hashed data MUST be generated with binary output and
|
||||
// encoded using Base64 as specified in Section 4 of RFC 4648
|
||||
// (note: the Base64 output MUST NOT include whitespace and MUST set
|
||||
// padding bits to zero).
|
||||
byte[] digest = md.digest(sb.toString().getBytes());
|
||||
return Base64.encodeBytes(digest);
|
||||
}
|
||||
|
||||
private static void formFieldValuesToCaps(Iterator<String> i, StringBuilder sb) {
|
||||
SortedSet<String> fvs = new TreeSet<String>();
|
||||
while (i.hasNext()) {
|
||||
fvs.add(i.next());
|
||||
}
|
||||
for (String fv : fvs) {
|
||||
sb.append(fv);
|
||||
sb.append("<");
|
||||
}
|
||||
}
|
||||
|
||||
public static class NodeVerHash {
|
||||
private String node;
|
||||
private String hash;
|
||||
private String ver;
|
||||
private String nodeVer;
|
||||
|
||||
NodeVerHash(String node, String ver, String hash) {
|
||||
this.node = node;
|
||||
this.ver = ver;
|
||||
this.hash = hash;
|
||||
nodeVer = node + "#" + ver;
|
||||
}
|
||||
|
||||
public String getNodeVer() {
|
||||
return nodeVer;
|
||||
}
|
||||
|
||||
public String getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public String getVer() {
|
||||
return ver;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue