EntityCapabilitiesManager.java 17.9 KB
Newer Older
1
/**
2
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
3
 *
4 5 6 7 8 9 10 11 12 13 14
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
15 16 17 18
 */

package org.jivesoftware.openfire.entitycaps;

19 20
import java.util.ArrayList;
import java.util.Collections;
21
import java.util.HashMap;
22 23 24 25
import java.util.Iterator;
import java.util.List;
import java.util.Map;

26
import org.dom4j.Element;
27
import org.dom4j.QName;
28 29 30 31 32 33 34
import org.jivesoftware.openfire.IQRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
35
import org.xmpp.component.IQResultListener;
36 37 38 39 40 41 42 43 44 45 46 47
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;

/**
 * Implements server side mechanics for XEP-0115: "Entity Capabilities"
 * Version 1.4
 * 
 * In particular, EntityCapabilitiesManager is useful for processing
 * "filtered-notifications" for use with Pubsub (XEP-0060) for contacts that
 * may not want to receive notifications for all payload types.
 * 
48 49 50 51 52 53 54
 * The server's role in managing Entity Capabilities is to cache previously
 * encountered entity capabilities for XMPP clients supporting the same
 * identities and features. If the server has not seen a particular
 * combination of identities and features, a Discover Information query is
 * sent to that client and its reply is cached for future use by clients
 * sharing those same entity capabilities.
 * 
55 56 57 58 59 60 61 62
 * @author Armando Jagucki
 *
 */
public class EntityCapabilitiesManager implements IQResultListener, UserEventListener {

    private static final EntityCapabilitiesManager instance = new EntityCapabilitiesManager();

    /**
63 64
     * Entity Capabilities cache map. This cache stores entity capabilities
     * that may be shared among users.
65 66 67 68 69 70 71 72 73 74 75 76
     * 
     * When we want to look up the entity capabilities for a user, we first
     * find their most recently advertised 'ver' hash using the
     * {@link #entityCapabilitiesUserMap}. Then we use that 'ver' hash as a
     * key into this map.
     * 
     * Key:   The 'ver' hash string that encapsulates identities+features.
     * Value: EntityCapabilities object representing the encapsulated values.
     */
    private Cache<String, EntityCapabilities> entityCapabilitiesMap;

    /**
77 78
     * Entity Capabilities user cache map. This map is used to determine which
     * entity capabilities are in use for a particular user.
79 80 81 82 83 84 85 86 87 88 89
     * 
     * When we want to look up the entity capabilities for a user, we first
     * find their most recently advertised 'ver' hash using this map. Then we
     * use this 'ver' hash as a key into the {@link #entityCapabilitiesMap}.
     * 
     * Key:   The JID of the user.
     * Value: The 'ver' hash string that encapsulates identities+features.
     */
    private Cache<JID, String> entityCapabilitiesUserMap;

    /**
90 91 92 93 94
     * Ver attributes are the hash strings that correspond to a certain
     * combination of entity capabilities. This hash string, representing a
     * particular identities+features combination, is found in the 'ver'
     * attribute of the caps element in a presence packet (caps packet).
     * 
95 96 97 98 99 100 101 102 103 104
     * Each unrecognized caps packet that is encountered has its verAttribute
     * added to this map. Since results to our disco#info queries can be
     * received in any order, the map is used by {@link #isValid(IQ)} so the
     * method can be sure it is comparing its generated 'ver' hash to the
     * correct 'ver' hash in the map, that was previously encountered in the
     * caps packet.
     * 
     * Key:   Packet ID of our disco#info request.
     * Value: The 'ver' hash string from the original caps packet.
     */
105
    private Map<String, EntityCapabilities> verAttributes;
106 107

    private EntityCapabilitiesManager() {
108 109
        entityCapabilitiesMap = CacheFactory.createLocalCache("Entity Capabilities");
        entityCapabilitiesUserMap = CacheFactory.createLocalCache("Entity Capabilities Users");
110
        verAttributes = new HashMap<>();
111 112 113 114 115 116 117 118 119 120 121 122
    }

    /**
     * Returns the unique instance of this class.
     *
     * @return the unique instance of this class.
     */
    public static EntityCapabilitiesManager getInstance() {
        return instance;
    }

