001/** 002 * 003 * Copyright © 2009 Jonas Ådahl, 2011-2014 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 org.jivesoftware.smack.AbstractConnectionListener; 020import org.jivesoftware.smack.SmackException.NoResponseException; 021import org.jivesoftware.smack.SmackException.NotConnectedException; 022import org.jivesoftware.smack.XMPPConnection; 023import org.jivesoftware.smack.ConnectionCreationListener; 024import org.jivesoftware.smack.Manager; 025import org.jivesoftware.smack.StanzaListener; 026import org.jivesoftware.smack.XMPPConnectionRegistry; 027import org.jivesoftware.smack.XMPPException.XMPPErrorException; 028import org.jivesoftware.smack.packet.IQ; 029import org.jivesoftware.smack.packet.Stanza; 030import org.jivesoftware.smack.packet.ExtensionElement; 031import org.jivesoftware.smack.packet.Presence; 032import org.jivesoftware.smack.filter.NotFilter; 033import org.jivesoftware.smack.filter.PresenceTypeFilter; 034import org.jivesoftware.smack.filter.StanzaFilter; 035import org.jivesoftware.smack.filter.AndFilter; 036import org.jivesoftware.smack.filter.StanzaTypeFilter; 037import org.jivesoftware.smack.filter.StanzaExtensionFilter; 038import org.jivesoftware.smack.util.StringUtils; 039import org.jivesoftware.smack.util.stringencoder.Base64; 040import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache; 041import org.jivesoftware.smackx.caps.packet.CapsExtension; 042import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider; 043import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 044import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 045import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Feature; 046import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity; 047import org.jivesoftware.smackx.xdata.FormField; 048import org.jivesoftware.smackx.xdata.packet.DataForm; 049import org.jxmpp.util.cache.LruCache; 050 051import java.util.Comparator; 052import java.util.HashMap; 053import java.util.LinkedList; 054import java.util.List; 055import java.util.Locale; 056import java.util.Map; 057import java.util.Queue; 058import java.util.SortedSet; 059import java.util.TreeSet; 060import java.util.WeakHashMap; 061import java.util.concurrent.ConcurrentLinkedQueue; 062import java.util.logging.Level; 063import java.util.logging.Logger; 064import java.security.MessageDigest; 065import java.security.NoSuchAlgorithmException; 066 067/** 068 * Keeps track of entity capabilities. 069 * 070 * @author Florian Schmaus 071 * @see <a href="http://www.xmpp.org/extensions/xep-0115.html">XEP-0115: Entity Capabilities</a> 072 */ 073public class EntityCapsManager extends Manager { 074 private static final Logger LOGGER = Logger.getLogger(EntityCapsManager.class.getName()); 075 076 public static final String NAMESPACE = CapsExtension.NAMESPACE; 077 public static final String ELEMENT = CapsExtension.ELEMENT; 078 079 private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>(); 080 081 /** 082 * The default hash. Currently 'sha-1'. 083 */ 084 private static final String DEFAULT_HASH = StringUtils.SHA1; 085 086 private static String DEFAULT_ENTITY_NODE = "http://www.igniterealtime.org/projects/smack"; 087 088 protected static EntityCapsPersistentCache persistentCache; 089 090 private static boolean autoEnableEntityCaps = true; 091 092 private static Map<XMPPConnection, EntityCapsManager> instances = new WeakHashMap<>(); 093 094 private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter( 095 ELEMENT, NAMESPACE)); 096 private static final StanzaFilter PRESENCES_WITHOUT_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new NotFilter(new StanzaExtensionFilter( 097 ELEMENT, NAMESPACE))); 098 099 /** 100 * Map of "node + '#' + hash" to DiscoverInfo data 101 */ 102 private static final LruCache<String, DiscoverInfo> CAPS_CACHE = new LruCache<String, DiscoverInfo>(1000); 103 104 /** 105 * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the 106 * key is formed as user@server/resource (resource is required) In case of 107 * link-local connection the key is formed as user@host (no resource) In 108 * case of a server or component the key is formed as domain 109 */ 110 private static final LruCache<String, NodeVerHash> JID_TO_NODEVER_CACHE = new LruCache<String, NodeVerHash>(10000); 111 112 static { 113 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 114 public void connectionCreated(XMPPConnection connection) { 115 getInstanceFor(connection); 116 } 117 }); 118 119 try { 120 MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH); 121 SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest); 122 } catch (NoSuchAlgorithmException e) { 123 // Ignore 124 } 125 } 126 127 /** 128 * Set the default entity node that will be used for new EntityCapsManagers 129 * 130 * @param entityNode 131 */ 132 public static void setDefaultEntityNode(String entityNode) { 133 DEFAULT_ENTITY_NODE = entityNode; 134 } 135 136 /** 137 * Add DiscoverInfo to the database. 138 * 139 * @param nodeVer 140 * The node and verification String (e.g. 141 * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). 142 * @param info 143 * DiscoverInfo for the specified node. 144 */ 145 public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) { 146 CAPS_CACHE.put(nodeVer, info); 147 148 if (persistentCache != null) 149 persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info); 150 } 151 152 /** 153 * Get the Node version (node#ver) of a JID. Returns a String or null if 154 * EntiyCapsManager does not have any information. 155 * 156 * @param jid 157 * the user (Full JID) 158 * @return the node version (node#ver) or null 159 */ 160 public static String getNodeVersionByJid(String jid) { 161 NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(jid); 162 if (nvh != null) { 163 return nvh.nodeVer; 164 } else { 165 return null; 166 } 167 } 168 169 public static NodeVerHash getNodeVerHashByJid(String jid) { 170 return JID_TO_NODEVER_CACHE.get(jid); 171 } 172 173 /** 174 * Get the discover info given a user name. The discover info is returned if 175 * the user has a node#ver associated with it and the node#ver has a 176 * discover info associated with it. 177 * 178 * @param user 179 * user name (Full JID) 180 * @return the discovered info 181 */ 182 public static DiscoverInfo getDiscoverInfoByUser(String user) { 183 NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(user); 184 if (nvh == null) 185 return null; 186 187 return getDiscoveryInfoByNodeVer(nvh.nodeVer); 188 } 189 190 /** 191 * Retrieve DiscoverInfo for a specific node. 192 * 193 * @param nodeVer 194 * The node name (e.g. 195 * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). 196 * @return The corresponding DiscoverInfo or null if none is known. 197 */ 198 public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) { 199 DiscoverInfo info = CAPS_CACHE.get(nodeVer); 200 201 // If it was not in CAPS_CACHE, try to retrieve the information from persistentCache 202 if (info == null && persistentCache != null) { 203 info = persistentCache.lookup(nodeVer); 204 // Promote the information to CAPS_CACHE if one was found 205 if (info != null) { 206 CAPS_CACHE.put(nodeVer, info); 207 } 208 } 209 210 // If we were able to retrieve information from one of the caches, copy it before returning 211 if (info != null) 212 info = new DiscoverInfo(info); 213 214 return info; 215 } 216 217 /** 218 * Set the persistent cache implementation 219 * 220 * @param cache 221 */ 222 public static void setPersistentCache(EntityCapsPersistentCache cache) { 223 persistentCache = cache; 224 } 225 226 /** 227 * Sets the maximum cache sizes 228 * 229 * @param maxJidToNodeVerSize 230 * @param maxCapsCacheSize 231 */ 232 public static void setMaxsCacheSizes(int maxJidToNodeVerSize, int maxCapsCacheSize) { 233 JID_TO_NODEVER_CACHE.setMaxCacheSize(maxJidToNodeVerSize); 234 CAPS_CACHE.setMaxCacheSize(maxCapsCacheSize); 235 } 236 237 /** 238 * Clears the memory cache. 239 */ 240 public static void clearMemoryCache() { 241 JID_TO_NODEVER_CACHE.clear(); 242 CAPS_CACHE.clear(); 243 } 244 245 private static void addCapsExtensionInfo(String from, CapsExtension capsExtension) { 246 String capsExtensionHash = capsExtension.getHash(); 247 String hashInUppercase = capsExtensionHash.toUpperCase(Locale.US); 248 // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1" 249 if (!SUPPORTED_HASHES.containsKey(hashInUppercase)) 250 return; 251 String hash = capsExtensionHash.toLowerCase(Locale.US); 252 253 String node = capsExtension.getNode(); 254 String ver = capsExtension.getVer(); 255 256 JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash)); 257 } 258 259 private final Queue<CapsVersionAndHash> lastLocalCapsVersions = new ConcurrentLinkedQueue<>(); 260 261 private final ServiceDiscoveryManager sdm; 262 263 private boolean entityCapsEnabled; 264 private CapsVersionAndHash currentCapsVersion; 265 private volatile Presence presenceSend; 266 267 /** 268 * The entity node String used by this EntityCapsManager instance. 269 */ 270 private String entityNode = DEFAULT_ENTITY_NODE; 271 272 private EntityCapsManager(XMPPConnection connection) { 273 super(connection); 274 this.sdm = ServiceDiscoveryManager.getInstanceFor(connection); 275 instances.put(connection, this); 276 277 connection.addConnectionListener(new AbstractConnectionListener() { 278 @Override 279 public void connected(XMPPConnection connection) { 280 // It's not clear when a server would report the caps stream 281 // feature, so we try to process it after we are connected and 282 // once after we are authenticated. 283 processCapsStreamFeatureIfAvailable(connection); 284 } 285 @Override 286 public void authenticated(XMPPConnection connection, boolean resumed) { 287 // It's not clear when a server would report the caps stream 288 // feature, so we try to process it after we are connected and 289 // once after we are authenticated. 290 processCapsStreamFeatureIfAvailable(connection); 291 292 // Reset presenceSend when the connection was not resumed 293 if (!resumed) { 294 presenceSend = null; 295 } 296 } 297 private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) { 298 CapsExtension capsExtension = connection.getFeature( 299 CapsExtension.ELEMENT, CapsExtension.NAMESPACE); 300 if (capsExtension == null) { 301 return; 302 } 303 String from = connection.getServiceName(); 304 addCapsExtensionInfo(from, capsExtension); 305 } 306 }); 307 308 // This calculates the local entity caps version 309 updateLocalEntityCaps(); 310 311 if (autoEnableEntityCaps) 312 enableEntityCaps(); 313 314 connection.addAsyncStanzaListener(new StanzaListener() { 315 // Listen for remote presence stanzas with the caps extension 316 // If we receive such a stanza, record the JID and nodeVer 317 @Override 318 public void processPacket(Stanza packet) { 319 if (!entityCapsEnabled()) 320 return; 321 322 CapsExtension capsExtension = CapsExtension.from(packet); 323 String from = packet.getFrom(); 324 addCapsExtensionInfo(from, capsExtension); 325 } 326 327 }, PRESENCES_WITH_CAPS); 328 329 connection.addAsyncStanzaListener(new StanzaListener() { 330 @Override 331 public void processPacket(Stanza packet) { 332 // always remove the JID from the map, even if entityCaps are 333 // disabled 334 String from = packet.getFrom(); 335 JID_TO_NODEVER_CACHE.remove(from); 336 } 337 }, PRESENCES_WITHOUT_CAPS); 338 339 connection.addPacketSendingListener(new StanzaListener() { 340 @Override 341 public void processPacket(Stanza packet) { 342 presenceSend = (Presence) packet; 343 } 344 }, PresenceTypeFilter.AVAILABLE); 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 StanzaListener packetInterceptor = new StanzaListener() { 350 public void processPacket(Stanza packet) { 351 if (!entityCapsEnabled) { 352 // Be sure to not send stanzas with the caps extension if it's not enabled 353 packet.removeExtension(CapsExtension.ELEMENT, CapsExtension.NAMESPACE); 354 return; 355 } 356 CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash(); 357 CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash); 358 packet.overrideExtension(caps); 359 } 360 }; 361 connection.addPacketInterceptor(packetInterceptor, PresenceTypeFilter.AVAILABLE); 362 // It's important to do this as last action. Since it changes the 363 // behavior of the SDM in some ways 364 sdm.setEntityCapsManager(this); 365 } 366 367 public static synchronized EntityCapsManager getInstanceFor(XMPPConnection connection) { 368 if (SUPPORTED_HASHES.size() <= 0) 369 throw new IllegalStateException("No supported hashes for EntityCapsManager"); 370 371 EntityCapsManager entityCapsManager = instances.get(connection); 372 373 if (entityCapsManager == null) { 374 entityCapsManager = new EntityCapsManager(connection); 375 } 376 377 return entityCapsManager; 378 } 379 380 public synchronized void enableEntityCaps() { 381 // Add Entity Capabilities (XEP-0115) feature node. 382 sdm.addFeature(NAMESPACE); 383 updateLocalEntityCaps(); 384 entityCapsEnabled = true; 385 } 386 387 public synchronized void disableEntityCaps() { 388 entityCapsEnabled = false; 389 sdm.removeFeature(NAMESPACE); 390 } 391 392 public boolean entityCapsEnabled() { 393 return entityCapsEnabled; 394 } 395 396 public void setEntityNode(String entityNode) throws NotConnectedException { 397 this.entityNode = entityNode; 398 updateLocalEntityCaps(); 399 } 400 401 /** 402 * Remove a record telling what entity caps node a user has. 403 * 404 * @param user 405 * the user (Full JID) 406 */ 407 public void removeUserCapsNode(String user) { 408 JID_TO_NODEVER_CACHE.remove(user); 409 } 410 411 /** 412 * Get our own caps version. The version depends on the enabled features. A 413 * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI=' 414 * 415 * @return our own caps version 416 */ 417 public CapsVersionAndHash getCapsVersionAndHash() { 418 return currentCapsVersion; 419 } 420 421 /** 422 * Returns the local entity's NodeVer (e.g. 423 * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI= 424 * ) 425 * 426 * @return the local NodeVer 427 */ 428 public String getLocalNodeVer() { 429 CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash(); 430 if (capsVersionAndHash == null) { 431 return null; 432 } 433 return entityNode + '#' + capsVersionAndHash.version; 434 } 435 436 /** 437 * Returns true if Entity Caps are supported by a given JID 438 * 439 * @param jid 440 * @return true if the entity supports Entity Capabilities. 441 * @throws XMPPErrorException 442 * @throws NoResponseException 443 * @throws NotConnectedException 444 */ 445 public boolean areEntityCapsSupported(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException { 446 return sdm.supportsFeature(jid, NAMESPACE); 447 } 448 449 /** 450 * Returns true if Entity Caps are supported by the local service/server 451 * 452 * @return true if the user's server supports Entity Capabilities. 453 * @throws XMPPErrorException 454 * @throws NoResponseException 455 * @throws NotConnectedException 456 */ 457 public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException { 458 return areEntityCapsSupported(connection().getServiceName()); 459 } 460 461 /** 462 * Updates the local user Entity Caps information with the data provided 463 * 464 * If we are connected and there was already a presence send, another 465 * presence is send to inform others about your new Entity Caps node string. 466 * 467 */ 468 public void updateLocalEntityCaps() { 469 XMPPConnection connection = connection(); 470 471 DiscoverInfo discoverInfo = new DiscoverInfo(); 472 discoverInfo.setType(IQ.Type.result); 473 sdm.addDiscoverInfoTo(discoverInfo); 474 475 // getLocalNodeVer() will return a result only after currentCapsVersion is set. Therefore 476 // set it first and then call getLocalNodeVer() 477 currentCapsVersion = generateVerificationString(discoverInfo); 478 final String localNodeVer = getLocalNodeVer(); 479 discoverInfo.setNode(localNodeVer); 480 addDiscoverInfoByNode(localNodeVer, discoverInfo); 481 if (lastLocalCapsVersions.size() > 10) { 482 CapsVersionAndHash oldCapsVersion = lastLocalCapsVersions.poll(); 483 sdm.removeNodeInformationProvider(entityNode + '#' + oldCapsVersion.version); 484 } 485 lastLocalCapsVersions.add(currentCapsVersion); 486 487 if (connection != null) 488 JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion)); 489 490 final List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities()); 491 sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() { 492 List<String> features = sdm.getFeatures(); 493 List<ExtensionElement> packetExtensions = sdm.getExtendedInfoAsList(); 494 @Override 495 public List<String> getNodeFeatures() { 496 return features; 497 } 498 @Override 499 public List<Identity> getNodeIdentities() { 500 return identities; 501 } 502 @Override 503 public List<ExtensionElement> getNodePacketExtensions() { 504 return packetExtensions; 505 } 506 }); 507 508 // Re-send the last sent presence, and let the stanza interceptor 509 // add a <c/> node to it. 510 // See http://xmpp.org/extensions/xep-0115.html#advertise 511 // We only send a presence packet if there was already one send 512 // to respect ConnectionConfiguration.isSendPresence() 513 if (connection != null && connection.isAuthenticated() && presenceSend != null) { 514 try { 515 connection.sendStanza(presenceSend.cloneWithNewId()); 516 } 517 catch (NotConnectedException e) { 518 LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e); 519 } 520 } 521 } 522 523 /** 524 * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing 525 * Method 526 * 527 * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115 528 * 5.4 Processing Method</a> 529 * 530 * @param ver 531 * @param hash 532 * @param info 533 * @return true if it's valid and should be cache, false if not 534 */ 535 public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) { 536 // step 3.3 check for duplicate identities 537 if (info.containsDuplicateIdentities()) 538 return false; 539 540 // step 3.4 check for duplicate features 541 if (info.containsDuplicateFeatures()) 542 return false; 543 544 // step 3.5 check for well-formed packet extensions 545 if (verifyPacketExtensions(info)) 546 return false; 547 548 String calculatedVer = generateVerificationString(info, hash).version; 549 550 if (!ver.equals(calculatedVer)) 551 return false; 552 553 return true; 554 } 555 556 /** 557 * 558 * @param info 559 * @return true if the stanza(/packet) extensions is ill-formed 560 */ 561 protected static boolean verifyPacketExtensions(DiscoverInfo info) { 562 List<FormField> foundFormTypes = new LinkedList<FormField>(); 563 for (ExtensionElement pe : info.getExtensions()) { 564 if (pe.getNamespace().equals(DataForm.NAMESPACE)) { 565 DataForm df = (DataForm) pe; 566 for (FormField f : df.getFields()) { 567 if (f.getVariable().equals("FORM_TYPE")) { 568 for (FormField fft : foundFormTypes) { 569 if (f.equals(fft)) 570 return true; 571 } 572 foundFormTypes.add(f); 573 } 574 } 575 } 576 } 577 return false; 578 } 579 580 protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo) { 581 return generateVerificationString(discoverInfo, null); 582 } 583 584 /** 585 * Generates a XEP-115 Verification String 586 * 587 * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115 588 * Verification String</a> 589 * 590 * @param discoverInfo 591 * @param hash 592 * the used hash function, if null, default hash will be used 593 * @return The generated verification String or null if the hash is not 594 * supported 595 */ 596 protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo, String hash) { 597 if (hash == null) { 598 hash = DEFAULT_HASH; 599 } 600 // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1" 601 MessageDigest md = SUPPORTED_HASHES.get(hash.toUpperCase(Locale.US)); 602 if (md == null) 603 return null; 604 // Then transform the hash to lowercase, as this value will be put on the wire within the caps element's hash 605 // attribute. I'm not sure if the standard is case insensitive here, but let's assume that even it is, there could 606 // be "broken" implementation in the wild, so we *always* transform to lowercase. 607 hash = hash.toLowerCase(Locale.US); 608 609 DataForm extendedInfo = DataForm.from(discoverInfo); 610 611 // 1. Initialize an empty string S ('sb' in this method). 612 StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't 613 // need thread-safe StringBuffer 614 615 // 2. Sort the service discovery identities by category and then by 616 // type and then by xml:lang 617 // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/' 618 // [NAME]. Note that each slash is included even if the LANG or 619 // NAME is not included (in accordance with XEP-0030, the category and 620 // type MUST be included. 621 SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>(); 622 623 for (DiscoverInfo.Identity i : discoverInfo.getIdentities()) 624 sortedIdentities.add(i); 625 626 // 3. For each identity, append the 'category/type/lang/name' to S, 627 // followed by the '<' character. 628 for (DiscoverInfo.Identity identity : sortedIdentities) { 629 sb.append(identity.getCategory()); 630 sb.append("/"); 631 sb.append(identity.getType()); 632 sb.append("/"); 633 sb.append(identity.getLanguage() == null ? "" : identity.getLanguage()); 634 sb.append("/"); 635 sb.append(identity.getName() == null ? "" : identity.getName()); 636 sb.append("<"); 637 } 638 639 // 4. Sort the supported service discovery features. 640 SortedSet<String> features = new TreeSet<String>(); 641 for (Feature f : discoverInfo.getFeatures()) 642 features.add(f.getVar()); 643 644 // 5. For each feature, append the feature to S, followed by the '<' 645 // character 646 for (String f : features) { 647 sb.append(f); 648 sb.append("<"); 649 } 650 651 // only use the data form for calculation is it has a hidden FORM_TYPE 652 // field 653 // see XEP-0115 5.4 step 3.6 654 if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) { 655 synchronized (extendedInfo) { 656 // 6. If the service discovery information response includes 657 // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., 658 // by the XML character data of the <value/> element). 659 SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() { 660 public int compare(FormField f1, FormField f2) { 661 return f1.getVariable().compareTo(f2.getVariable()); 662 } 663 }); 664 665 FormField ft = null; 666 667 for (FormField f : extendedInfo.getFields()) { 668 if (!f.getVariable().equals("FORM_TYPE")) { 669 fs.add(f); 670 } else { 671 ft = f; 672 } 673 } 674 675 // Add FORM_TYPE values 676 if (ft != null) { 677 formFieldValuesToCaps(ft.getValues(), sb); 678 } 679 680 // 7. 3. For each field other than FORM_TYPE: 681 // 1. Append the value of the "var" attribute, followed by the 682 // '<' character. 683 // 2. Sort values by the XML character data of the <value/> 684 // element. 685 // 3. For each <value/> element, append the XML character data, 686 // followed by the '<' character. 687 for (FormField f : fs) { 688 sb.append(f.getVariable()); 689 sb.append("<"); 690 formFieldValuesToCaps(f.getValues(), sb); 691 } 692 } 693 } 694 // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC 695 // 3269). 696 // 9. Compute the verification string by hashing S using the algorithm 697 // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC 698 // 3174). 699 // The hashed data MUST be generated with binary output and 700 // encoded using Base64 as specified in Section 4 of RFC 4648 701 // (note: the Base64 output MUST NOT include whitespace and MUST set 702 // padding bits to zero). 703 byte[] digest; 704 synchronized(md) { 705 digest = md.digest(sb.toString().getBytes()); 706 } 707 String version = Base64.encodeToString(digest); 708 return new CapsVersionAndHash(version, hash); 709 } 710 711 private static void formFieldValuesToCaps(List<String> i, StringBuilder sb) { 712 SortedSet<String> fvs = new TreeSet<String>(); 713 for (String s : i) { 714 fvs.add(s); 715 } 716 for (String fv : fvs) { 717 sb.append(fv); 718 sb.append("<"); 719 } 720 } 721 722 public static class NodeVerHash { 723 private String node; 724 private String hash; 725 private String ver; 726 private String nodeVer; 727 728 NodeVerHash(String node, CapsVersionAndHash capsVersionAndHash) { 729 this(node, capsVersionAndHash.version, capsVersionAndHash.hash); 730 } 731 732 NodeVerHash(String node, String ver, String hash) { 733 this.node = node; 734 this.ver = ver; 735 this.hash = hash; 736 nodeVer = node + "#" + ver; 737 } 738 739 public String getNodeVer() { 740 return nodeVer; 741 } 742 743 public String getNode() { 744 return node; 745 } 746 747 public String getHash() { 748 return hash; 749 } 750 751 public String getVer() { 752 return ver; 753 } 754 } 755}