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 -> 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