    public void process(Presence packet) {
Gaston Dombiak's avatar
Gaston Dombiak committed
123 124 125 126
        // Ignore unavailable presences
        if (Presence.Type.unavailable == packet.getType()) {
            return;
        }
127

128
        // Examine the packet and check if it has caps info,
129 130 131 132 133 134
        // if not -- do nothing by returning.
        Element capsElement = packet.getChildElement("c", "http://jabber.org/protocol/caps");
        if (capsElement == null) {
            return;
        }

135 136 137 138 139
        // Examine the packet and check if it's in legacy format (pre version 1.4
        // of XEP-0115). If so, do nothing by returning.
		// TODO: if this packet is in legacy format, we SHOULD check the 'node',
		// 'ver', and 'ext' combinations as specified in the archived version
		// 1.3 of the specification, and cache the results. See JM-1447
140 141
        final String hashAttribute = capsElement.attributeValue("hash");
        if (hashAttribute == null || hashAttribute.trim().length() == 0) {
142 143 144 145 146
            return;
        }
        
        // Examine the packet and check if it has and a 'ver' hash
        // if not -- do nothing by returning.
147 148
        final String newVerAttribute = capsElement.attributeValue("ver");
        if (newVerAttribute == null || newVerAttribute.trim().length() == 0) {
149 150 151 152 153
            return;
        }

        // Check to see if the 'ver' hash is already in our cache.
        if (isInCapsCache(newVerAttribute)) {
154 155 156
            // The 'ver' hash is in the cache already, so let's update the
            // entityCapabilitiesUserMap for the user that sent the caps
            // packet.
157 158 159
            entityCapabilitiesUserMap.put(packet.getFrom(), newVerAttribute);
        }
        else {
160 161
            // The 'ver' hash is not in the cache so send out a disco#info query
            // so that we may begin recognizing this 'ver' hash.
162 163 164
            IQ iq = new IQ(IQ.Type.get);
            iq.setTo(packet.getFrom());

165
            String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
166 167 168 169 170
            iq.setFrom(serverName);

            iq.setChildElement("query", "http://jabber.org/protocol/disco#info");

            String packetId = iq.getID();
171 172 173 174 175
            
            final EntityCapabilities caps = new EntityCapabilities();
            caps.setHashAttribute(hashAttribute);
            caps.setVerAttribute(newVerAttribute);
            verAttributes.put(packetId, caps);
176

177
            final IQRouter iqRouter = XMPPServer.getInstance().getIQRouter();
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
            iqRouter.addIQResultListener(packetId, this);
            iqRouter.route(iq);
        }
    }

    /**
     * Determines whether or not a particular 'ver' attribute is stored in the
     * {@link #entityCapabilitiesMap} cache.
     * 
     * @param verAttribute the 'ver' hash to check for.
     * @return true if the caps cache contains the 'ver' hash already, false if not.
     */
    private boolean isInCapsCache(String verAttribute) {
        return entityCapabilitiesMap.containsKey(verAttribute);
    }

    /**
     * Determines whether or not the packet received from a disco#info result
     * was valid by comparing its 'ver' hash (identites+features encapsulated
     * hash) with the 'ver' hash of the original caps packet that the
     * disco#info query was sent on behalf of.
     * 
     * @param packet the disco#info result packet.
     * @return true if the packet's generated 'ver' hash matches the 'ver'
     *         hash of the original caps packet.
     */
    private boolean isValid(IQ packet) {
205 206 207
        if (packet.getType() != IQ.Type.result)
            return false;

208 209 210 211 212
        final EntityCapabilities original = verAttributes.get(packet.getID());
        if (original == null) {
        	return false;
        }
        final String newVerHash = generateVerHash(packet, original.getHashAttribute());
213

214
        return newVerHash.equals(original.getVerAttribute());
215 216 217 218 219 220 221 222 223 224 225
    }

