• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(Connection connection) { if (connection instanceof XMPPConnection) new EntityCapsManager(connection); } })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      */
addDiscoverInfoByNode(String nodeVer, DiscoverInfo info)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      */
getNodeVersionByJid(String jid)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 
getNodeVerHashByJid(String jid)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      */
getDiscoverInfoByUser(String user)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      */
getDiscoveryInfoByNodeVer(String nodeVer)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      */
setPersistentCache(EntityCapsPersistentCache cache)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")
setJidCapsMaxCacheSize(int maxCacheSize)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")
setCapsMaxCacheSize(int maxCacheSize)221     public static void setCapsMaxCacheSize(int maxCacheSize) {
222         ((Cache) caps).setMaxCacheSize(maxCacheSize);
223     }
224 
EntityCapsManager(Connection connection)225     private EntityCapsManager(Connection connection) {
226         this.weakRefConnection = new WeakReference<Connection>(connection);
227         this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
228         init();
229     }
230 
init()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 
getInstanceFor(Connection connection)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 
enableEntityCaps()348     public void enableEntityCaps() {
349         // Add Entity Capabilities (XEP-0115) feature node.
350         sdm.addFeature(NAMESPACE);
351         updateLocalEntityCaps();
352         entityCapsEnabled = true;
353     }
354 
disableEntityCaps()355     public void disableEntityCaps() {
356         entityCapsEnabled = false;
357         sdm.removeFeature(NAMESPACE);
358     }
359 
entityCapsEnabled()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      */
removeUserCapsNode(String user)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      */
getCapsVersion()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      */
getLocalNodeVer()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      */
areEntityCapsSupported(String jid)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      */
areEntityCapsSupportedByServer()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      */
updateLocalEntityCaps()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      */
verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info)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      */
verifyPacketExtensions(DiscoverInfo info)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      */
generateVerificationString(DiscoverInfo discoverInfo, String hash)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 
formFieldValuesToCaps(Iterator<String> i, StringBuilder sb)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 
NodeVerHash(String node, String ver, String hash)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 
getNodeVer()697         public String getNodeVer() {
698             return nodeVer;
699         }
700 
getNode()701         public String getNode() {
702             return node;
703         }
704 
getHash()705         public String getHash() {
706             return hash;
707         }
708 
getVer()709         public String getVer() {
710             return ver;
711         }
712     }
713 }
714