001/**
002 *
003 * Copyright © 2009 Jonas Ådahl, 2011-2024 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.caps;
018
019import java.nio.charset.StandardCharsets;
020import java.security.MessageDigest;
021import java.security.NoSuchAlgorithmException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Queue;
032import java.util.Set;
033import java.util.SortedSet;
034import java.util.TreeSet;
035import java.util.WeakHashMap;
036import java.util.concurrent.ConcurrentLinkedQueue;
037
038import org.jivesoftware.smack.ConnectionCreationListener;
039import org.jivesoftware.smack.ConnectionListener;
040import org.jivesoftware.smack.Manager;
041import org.jivesoftware.smack.SmackConfiguration;
042import org.jivesoftware.smack.SmackException.NoResponseException;
043import org.jivesoftware.smack.SmackException.NotConnectedException;
044import org.jivesoftware.smack.StanzaListener;
045import org.jivesoftware.smack.XMPPConnection;
046import org.jivesoftware.smack.XMPPConnectionRegistry;
047import org.jivesoftware.smack.XMPPException.XMPPErrorException;
048import org.jivesoftware.smack.filter.AndFilter;
049import org.jivesoftware.smack.filter.PresenceTypeFilter;
050import org.jivesoftware.smack.filter.StanzaExtensionFilter;
051import org.jivesoftware.smack.filter.StanzaFilter;
052import org.jivesoftware.smack.filter.StanzaTypeFilter;
053import org.jivesoftware.smack.packet.Presence;
054import org.jivesoftware.smack.packet.PresenceBuilder;
055import org.jivesoftware.smack.packet.Stanza;
056import org.jivesoftware.smack.roster.AbstractPresenceEventListener;
057import org.jivesoftware.smack.roster.Roster;
058import org.jivesoftware.smack.util.StringUtils;
059import org.jivesoftware.smack.util.stringencoder.Base64;
060
061import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache;
062import org.jivesoftware.smackx.caps.packet.CapsExtension;
063import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
064import org.jivesoftware.smackx.disco.DiscoInfoLookupShortcutMechanism;
065import org.jivesoftware.smackx.disco.EntityCapabilitiesChangedListener;
066import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
067import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
068import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Feature;
069import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
070import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
071import org.jivesoftware.smackx.disco.packet.DiscoverInfoView;
072import org.jivesoftware.smackx.xdata.FormField;
073import org.jivesoftware.smackx.xdata.packet.DataForm;
074
075import org.jxmpp.jid.DomainBareJid;
076import org.jxmpp.jid.FullJid;
077import org.jxmpp.jid.Jid;
078import org.jxmpp.util.cache.LruCache;
079
080/**
081 * Manages own and others Entity Capabilities (XEP-0115).
082 * <p>
083 * Entity Capabilities is an XMPP extension which, in order to minimize network impact, caches the capabilities of
084 * remote XMPP entities. Those capabilities are determine with the help of the Service Discovery Protocol
085 * (<a href="https://xmpp.org/extensions/xep-0030.html">XEP-0030</a>, {@link ServiceDiscoveryManager}).
086 * </p>
087 *
088 * <h2>Usage</h2>
089 * <p>
090 * Entity Capabilities work silently in the background when enabled. If the remote XMPP entity does not support XEP-0115
091 * but XEP-0030 then XEP-0030 mechanisms are transparently used.
092 * </p>
093 * <p>
094 * The caches used by Smack for Entity Capabilities is non-persistent per default. However, it is is also possible to set
095 * a persistent Entity Capabilities cache, which is recommended.
096 * </p>
097 * <h2>Examples</h2>
098 *
099 * <h3>Enable Entity Capabilities</h3>
100 * <pre>{@code
101 * // Get an instance of entity caps manager for the specified connection
102 * EntityCapsManager mgr = EntityCapsManager.getInstanceFor(connection);
103 * // Enable entity capabilities
104 * mgr.enableEntityCaps();
105 * }</pre>
106 *
107 * <h3>Configure a persistent cache for Entity Capabilities</h3>
108 * <pre>{@code
109 * // Get an instance of entity caps manager for the specified connection
110 * EntityCapsManager mgr = EntityCapsManager.getInstanceFor(connection);
111 * // Create an cache, see smackx.entitycaps.cache for pre-defined cache implementations
112 * EntityCapsPersistentCache cache = new SimpleDirectoryPersistentCache(new File("/foo/cachedir"));
113 * // Set the cache
114 * mgr.setPersistentCache(cache);
115 * }</pre>
116 *
117 * @author Florian Schmaus
118 * @see <a href="http://www.xmpp.org/extensions/xep-0115.html">XEP-0115: Entity Capabilities</a>
119 */
120public final class EntityCapsManager extends Manager {
121
122    public static final String NAMESPACE = CapsExtension.NAMESPACE;
123    public static final String ELEMENT = CapsExtension.ELEMENT;
124
125    private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
126
127    /**
128     * The default hash. Currently 'sha-1'.
129     */
130    private static final String DEFAULT_HASH = StringUtils.SHA1;
131
132    private static String DEFAULT_ENTITY_NODE = SmackConfiguration.SMACK_URL_STRING;
133
134    static EntityCapsPersistentCache persistentCache;
135
136    private static boolean autoEnableEntityCaps = true;
137
138    private static final Map<XMPPConnection, EntityCapsManager> instances = new WeakHashMap<>();
139
140    private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter(
141                    ELEMENT, NAMESPACE));
142
143    /**
144     * Map of "node + '#' + hash" to DiscoverInfo data
145     */
146    static final LruCache<String, DiscoverInfo> CAPS_CACHE = new LruCache<>(1000);
147
148    /**
149     * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
150     * key is formed as user@server/resource (resource is required) In case of
151     * link-local connection the key is formed as user@host (no resource) In
152     * case of a server or component the key is formed as domain
153     */
154    static final LruCache<Jid, NodeVerHash> JID_TO_NODEVER_CACHE = new LruCache<>(10000);
155
156    static {
157        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
158            @Override
159            public void connectionCreated(XMPPConnection connection) {
160                getInstanceFor(connection);
161            }
162        });
163
164        try {
165            MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH);
166            SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest);
167        } catch (NoSuchAlgorithmException e) {
168            // Ignore
169        }
170
171        ServiceDiscoveryManager.addDiscoInfoLookupShortcutMechanism(new DiscoInfoLookupShortcutMechanism("XEP-0115: Entity Capabilities", 100) {
172            @Override
173            public DiscoverInfo getDiscoverInfoByUser(ServiceDiscoveryManager serviceDiscoveryManager, Jid jid) {
174                DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(jid);
175                if (info != null) {
176                    return info;
177                }
178
179                NodeVerHash nodeVerHash = getNodeVerHashByJid(jid);
180                if (nodeVerHash == null) {
181                    return null;
182                }
183
184                try {
185                    info = serviceDiscoveryManager.discoverInfo(jid, nodeVerHash.getNodeVer());
186                } catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) {
187                    // TODO log
188                    return null;
189                }
190
191                if (verifyDiscoverInfoVersion(nodeVerHash.getVer(), nodeVerHash.getHash(), info)) {
192                    addDiscoverInfoByNode(nodeVerHash.getNodeVer(), info);
193                } else {
194                    // TODO log
195                }
196
197                return info;
198            }
199        });
200    }
201
202    /**
203     * Set the default entity node that will be used for new EntityCapsManagers.
204     *
205     * @param entityNode TODO javadoc me please
206     */
207    public static void setDefaultEntityNode(String entityNode) {
208        DEFAULT_ENTITY_NODE = entityNode;
209    }
210
211    /**
212     * Add DiscoverInfo to the database.
213     *
214     * @param nodeVer TODO javadoc me please
215     *            The node and verification String (e.g.
216     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
217     * @param info TODO javadoc me please
218     *            DiscoverInfo for the specified node.
219     */
220    static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
221        CAPS_CACHE.put(nodeVer, info);
222
223        if (persistentCache != null)
224            persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
225    }
226
227    /**
228     * Get the Node version (node#ver) of a JID. Returns a String or null if
229     * EntityCapsManager does not have any information.
230     *
231     * @param jid TODO javadoc me please
232     *            the user (Full JID)
233     * @return the node version (node#ver) or null
234     */
235    public static String getNodeVersionByJid(Jid jid) {
236        NodeVerHash nvh = JID_TO_NODEVER_CACHE.lookup(jid);
237        if (nvh != null) {
238            return nvh.nodeVer;
239        } else {
240            return null;
241        }
242    }
243
244    public static NodeVerHash getNodeVerHashByJid(Jid jid) {
245        return JID_TO_NODEVER_CACHE.lookup(jid);
246    }
247
248    /**
249     * Get the discover info given a user name. The discover info is returned if
250     * the user has a node#ver associated with it and the node#ver has a
251     * discover info associated with it.
252     *
253     * @param user TODO javadoc me please
254     *            user name (Full JID)
255     * @return the discovered info
256     */
257    public static DiscoverInfo getDiscoverInfoByUser(Jid user) {
258        NodeVerHash nvh = JID_TO_NODEVER_CACHE.lookup(user);
259        if (nvh == null)
260            return null;
261
262        return getDiscoveryInfoByNodeVer(nvh.nodeVer);
263    }
264
265    /**
266     * Retrieve DiscoverInfo for a specific node.
267     *
268     * @param nodeVer TODO javadoc me please
269     *            The node name (e.g.
270     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
271     * @return The corresponding DiscoverInfo or null if none is known.
272     */
273    public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
274        DiscoverInfo info = CAPS_CACHE.lookup(nodeVer);
275
276        // If it was not in CAPS_CACHE, try to retrieve the information from persistentCache
277        if (info == null && persistentCache != null) {
278            info = persistentCache.lookup(nodeVer);
279            // Promote the information to CAPS_CACHE if one was found
280            if (info != null) {
281                CAPS_CACHE.put(nodeVer, info);
282            }
283        }
284
285        // If we were able to retrieve information from one of the caches, copy it before returning
286        if (info != null)
287            info = new DiscoverInfo(info);
288
289        return info;
290    }
291
292    /**
293     * Set the persistent cache implementation.
294     *
295     * @param cache TODO javadoc me please
296     */
297    public static void setPersistentCache(EntityCapsPersistentCache cache) {
298        persistentCache = cache;
299    }
300
301    /**
302     * Sets the maximum cache sizes.
303     *
304     * @param maxJidToNodeVerSize TODO javadoc me please
305     * @param maxCapsCacheSize TODO javadoc me please
306     */
307    public static void setMaxsCacheSizes(int maxJidToNodeVerSize, int maxCapsCacheSize) {
308        JID_TO_NODEVER_CACHE.setMaxCacheSize(maxJidToNodeVerSize);
309        CAPS_CACHE.setMaxCacheSize(maxCapsCacheSize);
310    }
311
312    /**
313     * Clears the memory cache.
314     */
315    public static void clearMemoryCache() {
316        JID_TO_NODEVER_CACHE.clear();
317        CAPS_CACHE.clear();
318    }
319
320    private static void addCapsExtensionInfo(Jid from, CapsExtension capsExtension) {
321        String capsExtensionHash = capsExtension.getHash();
322        String hashInUppercase = capsExtensionHash.toUpperCase(Locale.US);
323        // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
324        if (!SUPPORTED_HASHES.containsKey(hashInUppercase))
325            return;
326        String hash = capsExtensionHash.toLowerCase(Locale.US);
327
328        String node = capsExtension.getNode();
329        String ver = capsExtension.getVer();
330
331        JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash));
332    }
333
334    private final Queue<CapsVersionAndHash> lastLocalCapsVersions = new ConcurrentLinkedQueue<>();
335
336    private final ServiceDiscoveryManager sdm;
337
338    private boolean entityCapsEnabled;
339    private CapsVersionAndHash currentCapsVersion;
340
341    /**
342     * The entity node String used by this EntityCapsManager instance.
343     */
344    private String entityNode = DEFAULT_ENTITY_NODE;
345
346    // Intercept presence packages and add caps data when intended.
347    // XEP-0115 specifies that a client SHOULD include entity capabilities
348    // with every presence notification it sends.
349    private void addCapsExtension(PresenceBuilder presenceBuilder) {
350        final CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
351        if (capsVersionAndHash == null) {
352            return;
353        }
354        CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash);
355        presenceBuilder.overrideExtension(caps);
356    }
357
358    private EntityCapsManager(XMPPConnection connection) {
359        super(connection);
360        this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
361        instances.put(connection, this);
362
363        connection.addConnectionListener(new ConnectionListener() {
364            @Override
365            public void connected(XMPPConnection connection) {
366                // It's not clear when a server would report the caps stream
367                // feature, so we try to process it after we are connected and
368                // once after we are authenticated.
369                processCapsStreamFeatureIfAvailable(connection);
370            }
371            @Override
372            public void authenticated(XMPPConnection connection, boolean resumed) {
373                // It's not clear when a server would report the caps stream
374                // feature, so we try to process it after we are connected and
375                // once after we are authenticated.
376                processCapsStreamFeatureIfAvailable(connection);
377            }
378            private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) {
379                CapsExtension capsExtension = connection.getFeature(
380                                CapsExtension.class);
381                if (capsExtension == null) {
382                    return;
383                }
384                DomainBareJid from = connection.getXMPPServiceDomain();
385                addCapsExtensionInfo(from, capsExtension);
386            }
387        });
388
389        if (autoEnableEntityCaps)
390            enableEntityCaps();
391
392        // Note that this is a *synchronous* stanza listener to avoid unnecessary feature lookups. If this were to be an
393        // asynchronous listener, then it would be possible that the entity caps information was not processed when the
394        // features of entity are looked up. See SMACK-937.
395        connection.addStanzaListener(new StanzaListener() {
396            // Listen for remote presence stanzas with the caps extension
397            // If we receive such a stanza, record the JID and nodeVer
398            @Override
399            public void processStanza(Stanza packet) {
400                if (!entityCapsEnabled())
401                    return;
402
403                CapsExtension capsExtension = CapsExtension.from(packet);
404                Jid from = packet.getFrom();
405                addCapsExtensionInfo(from, capsExtension);
406            }
407
408        }, PRESENCES_WITH_CAPS);
409
410        Roster.getInstanceFor(connection).addPresenceEventListener(new AbstractPresenceEventListener() {
411            @Override
412            public void presenceUnavailable(FullJid from, Presence presence) {
413                JID_TO_NODEVER_CACHE.remove(from);
414            }
415        });
416
417        // It's important to do this as last action. Since it changes the
418        // behavior of the SDM in some ways
419        sdm.addEntityCapabilitiesChangedListener(new EntityCapabilitiesChangedListener() {
420            @Override
421            public void onEntityCapabilitiesChanged(DiscoverInfo synthesizedDiscoveryInfo) {
422                if (!entityCapsEnabled()) {
423                    return;
424                }
425
426                updateLocalEntityCaps(synthesizedDiscoveryInfo);
427            }
428        });
429    }
430
431    public static synchronized EntityCapsManager getInstanceFor(XMPPConnection connection) {
432        if (SUPPORTED_HASHES.size() <= 0)
433            throw new IllegalStateException("No supported hashes for EntityCapsManager");
434
435        EntityCapsManager entityCapsManager = instances.get(connection);
436
437        if (entityCapsManager == null) {
438            entityCapsManager = new EntityCapsManager(connection);
439        }
440
441        return entityCapsManager;
442    }
443
444    public synchronized void enableEntityCaps() {
445        connection().addPresenceInterceptor(this::addCapsExtension, p -> {
446            return PresenceTypeFilter.AVAILABLE.accept(p);
447        });
448
449        // Add Entity Capabilities (XEP-0115) feature node.
450        sdm.addFeature(NAMESPACE);
451        entityCapsEnabled = true;
452    }
453
454    public synchronized void disableEntityCaps() {
455        entityCapsEnabled = false;
456        sdm.removeFeature(NAMESPACE);
457
458        connection().removePresenceInterceptor(this::addCapsExtension);
459    }
460
461    public boolean entityCapsEnabled() {
462        return entityCapsEnabled;
463    }
464
465    /**
466     * Remove a record telling what entity caps node a user has.
467     *
468     * @param user TODO javadoc me please
469     *            the user (Full JID)
470     */
471    public static void removeUserCapsNode(Jid user) {
472        // While JID_TO_NODEVER_CHACHE has the generic types <Jid, NodeVerHash>, it is ok to call remove with String
473        // arguments, since the same Jid and String representations would be equal and have the same hash code.
474        JID_TO_NODEVER_CACHE.remove(user);
475    }
476
477    /**
478     * Get our own caps version or {@code null} if none is yet set. The version depends on the enabled features.
479     * A caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
480     *
481     * @return our own caps version or {@code null}.
482     */
483    public CapsVersionAndHash getCapsVersionAndHash() {
484        return currentCapsVersion;
485    }
486
487    /**
488     * Returns the local entity's NodeVer (e.g.
489     * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
490     * )
491     *
492     * @return the local NodeVer
493     */
494    public String getLocalNodeVer() {
495        CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
496        if (capsVersionAndHash == null) {
497            return null;
498        }
499        return entityNode + '#' + capsVersionAndHash.version;
500    }
501
502    /**
503     * Returns true if Entity Caps are supported by a given JID.
504     *
505     * @param jid TODO javadoc me please
506     * @return true if the entity supports Entity Capabilities.
507     * @throws XMPPErrorException if there was an XMPP error returned.
508     * @throws NoResponseException if there was no response from the remote entity.
509     * @throws NotConnectedException if the XMPP connection is not connected.
510     * @throws InterruptedException if the calling thread was interrupted.
511     */
512    public boolean areEntityCapsSupported(Jid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
513        return sdm.supportsFeature(jid, NAMESPACE);
514    }
515
516    /**
517     * Returns true if Entity Caps are supported by the local service/server.
518     *
519     * @return true if the user's server supports Entity Capabilities.
520     * @throws XMPPErrorException if there was an XMPP error returned.
521     * @throws NoResponseException if there was no response from the remote entity.
522     * @throws NotConnectedException if the XMPP connection is not connected.
523     * @throws InterruptedException if the calling thread was interrupted.
524     */
525    public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
526        return areEntityCapsSupported(connection().getXMPPServiceDomain());
527    }
528
529    /**
530     * Updates the local user Entity Caps information with the data provided
531     *
532     * If we are connected and there was already a presence send, another
533     * presence is send to inform others about your new Entity Caps node string.
534     *
535     */
536    private void updateLocalEntityCaps(DiscoverInfo synthesizedDiscoveryInfo) {
537        XMPPConnection connection = connection();
538
539        DiscoverInfoBuilder discoverInfoBuilder = synthesizedDiscoveryInfo.asBuilder("synthesized-disco-info-result");
540        // getLocalNodeVer() will return a result only after currentCapsVersion is set. Therefore
541        // set it first and then call getLocalNodeVer()
542        currentCapsVersion = generateVerificationString(discoverInfoBuilder);
543        final String localNodeVer = getLocalNodeVer();
544        discoverInfoBuilder.setNode(localNodeVer);
545
546        final DiscoverInfo discoverInfo = discoverInfoBuilder.build();
547        addDiscoverInfoByNode(localNodeVer, discoverInfo);
548
549        if (lastLocalCapsVersions.size() > 10) {
550            CapsVersionAndHash oldCapsVersion = lastLocalCapsVersions.poll();
551            sdm.removeNodeInformationProvider(entityNode + '#' + oldCapsVersion.version);
552        }
553        lastLocalCapsVersions.add(currentCapsVersion);
554
555        if (connection != null)
556            JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion));
557
558        final List<Identity> identities = new ArrayList<>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities());
559        sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() {
560            List<String> features = sdm.getFeatures();
561            List<DataForm> packetExtensions = sdm.getExtendedInfo();
562            @Override
563            public List<String> getNodeFeatures() {
564                return features;
565            }
566            @Override
567            public List<Identity> getNodeIdentities() {
568                return identities;
569            }
570            @Override
571            public List<DataForm> getNodePacketExtensions() {
572                return packetExtensions;
573            }
574        });
575    }
576
577    /**
578     * Verify DiscoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
579     * Method.
580     *
581     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
582     *      5.4 Processing Method</a>
583     *
584     * @param ver TODO javadoc me please
585     * @param hash TODO javadoc me please
586     * @param info TODO javadoc me please
587     * @return true if it's valid and should be cache, false if not
588     */
589    public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) {
590        // step 3.3 check for duplicate identities
591        if (info.containsDuplicateIdentities())
592            return false;
593
594        // step 3.4 check for duplicate features
595        if (info.containsDuplicateFeatures())
596            return false;
597
598        // step 3.5 check for well-formed packet extensions
599        if (!verifyPacketExtensions(info))
600            return false;
601
602        String calculatedVer = generateVerificationString(info, hash).version;
603
604        if (!ver.equals(calculatedVer))
605            return false;
606
607        return true;
608    }
609
610    /**
611     * Verify that the given discovery info is not ill-formed.
612     *
613     * @param info the discovery info to verify.
614     * @return true if the stanza extensions is not ill-formed
615     */
616    private static boolean verifyPacketExtensions(DiscoverInfo info) {
617        Set<String> foundFormTypes = new HashSet<>();
618        List<DataForm> dataForms = info.getExtensions(DataForm.class);
619        for (DataForm dataForm : dataForms) {
620            FormField formFieldTypeField = dataForm.getHiddenFormTypeField();
621            if (formFieldTypeField == null) {
622                continue;
623            }
624
625            String type = formFieldTypeField.getFirstValue();
626            boolean noDuplicate = foundFormTypes.add(type);
627            if (!noDuplicate) {
628                // Ill-formed extension: duplicate forms (by form field type string).
629                return false;
630            }
631        }
632
633        return true;
634    }
635
636    static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo) {
637        return generateVerificationString(discoverInfo, null);
638    }
639
640    /**
641     * Generates a XEP-115 Verification String
642     *
643     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
644     *      Verification String</a>
645     *
646     * @param discoverInfo TODO javadoc me please
647     * @param hash TODO javadoc me please
648     *            the used hash function, if null, default hash will be used
649     * @return The generated verification String or null if the hash is not
650     *         supported
651     */
652    static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo, String hash) {
653        if (hash == null) {
654            hash = DEFAULT_HASH;
655        }
656        // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
657        MessageDigest md = SUPPORTED_HASHES.get(hash.toUpperCase(Locale.US));
658        if (md == null)
659            return null;
660        // Then transform the hash to lowercase, as this value will be put on the wire within the caps element's hash
661        // attribute. I'm not sure if the standard is case insensitive here, but let's assume that even it is, there could
662        // be "broken" implementation in the wild, so we *always* transform to lowercase.
663        hash = hash.toLowerCase(Locale.US);
664
665        // 1. Initialize an empty string S ('sb' in this method).
666        StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
667                                                // need thread-safe StringBuffer
668
669        // 2. Sort the service discovery identities by category and then by
670        // type and then by xml:lang
671        // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
672        // [NAME]. Note that each slash is included even if the LANG or
673        // NAME is not included (in accordance with XEP-0030, the category and
674        // type MUST be included.
675        SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<>();
676
677        sortedIdentities.addAll(discoverInfo.getIdentities());
678
679        // 3. For each identity, append the 'category/type/lang/name' to S,
680        // followed by the '<' character.
681        for (DiscoverInfo.Identity identity : sortedIdentities) {
682            sb.append(identity.getCategory());
683            sb.append('/');
684            sb.append(identity.getType());
685            sb.append('/');
686            sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
687            sb.append('/');
688            sb.append(identity.getName() == null ? "" : identity.getName());
689            sb.append('<');
690        }
691
692        // 4. Sort the supported service discovery features.
693        SortedSet<String> features = new TreeSet<>();
694        for (Feature f : discoverInfo.getFeatures())
695            features.add(f.getVar());
696
697        // 5. For each feature, append the feature to S, followed by the '<'
698        // character
699        for (String f : features) {
700            sb.append(f);
701            sb.append('<');
702        }
703
704        List<DataForm> extendedInfos = discoverInfo.getExtensions(DataForm.class);
705        final Iterator<DataForm> iter = extendedInfos.iterator();
706        while (iter.hasNext()) {
707            if (!iter.next().hasHiddenFormTypeField()) {
708                // Only use the data form for calculation is it has a hidden FORM_TYPE field.
709                // See XEP-0115 5.4 step 3.f
710                iter.remove();
711            }
712        }
713
714        // 6. If the service discovery information response includes
715        // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
716        // by the XML character data of the <value/> element).
717        Collections.sort(extendedInfos, new Comparator<DataForm>() {
718            @Override
719            public int compare(DataForm dataFormLeft, DataForm dataFormRight) {
720                final String formTypeLeft = dataFormLeft.getFormType();
721                assert formTypeLeft != null; // ensured by the previous step.
722                final String formTypeRight = dataFormRight.getFormType();
723                assert formTypeRight != null; // ensured by the previous step.
724                return formTypeLeft.compareTo(formTypeRight);
725            }
726        });
727
728        for (DataForm extendedInfo : extendedInfos) {
729            SortedSet<FormField> fs = new TreeSet<>(new Comparator<FormField>() {
730                @Override
731                public int compare(FormField f1, FormField f2) {
732                    return f1.getFieldName().compareTo(f2.getFieldName());
733                }
734            });
735
736            for (FormField f : extendedInfo.getFields()) {
737                if (!f.getFieldName().equals("FORM_TYPE")) {
738                    fs.add(f);
739                }
740            }
741
742            // Add FORM_TYPE values
743            formFieldValuesToCaps(Collections.singletonList(extendedInfo.getFormType()), sb);
744
745            // 7. 3. For each field other than FORM_TYPE:
746            // 1. Append the value of the "var" attribute, followed by the
747            // '<' character.
748            // 2. Sort values by the XML character data of the <value/>
749            // element.
750            // 3. For each <value/> element, append the XML character data,
751            // followed by the '<' character.
752            for (FormField f : fs) {
753                sb.append(f.getFieldName());
754                sb.append('<');
755                formFieldValuesToCaps(f.getRawValueCharSequences(), sb);
756            }
757        }
758
759        // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
760        // 3269).
761        // 9. Compute the verification string by hashing S using the algorithm
762        // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
763        // 3174).
764        // The hashed data MUST be generated with binary output and
765        // encoded using Base64 as specified in Section 4 of RFC 4648
766        // (note: the Base64 output MUST NOT include whitespace and MUST set
767        // padding bits to zero).
768        byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8);
769        byte[] digest;
770        synchronized (md) {
771            digest = md.digest(bytes);
772        }
773        String version = Base64.encodeToString(digest);
774        return new CapsVersionAndHash(version, hash);
775    }
776
777    private static void formFieldValuesToCaps(List<? extends CharSequence> i, StringBuilder sb) {
778        SortedSet<CharSequence> fvs = new TreeSet<>();
779        fvs.addAll(i);
780        for (CharSequence fv : fvs) {
781            sb.append(fv);
782            sb.append('<');
783        }
784    }
785
786    public static class NodeVerHash {
787        private String node;
788        private String hash;
789        private String ver;
790        private String nodeVer;
791
792        NodeVerHash(String node, CapsVersionAndHash capsVersionAndHash) {
793            this(node, capsVersionAndHash.version, capsVersionAndHash.hash);
794        }
795
796        NodeVerHash(String node, String ver, String hash) {
797            this.node = node;
798            this.ver = ver;
799            this.hash = hash;
800            nodeVer = node + "#" + ver;
801        }
802
803        public String getNodeVer() {
804            return nodeVer;
805        }
806
807        public String getNode() {
808            return node;
809        }
810
811        public String getHash() {
812            return hash;
813        }
814
815        public String getVer() {
816            return ver;
817        }
818    }
819}