    /**
     * Generates a 'ver' hash attribute used in validation to help prevent
     * poisoning of entity capabilities information.
     * 
     * @see #isValid(IQ)
     * 
     * The value of the 'ver' attribute is generated according to the method
     * outlined in XEP-0115.
     * 
Gaston Dombiak's avatar
Gaston Dombiak committed
226
     * @param packet IQ reply to the entity cap request.
227
     * @param algorithm The hashing algorithm to use (e.g. SHA-1)
228 229
     * @return the generated 'ver' hash
     */
230
    public static String generateVerHash(IQ packet, String algorithm) {
231
        // Initialize an empty string S.
232
        final StringBuilder s = new StringBuilder();
233

234
        // Sort the service discovery identities by category and then by type
235 236
        // (if it exists), formatted as 'category' '/' 'type' / 'lang' / 'name'
        final List<String> discoIdentities = getIdentitiesFrom(packet);
237 238
        Collections.sort(discoIdentities);

239 240
        // For each identity, append the 'category/type/lang/name' to S, 
        // followed by the '<' character.
241
        for (String discoIdentity : discoIdentities) {
242 243
            s.append(discoIdentity);
            s.append('<');
244 245
        }

246 247
        // Sort the supported service discovery features.
        final List<String> discoFeatures = getFeaturesFrom(packet);
248 249
        Collections.sort(discoFeatures);

250 251
        // For each feature, append the feature to S, followed by the '<'
        // character.
252
        for (String discoFeature : discoFeatures) {
253 254
            s.append(discoFeature);
            s.append('<');
255
        }
256 257 258 259 260 261 262 263 264 265 266 267
        
        // If the service discovery information response includes XEP-0128 
        // data forms, sort the forms by the FORM_TYPE (i.e., by the XML
        // character data of the <value/> element).
        final List<String> extendedDataForms = getExtendedDataForms(packet);
        Collections.sort(extendedDataForms);
        
        for (String extendedDataForm : extendedDataForms) {
        	s.append(extendedDataForm);
        	// no need to add '<', this is done in #getExtendedDataForms()
        }
        
268 269 270 271
        // Compute ver by hashing S using the SHA-1 algorithm as specified in
        // RFC 3174 (with binary output) and encoding the hash using Base64 as
        // specified in Section 4 of RFC 4648 (note: the Base64 output
        // MUST NOT include whitespace and MUST set padding bits to zero).
272 273
        final String hashed = StringUtils.hash(s.toString(), "SHA-1");
        return StringUtils.encodeBase64(StringUtils.decodeHex(hashed));
274 275
    }

276
    @Override
277
    public void answerTimeout(String packetId) {
278 279
        // If we never received an answer, we can discard the cached
        // 'ver' attribute.
280 281 282
        verAttributes.remove(packetId);
    }

283
    @Override
284 285 286 287
    public void receivedAnswer(IQ packet) {
        String packetId = packet.getID();

        if (isValid(packet)) {
288 289
            // The packet was validated, so it can be added to the Entity
            // Capabilities cache map.
290

291 292 293 294
            // Add the resolved identities and features to the entity 
        	// EntityCapabilitiesManager.capabilities object and add it 
        	// to the cache map...
            EntityCapabilities caps = verAttributes.get(packetId);
295 296 297 298

            // Store identities.
            List<String> identities = getIdentitiesFrom(packet);
            for (String identity : identities) {
299
            	caps.addIdentity(identity);
300 301 302 303 304
            }

            // Store features.
            List<String> features = getFeaturesFrom(packet);
            for (String feature : features) {
305
            	caps.addFeature(feature);
306 307
            }

308 309
            entityCapabilitiesMap.put(caps.getVerAttribute(), caps);
            entityCapabilitiesUserMap.put(packet.getFrom(), caps.getVerAttribute());
310 311 312 313 314 315 316
        }

        // Remove cached 'ver' attribute.
        verAttributes.remove(packetId);
    }

    /**
Gaston Dombiak's avatar
Gaston Dombiak committed
317 318
     * Returns the entity capabilities for a specific JID. The specified JID
     * should be a full JID that identitied the entity's connection.
319
     * 
Gaston Dombiak's avatar
Gaston Dombiak committed
320
     * @param jid the full JID of entity
321 322 323 324 325 326 327 328 329 330 331 332 333
     * @return the entity capabilities of jid.
     */
    public EntityCapabilities getEntityCapabilities(JID jid) {
        String verAttribute = entityCapabilitiesUserMap.get(jid);
        return entityCapabilitiesMap.get(verAttribute);
    }

    /**
     * Extracts a list of identities from an IQ packet.
     * 
     * @param packet the packet
     * @return a list of identities
     */
334
    private static List<String> getIdentitiesFrom(IQ packet) {
335
        List<String> discoIdentities = new ArrayList<>();
336
        Element query = packet.getChildElement();
337
        Iterator<Element> identitiesIterator = query.elementIterator("identity");
338 339
        if (identitiesIterator != null) {
            while (identitiesIterator.hasNext()) {
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
                Element identityElement = identitiesIterator.next();

                StringBuilder discoIdentity = new StringBuilder();
                
                String cat = identityElement.attributeValue("category");
                String type = identityElement.attributeValue("type");
                String lang = identityElement.attributeValue("xml:lang");
                String name = identityElement.attributeValue("name");
                
                if (cat != null) {
                	discoIdentity.append(cat);
                }
                discoIdentity.append('/');

                if (type != null) {
                	discoIdentity.append(type);
                }
                discoIdentity.append('/');

                if (lang != null) {
                	discoIdentity.append(lang);
                }
                discoIdentity.append('/');

                if (name != null) {
                	discoIdentity.append(name);
                }

                discoIdentities.add(discoIdentity.toString());
369 370 371 372 373 374 375 376 377 378 379
            }
        }
        return discoIdentities;
    }

