Home | History | Annotate | Download | only in entitycaps
      1 /**
      2  * Copyright 2009 Jonas dahl.
      3  * Copyright 2011-2013 Florian Schmaus
      4  *
      5  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package org.jivesoftware.smackx.entitycaps;
     19 
     20 import org.jivesoftware.smack.Connection;
     21 import org.jivesoftware.smack.ConnectionCreationListener;
     22 import org.jivesoftware.smack.ConnectionListener;
     23 import org.jivesoftware.smack.PacketInterceptor;
     24 import org.jivesoftware.smack.PacketListener;
     25 import org.jivesoftware.smack.SmackConfiguration;
     26 import org.jivesoftware.smack.XMPPConnection;
     27 import org.jivesoftware.smack.XMPPException;
     28 import org.jivesoftware.smack.packet.IQ;
     29 import org.jivesoftware.smack.packet.Packet;
     30 import org.jivesoftware.smack.packet.PacketExtension;
     31 import org.jivesoftware.smack.packet.Presence;
     32 import org.jivesoftware.smack.filter.NotFilter;
     33 import org.jivesoftware.smack.filter.PacketFilter;
     34 import org.jivesoftware.smack.filter.AndFilter;
     35 import org.jivesoftware.smack.filter.PacketTypeFilter;
     36 import org.jivesoftware.smack.filter.PacketExtensionFilter;
     37 import org.jivesoftware.smack.util.Base64;
     38 import org.jivesoftware.smack.util.Cache;
     39 import org.jivesoftware.smackx.Form;
     40 import org.jivesoftware.smackx.FormField;
     41 import org.jivesoftware.smackx.NodeInformationProvider;
     42 import org.jivesoftware.smackx.ServiceDiscoveryManager;
     43 import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache;
     44 import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
     45 import org.jivesoftware.smackx.packet.DiscoverInfo;
     46 import org.jivesoftware.smackx.packet.DataForm;
     47 import org.jivesoftware.smackx.packet.DiscoverInfo.Feature;
     48 import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
     49 import org.jivesoftware.smackx.packet.DiscoverItems.Item;
     50 
     51 import java.util.Collections;
     52 import java.util.Comparator;
     53 import java.util.HashMap;
     54 import java.util.Iterator;
     55 import java.util.LinkedList;
     56 import java.util.List;
     57 import java.util.Map;
     58 import java.util.Queue;
     59 import java.util.SortedSet;
     60 import java.util.TreeSet;
     61 import java.util.WeakHashMap;
     62 import java.util.concurrent.ConcurrentLinkedQueue;
     63 import java.io.IOException;
     64 import java.lang.ref.WeakReference;
     65 import java.security.MessageDigest;
     66 import java.security.NoSuchAlgorithmException;
     67 
     68 /**
     69  * Keeps track of entity capabilities.
     70  *
     71  * @author Florian Schmaus
     72  */
     73 public class EntityCapsManager {
     74 
     75     public static final String NAMESPACE = "http://jabber.org/protocol/caps";
     76     public static final String ELEMENT = "c";
     77 
     78     private static final String ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
     79     private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
     80 
     81     protected static EntityCapsPersistentCache persistentCache;
     82 
     83     private static Map<Connection, EntityCapsManager> instances = Collections
     84             .synchronizedMap(new WeakHashMap<Connection, EntityCapsManager>());
     85 
     86     /**
     87      * Map of (node + '#" + hash algorithm) to DiscoverInfo data
     88      */
     89     protected static Map<String, DiscoverInfo> caps = new Cache<String, DiscoverInfo>(1000, -1);
     90 
     91     /**
     92      * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
     93      * key is formed as user@server/resource (resource is required) In case of
     94      * link-local connection the key is formed as user@host (no resource) In
     95      * case of a server or component the key is formed as domain
     96      */
     97     protected static Map<String, NodeVerHash> jidCaps = new Cache<String, NodeVerHash>(10000, -1);
     98 
     99     static {
    100         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
    101             public void connectionCreated(Connection connection) {
    102                 if (connection instanceof XMPPConnection)
    103                     new EntityCapsManager(connection);
    104             }
    105         });
    106 
    107         try {
    108             MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1");
    109             SUPPORTED_HASHES.put("sha-1", sha1MessageDigest);
    110         } catch (NoSuchAlgorithmException e) {
    111             // Ignore
    112         }
    113     }
    114 
    115     private WeakReference<Connection> weakRefConnection;
    116     private ServiceDiscoveryManager sdm;
    117     private boolean entityCapsEnabled;
    118     private String currentCapsVersion;
    119     private boolean presenceSend = false;
    120     private Queue<String> lastLocalCapsVersions = new ConcurrentLinkedQueue<String>();
    121 
    122     /**
    123      * Add DiscoverInfo to the database.
    124      *
    125      * @param nodeVer
    126      *            The node and verification String (e.g.
    127      *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
    128      * @param info
    129      *            DiscoverInfo for the specified node.
    130      */
    131     public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
    132         caps.put(nodeVer, info);
    133 
    134         if (persistentCache != null)
    135             persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
    136     }
    137 
    138     /**
    139      * Get the Node version (node#ver) of a JID. Returns a String or null if
    140      * EntiyCapsManager does not have any information.
    141      *
    142      * @param user
    143      *            the user (Full JID)
    144      * @return the node version (node#ver) or null
    145      */
    146     public static String getNodeVersionByJid(String jid) {
    147         NodeVerHash nvh = jidCaps.get(jid);
    148         if (nvh != null) {
    149             return nvh.nodeVer;
    150         } else {
    151             return null;
    152         }
    153     }
    154 
    155     public static NodeVerHash getNodeVerHashByJid(String jid) {
    156         return jidCaps.get(jid);
    157     }
    158 
    159     /**
    160      * Get the discover info given a user name. The discover info is returned if
    161      * the user has a node#ver associated with it and the node#ver has a
    162      * discover info associated with it.
    163      *
    164      * @param user
    165      *            user name (Full JID)
    166      * @return the discovered info
    167      */
    168     public static DiscoverInfo getDiscoverInfoByUser(String user) {
    169         NodeVerHash nvh = jidCaps.get(user);
    170         if (nvh == null)
    171             return null;
    172 
    173         return getDiscoveryInfoByNodeVer(nvh.nodeVer);
    174     }
    175 
    176     /**
    177      * Retrieve DiscoverInfo for a specific node.
    178      *
    179      * @param nodeVer
    180      *            The node name (e.g.
    181      *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
    182      * @return The corresponding DiscoverInfo or null if none is known.
    183      */
    184     public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
    185         DiscoverInfo info = caps.get(nodeVer);
    186         if (info != null)
    187             info = new DiscoverInfo(info);
    188 
    189         return info;
    190     }
    191 
    192     /**
    193      * Set the persistent cache implementation
    194      *
    195      * @param cache
    196      * @throws IOException
    197      */
    198     public static void setPersistentCache(EntityCapsPersistentCache cache) throws IOException {
    199         if (persistentCache != null)
    200             throw new IllegalStateException("Entity Caps Persistent Cache was already set");
    201         persistentCache = cache;
    202         persistentCache.replay();
    203     }
    204 
    205     /**
    206      * Sets the maximum Cache size for the JID to nodeVer Cache
    207      *
    208      * @param maxCacheSize
    209      */
    210     @SuppressWarnings("rawtypes")
    211     public static void setJidCapsMaxCacheSize(int maxCacheSize) {
    212         ((Cache) jidCaps).setMaxCacheSize(maxCacheSize);
    213     }
    214 
    215     /**
    216      * Sets the maximum Cache size for the nodeVer to DiscoverInfo Cache
    217      *
    218      * @param maxCacheSize
    219      */
    220     @SuppressWarnings("rawtypes")
    221     public static void setCapsMaxCacheSize(int maxCacheSize) {
    222         ((Cache) caps).setMaxCacheSize(maxCacheSize);
    223     }
    224 
    225     private EntityCapsManager(Connection connection) {
    226         this.weakRefConnection = new WeakReference<Connection>(connection);
    227         this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
    228         init();
    229     }
    230 
    231     private void init() {
    232         Connection connection = weakRefConnection.get();
    233         instances.put(connection, this);
    234 
    235         connection.addConnectionListener(new ConnectionListener() {
    236             public void connectionClosed() {
    237                 // Unregister this instance since the connection has been closed
    238                 presenceSend = false;
    239                 instances.remove(weakRefConnection.get());
    240             }
    241 
    242             public void connectionClosedOnError(Exception e) {
    243                 presenceSend = false;
    244             }
    245 
    246             public void reconnectionFailed(Exception e) {
    247                 // ignore
    248             }
    249 
    250             public void reconnectingIn(int seconds) {
    251                 // ignore
    252             }
    253 
    254             public void reconnectionSuccessful() {
    255                 // ignore
    256             }
    257         });
    258 
    259         // This calculates the local entity caps version
    260         updateLocalEntityCaps();
    261 
    262         if (SmackConfiguration.autoEnableEntityCaps())
    263             enableEntityCaps();
    264 
    265         PacketFilter packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new PacketExtensionFilter(
    266                 ELEMENT, NAMESPACE));
    267         connection.addPacketListener(new PacketListener() {
    268             // Listen for remote presence stanzas with the caps extension
    269             // If we receive such a stanza, record the JID and nodeVer
    270             @Override
    271             public void processPacket(Packet packet) {
    272                 if (!entityCapsEnabled())
    273                     return;
    274 
    275                 CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT,
    276                         EntityCapsManager.NAMESPACE);
    277 
    278                 String hash = ext.getHash().toLowerCase();
    279                 if (!SUPPORTED_HASHES.containsKey(hash))
    280                     return;
    281 
    282                 String from = packet.getFrom();
    283                 String node = ext.getNode();
    284                 String ver = ext.getVer();
    285 
    286                 jidCaps.put(from, new NodeVerHash(node, ver, hash));
    287             }
    288 
    289         }, packetFilter);
    290 
    291         packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new NotFilter(new PacketExtensionFilter(
    292                 ELEMENT, NAMESPACE)));
    293         connection.addPacketListener(new PacketListener() {
    294             @Override
    295             public void processPacket(Packet packet) {
    296                 // always remove the JID from the map, even if entityCaps are
    297                 // disabled
    298                 String from = packet.getFrom();
    299                 jidCaps.remove(from);
    300             }
    301         }, packetFilter);
    302 
    303         packetFilter = new PacketTypeFilter(Presence.class);
    304         connection.addPacketSendingListener(new PacketListener() {
    305             @Override
    306             public void processPacket(Packet packet) {
    307                 presenceSend = true;
    308             }
    309         }, packetFilter);
    310 
    311         // Intercept presence packages and add caps data when intended.
    312         // XEP-0115 specifies that a client SHOULD include entity capabilities
    313         // with every presence notification it sends.
    314         PacketFilter capsPacketFilter = new PacketTypeFilter(Presence.class);
    315         PacketInterceptor packetInterceptor = new PacketInterceptor() {
    316             public void interceptPacket(Packet packet) {
    317                 if (!entityCapsEnabled)
    318                     return;
    319 
    320                 CapsExtension caps = new CapsExtension(ENTITY_NODE, getCapsVersion(), "sha-1");
    321                 packet.addExtension(caps);
    322             }
    323         };
    324         connection.addPacketInterceptor(packetInterceptor, capsPacketFilter);
    325         // It's important to do this as last action. Since it changes the
    326         // behavior of the SDM in some ways
    327         sdm.setEntityCapsManager(this);
    328     }
    329 
    330     public static synchronized EntityCapsManager getInstanceFor(Connection connection) {
    331         // For testing purposed forbid EntityCaps for non XMPPConnections
    332         // it may work on BOSH connections too
    333         if (!(connection instanceof XMPPConnection))
    334             return null;
    335 
    336         if (SUPPORTED_HASHES.size() <= 0)
    337             return null;
    338 
    339         EntityCapsManager entityCapsManager = instances.get(connection);
    340 
    341         if (entityCapsManager == null) {
    342             entityCapsManager = new EntityCapsManager(connection);
    343         }
    344 
    345         return entityCapsManager;
    346     }
    347 
    348     public void enableEntityCaps() {
    349         // Add Entity Capabilities (XEP-0115) feature node.
    350         sdm.addFeature(NAMESPACE);
    351         updateLocalEntityCaps();
    352         entityCapsEnabled = true;
    353     }
    354 
    355     public void disableEntityCaps() {
    356         entityCapsEnabled = false;
    357         sdm.removeFeature(NAMESPACE);
    358     }
    359 
    360     public boolean entityCapsEnabled() {
    361         return entityCapsEnabled;
    362     }
    363 
    364     /**
    365      * Remove a record telling what entity caps node a user has.
    366      *
    367      * @param user
    368      *            the user (Full JID)
    369      */
    370     public void removeUserCapsNode(String user) {
    371         jidCaps.remove(user);
    372     }
    373 
    374     /**
    375      * Get our own caps version. The version depends on the enabled features. A
    376      * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
    377      *
    378      * @return our own caps version
    379      */
    380     public String getCapsVersion() {
    381         return currentCapsVersion;
    382     }
    383 
    384     /**
    385      * Returns the local entity's NodeVer (e.g.
    386      * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
    387      * )
    388      *
    389      * @return
    390      */
    391     public String getLocalNodeVer() {
    392         return ENTITY_NODE + '#' + getCapsVersion();
    393     }
    394 
    395     /**
    396      * Returns true if Entity Caps are supported by a given JID
    397      *
    398      * @param jid
    399      * @return
    400      */
    401     public boolean areEntityCapsSupported(String jid) {
    402         if (jid == null)
    403             return false;
    404 
    405         try {
    406             DiscoverInfo result = sdm.discoverInfo(jid);
    407             return result.containsFeature(NAMESPACE);
    408         } catch (XMPPException e) {
    409             return false;
    410         }
    411     }
    412 
    413     /**
    414      * Returns true if Entity Caps are supported by the local service/server
    415      *
    416      * @return
    417      */
    418     public boolean areEntityCapsSupportedByServer() {
    419         return areEntityCapsSupported(weakRefConnection.get().getServiceName());
    420     }
    421 
    422     /**
    423      * Updates the local user Entity Caps information with the data provided
    424      *
    425      * If we are connected and there was already a presence send, another
    426      * presence is send to inform others about your new Entity Caps node string.
    427      *
    428      * @param discoverInfo
    429      *            the local users discover info (mostly the service discovery
    430      *            features)
    431      * @param identityType
    432      *            the local users identity type
    433      * @param identityName
    434      *            the local users identity name
    435      * @param extendedInfo
    436      *            the local users extended info
    437      */
    438     public void updateLocalEntityCaps() {
    439         Connection connection = weakRefConnection.get();
    440 
    441         DiscoverInfo discoverInfo = new DiscoverInfo();
    442         discoverInfo.setType(IQ.Type.RESULT);
    443         discoverInfo.setNode(getLocalNodeVer());
    444         if (connection != null)
    445             discoverInfo.setFrom(connection.getUser());
    446         sdm.addDiscoverInfoTo(discoverInfo);
    447 
    448         currentCapsVersion = generateVerificationString(discoverInfo, "sha-1");
    449         addDiscoverInfoByNode(ENTITY_NODE + '#' + currentCapsVersion, discoverInfo);
    450         if (lastLocalCapsVersions.size() > 10) {
    451             String oldCapsVersion = lastLocalCapsVersions.poll();
    452             sdm.removeNodeInformationProvider(ENTITY_NODE + '#' + oldCapsVersion);
    453         }
    454         lastLocalCapsVersions.add(currentCapsVersion);
    455 
    456         caps.put(currentCapsVersion, discoverInfo);
    457         if (connection != null)
    458             jidCaps.put(connection.getUser(), new NodeVerHash(ENTITY_NODE, currentCapsVersion, "sha-1"));
    459 
    460         sdm.setNodeInformationProvider(ENTITY_NODE + '#' + currentCapsVersion, new NodeInformationProvider() {
    461             List<String> features = sdm.getFeaturesList();
    462             List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getIdentities());
    463             List<PacketExtension> packetExtensions = sdm.getExtendedInfoAsList();
    464 
    465             @Override
    466             public List<Item> getNodeItems() {
    467                 return null;
    468             }
    469 
    470             @Override
    471             public List<String> getNodeFeatures() {
    472                 return features;
    473             }
    474 
    475             @Override
    476             public List<Identity> getNodeIdentities() {
    477                 return identities;
    478             }
    479 
    480             @Override
    481             public List<PacketExtension> getNodePacketExtensions() {
    482                 return packetExtensions;
    483             }
    484         });
    485 
    486         // Send an empty presence, and let the packet intercepter
    487         // add a <c/> node to it.
    488         // See http://xmpp.org/extensions/xep-0115.html#advertise
    489         // We only send a presence packet if there was already one send
    490         // to respect ConnectionConfiguration.isSendPresence()
    491         if (connection != null && connection.isAuthenticated() && presenceSend) {
    492             Presence presence = new Presence(Presence.Type.available);
    493             connection.sendPacket(presence);
    494         }
    495     }
    496 
    497     /**
    498      * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
    499      * Method
    500      *
    501      * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
    502      *      5.4 Processing Method</a>
    503      *
    504      * @param capsNode
    505      *            the caps node (i.e. node#ver)
    506      * @param info
    507      * @return true if it's valid and should be cache, false if not
    508      */
    509     public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) {
    510         // step 3.3 check for duplicate identities
    511         if (info.containsDuplicateIdentities())
    512             return false;
    513 
    514         // step 3.4 check for duplicate features
    515         if (info.containsDuplicateFeatures())
    516             return false;
    517 
    518         // step 3.5 check for well-formed packet extensions
    519         if (verifyPacketExtensions(info))
    520             return false;
    521 
    522         String calculatedVer = generateVerificationString(info, hash);
    523 
    524         if (!ver.equals(calculatedVer))
    525             return false;
    526 
    527         return true;
    528     }
    529 
    530     /**
    531      *
    532      * @param info
    533      * @return true if the packet extensions is ill-formed
    534      */
    535     protected static boolean verifyPacketExtensions(DiscoverInfo info) {
    536         List<FormField> foundFormTypes = new LinkedList<FormField>();
    537         for (Iterator<PacketExtension> i = info.getExtensions().iterator(); i.hasNext();) {
    538             PacketExtension pe = i.next();
    539             if (pe.getNamespace().equals(Form.NAMESPACE)) {
    540                 DataForm df = (DataForm) pe;
    541                 for (Iterator<FormField> it = df.getFields(); it.hasNext();) {
    542                     FormField f = it.next();
    543                     if (f.getVariable().equals("FORM_TYPE")) {
    544                         for (FormField fft : foundFormTypes) {
    545                             if (f.equals(fft))
    546                                 return true;
    547                         }
    548                         foundFormTypes.add(f);
    549                     }
    550                 }
    551             }
    552         }
    553         return false;
    554     }
    555 
    556     /**
    557      * Generates a XEP-115 Verification String
    558      *
    559      * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
    560      *      Verification String</a>
    561      *
    562      * @param discoverInfo
    563      * @param hash
    564      *            the used hash function
    565      * @return The generated verification String or null if the hash is not
    566      *         supported
    567      */
    568     protected static String generateVerificationString(DiscoverInfo discoverInfo, String hash) {
    569         MessageDigest md = SUPPORTED_HASHES.get(hash.toLowerCase());
    570         if (md == null)
    571             return null;
    572 
    573         DataForm extendedInfo = (DataForm) discoverInfo.getExtension(Form.ELEMENT, Form.NAMESPACE);
    574 
    575         // 1. Initialize an empty string S ('sb' in this method).
    576         StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
    577                                                 // need thread-safe StringBuffer
    578 
    579         // 2. Sort the service discovery identities by category and then by
    580         // type and then by xml:lang
    581         // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
    582         // [NAME]. Note that each slash is included even if the LANG or
    583         // NAME is not included (in accordance with XEP-0030, the category and
    584         // type MUST be included.
    585         SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
    586 
    587         for (Iterator<DiscoverInfo.Identity> it = discoverInfo.getIdentities(); it.hasNext();)
    588             sortedIdentities.add(it.next());
    589 
    590         // 3. For each identity, append the 'category/type/lang/name' to S,
    591         // followed by the '<' character.
    592         for (Iterator<DiscoverInfo.Identity> it = sortedIdentities.iterator(); it.hasNext();) {
    593             DiscoverInfo.Identity identity = it.next();
    594             sb.append(identity.getCategory());
    595             sb.append("/");
    596             sb.append(identity.getType());
    597             sb.append("/");
    598             sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
    599             sb.append("/");
    600             sb.append(identity.getName() == null ? "" : identity.getName());
    601             sb.append("<");
    602         }
    603 
    604         // 4. Sort the supported service discovery features.
    605         SortedSet<String> features = new TreeSet<String>();
    606         for (Iterator<Feature> it = discoverInfo.getFeatures(); it.hasNext();)
    607             features.add(it.next().getVar());
    608 
    609         // 5. For each feature, append the feature to S, followed by the '<'
    610         // character
    611         for (String f : features) {
    612             sb.append(f);
    613             sb.append("<");
    614         }
    615 
    616         // only use the data form for calculation is it has a hidden FORM_TYPE
    617         // field
    618         // see XEP-0115 5.4 step 3.6
    619         if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) {
    620             synchronized (extendedInfo) {
    621                 // 6. If the service discovery information response includes
    622                 // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
    623                 // by the XML character data of the <value/> element).
    624                 SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
    625                     public int compare(FormField f1, FormField f2) {
    626                         return f1.getVariable().compareTo(f2.getVariable());
    627                     }
    628                 });
    629 
    630                 FormField ft = null;
    631 
    632                 for (Iterator<FormField> i = extendedInfo.getFields(); i.hasNext();) {
    633                     FormField f = i.next();
    634                     if (!f.getVariable().equals("FORM_TYPE")) {
    635                         fs.add(f);
    636                     } else {
    637                         ft = f;
    638                     }
    639                 }
    640 
    641                 // Add FORM_TYPE values
    642                 if (ft != null) {
    643                     formFieldValuesToCaps(ft.getValues(), sb);
    644                 }
    645 
    646                 // 7. 3. For each field other than FORM_TYPE:
    647                 // 1. Append the value of the "var" attribute, followed by the
    648                 // '<' character.
    649                 // 2. Sort values by the XML character data of the <value/>
    650                 // element.
    651                 // 3. For each <value/> element, append the XML character data,
    652                 // followed by the '<' character.
    653                 for (FormField f : fs) {
    654                     sb.append(f.getVariable());
    655                     sb.append("<");
    656                     formFieldValuesToCaps(f.getValues(), sb);
    657                 }
    658             }
    659         }
    660         // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
    661         // 3269).
    662         // 9. Compute the verification string by hashing S using the algorithm
    663         // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
    664         // 3174).
    665         // The hashed data MUST be generated with binary output and
    666         // encoded using Base64 as specified in Section 4 of RFC 4648
    667         // (note: the Base64 output MUST NOT include whitespace and MUST set
    668         // padding bits to zero).
    669         byte[] digest = md.digest(sb.toString().getBytes());
    670         return Base64.encodeBytes(digest);
    671     }
    672 
    673     private static void formFieldValuesToCaps(Iterator<String> i, StringBuilder sb) {
    674         SortedSet<String> fvs = new TreeSet<String>();
    675         while (i.hasNext()) {
    676             fvs.add(i.next());
    677         }
    678         for (String fv : fvs) {
    679             sb.append(fv);
    680             sb.append("<");
    681         }
    682     }
    683 
    684     public static class NodeVerHash {
    685         private String node;
    686         private String hash;
    687         private String ver;
    688         private String nodeVer;
    689 
    690         NodeVerHash(String node, String ver, String hash) {
    691             this.node = node;
    692             this.ver = ver;
    693             this.hash = hash;
    694             nodeVer = node + "#" + ver;
    695         }
    696 
    697         public String getNodeVer() {
    698             return nodeVer;
    699         }
    700 
    701         public String getNode() {
    702             return node;
    703         }
    704 
    705         public String getHash() {
    706             return hash;
    707         }
    708 
    709         public String getVer() {
    710             return ver;
    711         }
    712     }
    713 }
    714