Package org.jivesoftware.smackx.entitycaps

Source Code of org.jivesoftware.smackx.entitycaps.EntityCapsManager

/**
* 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 -&gt; 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 verifyDiscoverInfoVersion(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.hasHiddenFormTypeField()) {
            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;
        }
    }
}
TOP

Related Classes of org.jivesoftware.smackx.entitycaps.EntityCapsManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.