    /**
     * Extracts a list of features from an IQ packet.
     * 
     * @param packet the packet
     * @return a list of features
     */
380
    private static List<String> getFeaturesFrom(IQ packet) {
381
        List<String> discoFeatures = new ArrayList<>();
382
        Element query = packet.getChildElement();
383
        Iterator<Element> featuresIterator = query.elementIterator("feature");
384 385
        if (featuresIterator != null) {
            while (featuresIterator.hasNext()) {
386
                Element featureElement = featuresIterator.next();
387 388 389 390 391 392 393 394
                String discoFeature = featureElement.attributeValue("var");

                discoFeatures.add(discoFeature);
            }
        }
        return discoFeatures;
    }

395 396 397 398 399 400 401 402 403
    /**
	 * Extracts a list of extended service discovery information from an IQ
	 * packet.
	 * 
	 * @param packet
	 *            the packet
	 * @return a list of extended service discoverin information features.
	 */
	private static List<String> getExtendedDataForms(IQ packet) {
404
		List<String> results = new ArrayList<>();
405 406 407 408 409 410 411 412 413 414
		Element query = packet.getChildElement();
		Iterator<Element> extensionIterator = query.elementIterator(QName.get(
				"x", "jabber:x:data"));
		if (extensionIterator != null) {
			while (extensionIterator.hasNext()) {
				Element extensionElement = extensionIterator.next();
				final StringBuilder formType = new StringBuilder();

				Iterator<Element> fieldIterator = extensionElement
						.elementIterator("field");
415
				List<String> vars = new ArrayList<>();
416 417 418 419 420 421 422 423 424 425 426 427
				while (fieldIterator != null && fieldIterator.hasNext()) {
					final Element fieldElement = fieldIterator.next();
					if (fieldElement.attributeValue("var").equals("FORM_TYPE")) {
						formType
								.append(fieldElement.element("value").getText());
						formType.append('<');
					} else {
						final StringBuilder var = new StringBuilder();
						var.append(fieldElement.attributeValue("var"));
						var.append('<');
						Iterator<Element> valIter = fieldElement
								.elementIterator("value");
428
						List<String> values = new ArrayList<>();
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
						while (valIter != null && valIter.hasNext()) {
							Element value = valIter.next();
							values.add(value.getText());
						}
						Collections.sort(values);
						for (String v : values) {
							var.append(v);
							var.append('<');
						}
						vars.add(var.toString());
					}
				}
				Collections.sort(vars);
				for (String v : vars) {
					formType.append(v);
				}

				results.add(formType.toString());
			}
		}
		return results;
	}
    
452
    @Override
453 454
    public void userDeleting(User user, Map<String, Object> params) {
        // Delete this user's association in entityCapabilitiesUserMap.
455
        JID jid = XMPPServer.getInstance().createJID(user.getUsername(), null, true);
Gaston Dombiak's avatar
Gaston Dombiak committed
456
        String verHashOfUser = entityCapabilitiesUserMap.remove(jid);
457 458 459 460

        // If there are no other references to the deleted user's 'ver' hash,
        // it is safe to remove that 'ver' hash's associated entity
        // capabilities from the entityCapabilitiesMap cache.
461 462
        for (String verHash : entityCapabilitiesUserMap.values()) {
            if (verHash.equals(verHashOfUser)) {
463 464 465
                // A different user is making use of the deleted user's same
                // 'ver' hash, so let's not remove the associated entity
                // capabilities from the entityCapabilitiesMap.
466 467
                return;
            }
468
        }
469 470 471
        entityCapabilitiesMap.remove(verHashOfUser);
    }

472
    @Override
473 474 475 476
    public void userCreated(User user, Map<String, Object> params) {
        // Do nothing.
    }

477
    @Override
478 479 480 481
    public void userModified(User user, Map<String, Object> params) {
        // Do nothing.
    }
}