EntityCapabilitiesManager.java 17.9 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile: $
 * $Revision: $
 * $Date: $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * 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.
19 20 21 22
 */

package org.jivesoftware.openfire.entitycaps;

23 24
import java.util.ArrayList;
import java.util.Collections;
25
import java.util.HashMap;
26 27 28 29
import java.util.Iterator;
import java.util.List;
import java.util.Map;

30
import org.dom4j.Element;
31
import org.dom4j.QName;
32 33 34 35 36 37 38
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;
39
import org.xmpp.component.IQResultListener;
40 41 42 43 44 45 46 47 48 49 50 51
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.
 * 
52 53 54 55 56 57 58
 * 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.
 * 
59 60 61 62 63 64 65 66
 * @author Armando Jagucki
 *
 */
public class EntityCapabilitiesManager implements IQResultListener, UserEventListener {

    private static final EntityCapabilitiesManager instance = new EntityCapabilitiesManager();

    /**
67 68
     * Entity Capabilities cache map. This cache stores entity capabilities
     * that may be shared among users.
69 70 71 72 73 74 75 76 77 78 79 80
     * 
     * 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;

    /**
81 82
     * Entity Capabilities user cache map. This map is used to determine which
     * entity capabilities are in use for a particular user.
83 84 85 86 87 88 89 90 91 92 93
     * 
     * 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;

    /**
94 95 96 97 98
     * 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).
     * 
99 100 101 102 103 104 105 106 107 108
     * 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.
     */
109
    private Map<String, EntityCapabilities> verAttributes;
110 111 112 113

    private EntityCapabilitiesManager() {
        entityCapabilitiesMap = CacheFactory.createCache("Entity Capabilities");
        entityCapabilitiesUserMap = CacheFactory.createCache("Entity Capabilities Users");
114
        verAttributes = new HashMap<String, EntityCapabilities>();
115 116 117 118 119 120 121 122 123 124 125 126
    }

    /**
     * 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
127 128 129 130
        // Ignore unavailable presences
        if (Presence.Type.unavailable == packet.getType()) {
            return;
        }
131

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

139 140 141 142 143
        // 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
144 145
        final String hashAttribute = capsElement.attributeValue("hash");
        if (hashAttribute == null || hashAttribute.trim().length() == 0) {
146 147 148 149 150
            return;
        }
        
        // Examine the packet and check if it has and a 'ver' hash
        // if not -- do nothing by returning.
151 152
        final String newVerAttribute = capsElement.attributeValue("ver");
        if (newVerAttribute == null || newVerAttribute.trim().length() == 0) {
153 154 155 156 157
            return;
        }

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

169
            String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
170 171 172 173 174
            iq.setFrom(serverName);

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

            String packetId = iq.getID();
175 176 177 178 179
            
            final EntityCapabilities caps = new EntityCapabilities();
            caps.setHashAttribute(hashAttribute);
            caps.setVerAttribute(newVerAttribute);
            verAttributes.put(packetId, caps);
180

181
            final IQRouter iqRouter = XMPPServer.getInstance().getIQRouter();
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
            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) {
209 210 211 212 213
        final EntityCapabilities original = verAttributes.get(packet.getID());
        if (original == null) {
        	return false;
        }
        final String newVerHash = generateVerHash(packet, original.getHashAttribute());
214

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

    /**
     * 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
227
     * @param packet IQ reply to the entity cap request.
228
     * @param algorithm The hashing algorithm to use (e.g. SHA-1)
229 230
     * @return the generated 'ver' hash
     */
231
    public static String generateVerHash(IQ packet, String algorithm) {
232
        // Initialize an empty string S.
233
        final StringBuilder s = new StringBuilder();
234

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

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

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

251 252
        // For each feature, append the feature to S, followed by the '<'
        // character.
253
        for (String discoFeature : discoFeatures) {
254 255
            s.append(discoFeature);
            s.append('<');
256
        }
257 258 259 260 261 262 263 264 265 266 267 268
        
        // 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()
        }
        
269 270 271 272
        // 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).
273 274
        final String hashed = StringUtils.hash(s.toString(), "SHA-1");
        return StringUtils.encodeBase64(StringUtils.decodeHex(hashed));
275 276 277
    }

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

    public void receivedAnswer(IQ packet) {
        String packetId = packet.getID();

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

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

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

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

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

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

    /**
Gaston Dombiak's avatar
Gaston Dombiak committed
316 317
     * Returns the entity capabilities for a specific JID. The specified JID
     * should be a full JID that identitied the entity's connection.
318
     * 
Gaston Dombiak's avatar
Gaston Dombiak committed
319
     * @param jid the full JID of entity
320 321 322 323 324 325 326 327 328 329 330 331 332
     * @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
     */
333
    private static List<String> getIdentitiesFrom(IQ packet) {
334 335
        List<String> discoIdentities = new ArrayList<String>();
        Element query = packet.getChildElement();
336
        Iterator<Element> identitiesIterator = query.elementIterator("identity");
337 338
        if (identitiesIterator != null) {
            while (identitiesIterator.hasNext()) {
339 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
                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());
368 369 370 371 372 373 374 375 376 377 378
            }
        }
        return discoIdentities;
    }

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

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

394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    /**
	 * 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) {
		List<String> results = new ArrayList<String>();
		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");
				List<String> vars = new ArrayList<String>();
				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");
						List<String> values = new ArrayList<String>();
						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;
	}
    
451 452
    public void userDeleting(User user, Map<String, Object> params) {
        // Delete this user's association in entityCapabilitiesUserMap.
453
        JID jid = XMPPServer.getInstance().createJID(user.getUsername(), null, true);
Gaston Dombiak's avatar
Gaston Dombiak committed
454
        String verHashOfUser = entityCapabilitiesUserMap.remove(jid);
455 456 457 458

        // 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.
459 460
        for (String verHash : entityCapabilitiesUserMap.values()) {
            if (verHash.equals(verHashOfUser)) {
461 462 463
                // 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.
464 465
                return;
            }
466
        }
467 468 469 470 471 472 473 474 475 476 477
        entityCapabilitiesMap.remove(verHashOfUser);
    }

    public void userCreated(User user, Map<String, Object> params) {
        // Do nothing.
    }

    public void userModified(User user, Map<String, Object> params) {
        // Do nothing.
    }
}