/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * 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. */ package org.jivesoftware.openfire.entitycaps; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.dom4j.Element; import org.dom4j.QName; 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; import org.xmpp.component.IQResultListener; 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. * * 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. * * @author Armando Jagucki * */ public class EntityCapabilitiesManager implements IQResultListener, UserEventListener { private static final EntityCapabilitiesManager instance = new EntityCapabilitiesManager(); /** * Entity Capabilities cache map. This cache stores entity capabilities * that may be shared among users. * * 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; /** * Entity Capabilities user cache map. This map is used to determine which * entity capabilities are in use for a particular user. * * 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; /** * 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). * * 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. */ private Map<String, EntityCapabilities> verAttributes; private EntityCapabilitiesManager() { entityCapabilitiesMap = CacheFactory.createLocalCache("Entity Capabilities"); entityCapabilitiesUserMap = CacheFactory.createLocalCache("Entity Capabilities Users"); verAttributes = new HashMap<>(); } /** * 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) { // Ignore unavailable presences if (Presence.Type.unavailable == packet.getType()) { return; } // Examine the packet and check if it has caps info, // if not -- do nothing by returning. Element capsElement = packet.getChildElement("c", "http://jabber.org/protocol/caps"); if (capsElement == null) { return; } // 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 final String hashAttribute = capsElement.attributeValue("hash"); if (hashAttribute == null || hashAttribute.trim().length() == 0) { return; } // Examine the packet and check if it has and a 'ver' hash // if not -- do nothing by returning. final String newVerAttribute = capsElement.attributeValue("ver"); if (newVerAttribute == null || newVerAttribute.trim().length() == 0) { return; } // Check to see if the 'ver' hash is already in our cache. if (isInCapsCache(newVerAttribute)) { // The 'ver' hash is in the cache already, so let's update the // entityCapabilitiesUserMap for the user that sent the caps // packet. entityCapabilitiesUserMap.put(packet.getFrom(), newVerAttribute); } else { // The 'ver' hash is not in the cache so send out a disco#info query // so that we may begin recognizing this 'ver' hash. IQ iq = new IQ(IQ.Type.get); iq.setTo(packet.getFrom()); String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain(); iq.setFrom(serverName); iq.setChildElement("query", "http://jabber.org/protocol/disco#info"); String packetId = iq.getID(); final EntityCapabilities caps = new EntityCapabilities(); caps.setHashAttribute(hashAttribute); caps.setVerAttribute(newVerAttribute); verAttributes.put(packetId, caps); final IQRouter iqRouter = XMPPServer.getInstance().getIQRouter(); 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) { if (packet.getType() != IQ.Type.result) return false; final EntityCapabilities original = verAttributes.get(packet.getID()); if (original == null) { return false; } final String newVerHash = generateVerHash(packet, original.getHashAttribute()); return newVerHash.equals(original.getVerAttribute()); } /** * 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. * * @param packet IQ reply to the entity cap request. * @param algorithm The hashing algorithm to use (e.g. SHA-1) * @return the generated 'ver' hash */ public static String generateVerHash(IQ packet, String algorithm) { // Initialize an empty string S. final StringBuilder s = new StringBuilder(); // Sort the service discovery identities by category and then by type // (if it exists), formatted as 'category' '/' 'type' / 'lang' / 'name' final List<String> discoIdentities = getIdentitiesFrom(packet); Collections.sort(discoIdentities); // For each identity, append the 'category/type/lang/name' to S, // followed by the '<' character. for (String discoIdentity : discoIdentities) { s.append(discoIdentity); s.append('<'); } // Sort the supported service discovery features. final List<String> discoFeatures = getFeaturesFrom(packet); Collections.sort(discoFeatures); // For each feature, append the feature to S, followed by the '<' // character. for (String discoFeature : discoFeatures) { s.append(discoFeature); s.append('<'); } // 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() } // 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). final String hashed = StringUtils.hash(s.toString(), "SHA-1"); return StringUtils.encodeBase64(StringUtils.decodeHex(hashed)); } @Override public void answerTimeout(String packetId) { // If we never received an answer, we can discard the cached // 'ver' attribute. verAttributes.remove(packetId); } @Override public void receivedAnswer(IQ packet) { String packetId = packet.getID(); if (isValid(packet)) { // The packet was validated, so it can be added to the Entity // Capabilities cache map. // Add the resolved identities and features to the entity // EntityCapabilitiesManager.capabilities object and add it // to the cache map... EntityCapabilities caps = verAttributes.get(packetId); // Store identities. List<String> identities = getIdentitiesFrom(packet); for (String identity : identities) { caps.addIdentity(identity); } // Store features. List<String> features = getFeaturesFrom(packet); for (String feature : features) { caps.addFeature(feature); } entityCapabilitiesMap.put(caps.getVerAttribute(), caps); entityCapabilitiesUserMap.put(packet.getFrom(), caps.getVerAttribute()); } // Remove cached 'ver' attribute. verAttributes.remove(packetId); } /** * Returns the entity capabilities for a specific JID. The specified JID * should be a full JID that identitied the entity's connection. * * @param jid the full JID of entity * @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 */ private static List<String> getIdentitiesFrom(IQ packet) { List<String> discoIdentities = new ArrayList<>(); Element query = packet.getChildElement(); Iterator<Element> identitiesIterator = query.elementIterator("identity"); if (identitiesIterator != null) { while (identitiesIterator.hasNext()) { 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()); } } return discoIdentities; } /** * Extracts a list of features from an IQ packet. * * @param packet the packet * @return a list of features */ private static List<String> getFeaturesFrom(IQ packet) { List<String> discoFeatures = new ArrayList<>(); Element query = packet.getChildElement(); Iterator<Element> featuresIterator = query.elementIterator("feature"); if (featuresIterator != null) { while (featuresIterator.hasNext()) { Element featureElement = featuresIterator.next(); String discoFeature = featureElement.attributeValue("var"); discoFeatures.add(discoFeature); } } return discoFeatures; } /** * 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<>(); 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<>(); 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<>(); 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; } @Override public void userDeleting(User user, Map<String, Object> params) { // Delete this user's association in entityCapabilitiesUserMap. JID jid = XMPPServer.getInstance().createJID(user.getUsername(), null, true); String verHashOfUser = entityCapabilitiesUserMap.remove(jid); // 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. for (String verHash : entityCapabilitiesUserMap.values()) { if (verHash.equals(verHashOfUser)) { // 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. return; } } entityCapabilitiesMap.remove(verHashOfUser); } @Override public void userCreated(User user, Map<String, Object> params) { // Do nothing. } @Override public void userModified(User user, Map<String, Object> params) { // Do nothing. } }