Commit 870f8b53 authored by Armando Jagucki's avatar Armando Jagucki Committed by ajagucki

Implementation of Entity Capabilities (XEP-0115) including server side...

Implementation of Entity Capabilities (XEP-0115) including server side cacheing and utilization by PEP. JM-1132

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@9434 b35dd754-fafc-0310-a699-88a17e54d16e
parent 1d760538
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
package org.jivesoftware.openfire; package org.jivesoftware.openfire;
import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.entitycaps.EntityCapabilitiesManager;
import org.jivesoftware.openfire.handler.PresenceSubscribeHandler; import org.jivesoftware.openfire.handler.PresenceSubscribeHandler;
import org.jivesoftware.openfire.handler.PresenceUpdateHandler; import org.jivesoftware.openfire.handler.PresenceUpdateHandler;
import org.jivesoftware.openfire.interceptor.InterceptorManager; import org.jivesoftware.openfire.interceptor.InterceptorManager;
...@@ -39,6 +40,7 @@ public class PresenceRouter extends BasicModule { ...@@ -39,6 +40,7 @@ public class PresenceRouter extends BasicModule {
private PresenceSubscribeHandler subscribeHandler; private PresenceSubscribeHandler subscribeHandler;
private PresenceManager presenceManager; private PresenceManager presenceManager;
private SessionManager sessionManager; private SessionManager sessionManager;
private EntityCapabilitiesManager entityCapsManager;
private MulticastRouter multicastRouter; private MulticastRouter multicastRouter;
private String serverName; private String serverName;
...@@ -127,12 +129,14 @@ public class PresenceRouter extends BasicModule { ...@@ -127,12 +129,14 @@ public class PresenceRouter extends BasicModule {
"".equals(recipientJID.getDomain()) || (recipientJID.getNode() == null && "".equals(recipientJID.getDomain()) || (recipientJID.getNode() == null &&
recipientJID.getResource() == null) && recipientJID.getResource() == null) &&
serverName.equals(recipientJID.getDomain())) { serverName.equals(recipientJID.getDomain())) {
entityCapsManager.process(packet);
updateHandler.process(packet); updateHandler.process(packet);
} }
else { else {
// Trigger events for presences of remote users // Trigger events for presences of remote users
if (senderJID != null && !serverName.equals(senderJID.getDomain()) && if (senderJID != null && !serverName.equals(senderJID.getDomain()) &&
!routingTable.hasComponentRoute(senderJID)) { !routingTable.hasComponentRoute(senderJID)) {
entityCapsManager.process(packet);
if (type == null) { if (type == null) {
// Remote user has become available // Remote user has become available
RemotePresenceEventDispatcher.remoteUserAvailable(packet); RemotePresenceEventDispatcher.remoteUserAvailable(packet);
...@@ -203,6 +207,7 @@ public class PresenceRouter extends BasicModule { ...@@ -203,6 +207,7 @@ public class PresenceRouter extends BasicModule {
presenceManager = server.getPresenceManager(); presenceManager = server.getPresenceManager();
multicastRouter = server.getMulticastRouter(); multicastRouter = server.getMulticastRouter();
sessionManager = server.getSessionManager(); sessionManager = server.getSessionManager();
entityCapsManager = EntityCapabilitiesManager.getInstance();
} }
/** /**
......
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.openfire.entitycaps;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashSet;
import java.util.Set;
import org.jivesoftware.util.cache.CacheSizes;
import org.jivesoftware.util.cache.Cacheable;
import org.jivesoftware.util.cache.ExternalizableUtil;
/**
* Contains identities and supported features describing client capabilities
* for an entity.
*
* @author Armando Jagucki
*
*/
public class EntityCapabilities implements Cacheable, Externalizable {
/**
* Identities included in these entity capabilities.
*/
private Set<String> identities = new HashSet<String>();
/**
* Features included in these entity capabilities.
*/
private Set<String> features = new HashSet<String>();
/**
* Hash string that corresponds to the entity capabilities. To be
* regenerated and used for discovering potential poisoning of entity
* capabilities information.
*/
private String verAttribute;
/**
* Adds an identity to the entity capabilities.
*
* @param identity the identity
* @return true if the entity capabilities did not already include the
* identity
*/
public boolean addIdentity(String identity) {
return identities.add(identity);
}
/**
* Adds a feature to the entity capabilities.
*
* @param feature the feature
* @return true if the entity capabilities did not already include the
* feature
*/
public boolean addFeature(String feature) {
return features.add(feature);
}
/**
* Determines whether or not a given identity is included in these entity
* capabilities.
*
* @param identity the identity
* @return true if identity is included, false if not
*/
public boolean containsIdentity(String identity) {
return identities.contains(identity);
}
/**
* Determines whether or not a given feature is included in these entity
* capabilities.
*
* @param feature the feature
* @return true if feature is included, false if not
*/
public boolean containsFeature(String feature) {
return features.contains(feature);
}
/**
* @return the verAttribute
*/
public String getVerAttribute() {
return verAttribute;
}
/**
* @param verAttribute the verAttribute to set
*/
public void setVerAttribute(String verAttribute) {
this.verAttribute = verAttribute;
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
ExternalizableUtil.getInstance().readStrings(in, identities);
ExternalizableUtil.getInstance().readStrings(in, features);
verAttribute = ExternalizableUtil.getInstance().readSafeUTF(in);
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeStrings(out, identities);
ExternalizableUtil.getInstance().writeStrings(out, features);
ExternalizableUtil.getInstance().writeSafeUTF(out, verAttribute);
}
public int getCachedSize() {
int size = CacheSizes.sizeOfCollection(identities);
size += CacheSizes.sizeOfCollection(features);
size += CacheSizes.sizeOfString(verAttribute);
return size;
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.openfire.entitycaps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.dom4j.Element;
import org.jivesoftware.openfire.IQResultListener;
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.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.
*
* @author Armando Jagucki
*
*/
public class EntityCapabilitiesManager implements IQResultListener, UserEventListener {
private static final EntityCapabilitiesManager instance = new EntityCapabilitiesManager();
/**
* Entity Capabilities cache map.
*
* 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.
*
* 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;
/**
* 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 Cache<String, String> verAttributes;
private EntityCapabilitiesManager() {
entityCapabilitiesMap = CacheFactory.createCache("Entity Capabilities");
entityCapabilitiesUserMap = CacheFactory.createCache("Entity Capabilities Users");
verAttributes = CacheFactory.createCache("Entity Capabilities ver Attributes");
}
/**
* 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) {
// Examine the packet and check if it has caps info and a 'ver' hash,
// if not -- do nothing by returning.
Element capsElement = packet.getChildElement("c", "http://jabber.org/protocol/caps");
if (capsElement == null) {
return;
}
String newVerAttribute = capsElement.attributeValue("ver");
if (newVerAttribute == null) {
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().getName();
iq.setFrom(serverName);
iq.setChildElement("query", "http://jabber.org/protocol/disco#info");
String packetId = iq.getID();
verAttributes.put(packetId, newVerAttribute);
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) {
String newVerHash = generateVerHash(packet);
String originalVerAttribute = verAttributes.get(packet.getID());
if (originalVerAttribute.equals(newVerHash)) {
return true;
}
else {
return false;
}
}
/**
* 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
* @return the generated 'ver' hash
*/
private String generateVerHash(IQ packet) {
// Initialize an empty string S.
String S = "";
/*
* Sort the service discovery identities by category and then by type
* (if it exists), formatted as 'category' '/' 'type'.
*/
List<String> discoIdentities = getIdentitiesFrom(packet);
Collections.sort(discoIdentities);
/*
* For each identity, append the 'category/type' to S, followed by the
* '<' character.
*/
for (String discoIdentity : discoIdentities) {
S += discoIdentity;
S += '<';
}
// Sort the supported features.
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 += discoFeature;
S += '<';
}
/*
* 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).
*/
S = StringUtils.hash(S, "SHA-1");
S = StringUtils.encodeBase64(StringUtils.decodeHex(S));
return S;
}
public void answerTimeout(String packetId) {
/*
* If we never received an answer, we can discard the cached
* 'ver' attribute.
*/
verAttributes.remove(packetId);
}
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.
*/
// Create the entity capabilities object and add it to the cache map...
EntityCapabilities entityCapabilities = new EntityCapabilities();
// Store identities.
List<String> identities = getIdentitiesFrom(packet);
for (String identity : identities) {
entityCapabilities.addIdentity(identity);
}
// Store features.
List<String> features = getFeaturesFrom(packet);
for (String feature : features) {
entityCapabilities.addFeature(feature);
}
String originalVerAttribute = verAttributes.get(packetId);
entityCapabilities.setVerAttribute(originalVerAttribute);
entityCapabilitiesMap.put(originalVerAttribute, entityCapabilities);
entityCapabilitiesUserMap.put(packet.getFrom(), originalVerAttribute);
}
// Remove cached 'ver' attribute.
verAttributes.remove(packetId);
}
/**
* Returns the entity capabilities for a specific JID.
*
* @param jid the 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 List<String> getIdentitiesFrom(IQ packet) {
List<String> discoIdentities = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator identitiesIterator = query.elementIterator("identity");
if (identitiesIterator != null) {
while (identitiesIterator.hasNext()) {
Element identityElement = (Element) identitiesIterator.next();
String discoIdentity = identityElement.attributeValue("category");
discoIdentity += '/';
discoIdentity += identityElement.attributeValue("type");
discoIdentities.add(discoIdentity);
}
}
return discoIdentities;
}
/**
* Extracts a list of features from an IQ packet.
*
* @param packet the packet
* @return a list of features
*/
private List<String> getFeaturesFrom(IQ packet) {
List<String> discoFeatures = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator featuresIterator = query.elementIterator("feature");
if (featuresIterator != null) {
while (featuresIterator.hasNext()) {
Element featureElement = (Element) featuresIterator.next();
String discoFeature = featureElement.attributeValue("var");
discoFeatures.add(discoFeature);
}
}
return discoFeatures;
}
public void userDeleting(User user, Map<String, Object> params) {
// Delete this user's association in entityCapabilitiesUserMap.
JID jid = XMPPServer.getInstance().createJID(user.getUsername(), null);
String verHashOfUser = entityCapabilitiesUserMap.get(jid);
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);
}
public void userCreated(User user, Map<String, Object> params) {
// Do nothing.
}
public void userModified(User user, Map<String, Object> params) {
// Do nothing.
}
}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
</head>
<body>
<p>Implementation of Entity Capabilities (XEP-0115).</p>
</body>
</html>
...@@ -50,19 +50,20 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -50,19 +50,20 @@ import java.util.concurrent.ConcurrentHashMap;
/** /**
* <p> * <p>
* An IQHandler used to implement XEP-0163: "Personal Eventing via Pubsub." * An {@link IQHandler} used to implement XEP-0163: "Personal Eventing via Pubsub"
* Version 1.0
* </p> * </p>
* *
* <p> * <p>
* For each user on the server there is an associated PEPService interacting * For each user on the server there is an associated {@link PEPService} interacting
* with a single PubSubEngine for managing the user's PEP nodes. * with a single {@link PubSubEngine} for managing the user's PEP nodes.
* </p> * </p>
* *
* <p> * <p>
* An IQHandler can only handle one namespace in its IQHandlerInfo. However, PEP * An IQHandler can only handle one namespace in its IQHandlerInfo. However, PEP
* related packets are seen having a variety of different namespaces. Thus, * related packets are seen having a variety of different namespaces. Thus,
* classes like IQPEPOwnerHandler are used to forward packets having these other * classes like {@link IQPEPOwnerHandler} are used to forward packets having these other
* namespaces to IQPEPHandler.handleIQ(). * namespaces to {@link IQPEPHandler#handleIQ(IQ)}.
* <p> * <p>
* *
* <p> * <p>
...@@ -85,15 +86,6 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -85,15 +86,6 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
*/ */
private Map<String, PEPService> pepServices; private Map<String, PEPService> pepServices;
/**
* Nodes to send filtered notifications for, table: key JID (String); value Set of nodes
*
* filteredNodesMap are used for Contact Notification Filtering as described in XEP-0163. The JID
* of a user is associated with a set of PEP node IDs they are interested in receiving notifications
* for.
*/
private Map<String, Set<String>> filteredNodesMap = new ConcurrentHashMap<String, Set<String>>();
private IQHandlerInfo info; private IQHandlerInfo info;
private PubSubEngine pubSubEngine = null; private PubSubEngine pubSubEngine = null;
...@@ -125,19 +117,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -125,19 +117,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
// Listen to roster events for PEP subscription cancelling on contact deletion. // Listen to roster events for PEP subscription cancelling on contact deletion.
RosterEventDispatcher.addListener(this); RosterEventDispatcher.addListener(this);
// Listen to user events in order to destroy a PEP service when a user is deleted. // Listen to user events in order to destroy a PEP service when a user is deleted.
UserEventDispatcher.addListener(this); UserEventDispatcher.addListener(this);
pubSubEngine = new PubSubEngine(server.getPacketRouter()); pubSubEngine = new PubSubEngine(server.getPacketRouter());
// TODO: This will need to be refactored once XEP-0115 (Entity Capabilities) is implemented.
/*
// Add this PEP handler as a packet interceptor so we may deal with
// client packets that send disco#info's explaining capabilities
// including PEP contact notification filters.
InterceptorManager.getInstance().addInterceptor(this);
*/
} }
/** /**
...@@ -147,10 +131,9 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -147,10 +131,9 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
* @return the loaded PEP service, or null if not found. * @return the loaded PEP service, or null if not found.
*/ */
private PEPService loadPEPServiceFromDB(String jid) { private PEPService loadPEPServiceFromDB(String jid) {
String GET_PEP_SERVICE = "SELECT DISTINCT serviceID FROM pubsubNode " + String GET_PEP_SERVICE = "SELECT DISTINCT serviceID FROM pubsubNode " + "WHERE serviceID='" + jid + "'";
"WHERE serviceID='" + jid + "'";
PEPService pepService = null; PEPService pepService = null;
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
try { try {
...@@ -166,7 +149,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -166,7 +149,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
pepService = new PEPService(XMPPServer.getInstance(), serviceID); pepService = new PEPService(XMPPServer.getInstance(), serviceID);
pepServices.put(serviceID, pepService); pepServices.put(serviceID, pepService);
pubSubEngine.start(pepService); pubSubEngine.start(pepService);
if (Log.isDebugEnabled()) { if (Log.isDebugEnabled()) {
Log.debug("PEP: Restored service for " + serviceID + " from the database."); Log.debug("PEP: Restored service for " + serviceID + " from the database.");
} }
...@@ -193,7 +176,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -193,7 +176,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
Log.error(e); Log.error(e);
} }
} }
return pepService; return pepService;
} }
...@@ -218,15 +201,6 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -218,15 +201,6 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
return info; return info;
} }
/**
* Returns the filteredNodesMap.
*
* @return the filteredNodesMap
*/
public Map<String, Set<String>> getFilteredNodesMap() {
return filteredNodesMap;
}
/** /**
* Returns the knownRemotePresences map. * Returns the knownRemotePresences map.
* *
...@@ -242,30 +216,29 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -242,30 +216,29 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
if (packet.getTo() == null) { if (packet.getTo() == null) {
if (packet.getType() == IQ.Type.set) { if (packet.getType() == IQ.Type.set) {
String jidFrom = senderJID.toBareJID(); String jidFrom = senderJID.toBareJID();
PEPService pepService = getPEPService(jidFrom); PEPService pepService = getPEPService(jidFrom);
// If no service exists yet for jidFrom, create one. // If no service exists yet for jidFrom, create one.
if (pepService == null) { if (pepService == null) {
// Return an error if the packet is from an anonymous, unregistered user // Return an error if the packet is from an anonymous, unregistered user
// or remote user // or remote user
if (!XMPPServer.getInstance().isLocal(senderJID) || if (!XMPPServer.getInstance().isLocal(senderJID) || !UserManager.getInstance().isRegisteredUser(senderJID.getNode())) {
!UserManager.getInstance().isRegisteredUser(senderJID.getNode())) {
IQ reply = IQ.createResultIQ(packet); IQ reply = IQ.createResultIQ(packet);
reply.setChildElement(packet.getChildElement().createCopy()); reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.not_allowed); reply.setError(PacketError.Condition.not_allowed);
return reply; return reply;
} }
pepService = new PEPService(XMPPServer.getInstance(), jidFrom); pepService = new PEPService(XMPPServer.getInstance(), jidFrom);
pepServices.put(jidFrom, pepService); pepServices.put(jidFrom, pepService);
// Probe presences // Probe presences
pubSubEngine.start(pepService); pubSubEngine.start(pepService);
if (Log.isDebugEnabled()) { if (Log.isDebugEnabled()) {
Log.debug("PEP: " + jidFrom + " had a PEPService created"); Log.debug("PEP: " + jidFrom + " had a PEPService created");
} }
// Those who already have presence subscriptions to jidFrom // Those who already have presence subscriptions to jidFrom
// will now automatically be subscribed to this new PEPService. // will now automatically be subscribed to this new PEPService.
try { try {
...@@ -280,13 +253,13 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -280,13 +253,13 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
// Do nothing // Do nothing
} }
} }
// If publishing a node, and the node doesn't exist, create it. // If publishing a node, and the node doesn't exist, create it.
Element childElement = packet.getChildElement(); Element childElement = packet.getChildElement();
Element publishElement = childElement.element("publish"); Element publishElement = childElement.element("publish");
if (publishElement != null) { if (publishElement != null) {
String nodeID = publishElement.attributeValue("node"); String nodeID = publishElement.attributeValue("node");
// Do not allow User Avatar nodes to be created. // Do not allow User Avatar nodes to be created.
// TODO: Implement XEP-0084 // TODO: Implement XEP-0084
if (nodeID.startsWith("http://www.xmpp.org/extensions/xep-0084.html")) { if (nodeID.startsWith("http://www.xmpp.org/extensions/xep-0084.html")) {
...@@ -295,7 +268,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -295,7 +268,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
reply.setError(PacketError.Condition.feature_not_implemented); reply.setError(PacketError.Condition.feature_not_implemented);
return reply; return reply;
} }
if (pepService.getNode(nodeID) == null) { if (pepService.getNode(nodeID) == null) {
// Create the node // Create the node
JID creator = new JID(jidFrom); JID creator = new JID(jidFrom);
...@@ -304,7 +277,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -304,7 +277,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
newNode.saveToDB(); newNode.saveToDB();
} }
} }
// Process with PubSub as usual. // Process with PubSub as usual.
pubSubEngine.process(pepService, packet); pubSubEngine.process(pepService, packet);
} }
...@@ -335,7 +308,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -335,7 +308,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
// Other error flows were handled in pubSubEngine.process(...) // Other error flows were handled in pubSubEngine.process(...)
return null; return null;
} }
/** /**
* Retrieves a PEP service -- attempting first from memory, then from the database. * Retrieves a PEP service -- attempting first from memory, then from the database.
* *
...@@ -343,11 +316,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -343,11 +316,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
*/ */
private PEPService getPEPService(String jid) { private PEPService getPEPService(String jid) {
PEPService pepService = pepServices.get(jid); PEPService pepService = pepServices.get(jid);
if (pepService == null) { if (pepService == null) {
pepService = loadPEPServiceFromDB(jid); pepService = loadPEPServiceFromDB(jid);
} }
return pepService; return pepService;
} }
...@@ -520,7 +493,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -520,7 +493,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
} }
public void availableSession(ClientSession session, Presence presence) { public void availableSession(ClientSession session, Presence presence) {
JID newlyAvailableJID = presence.getFrom(); JID newlyAvailableJID = presence.getFrom();
// Send the last published items for the contacts on newlyAvailableJID's roster. // Send the last published items for the contacts on newlyAvailableJID's roster.
try { try {
...@@ -541,7 +514,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -541,7 +514,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
public void remoteUserAvailable(Presence presence) { public void remoteUserAvailable(Presence presence) {
JID jidFrom = presence.getFrom(); JID jidFrom = presence.getFrom();
JID jidTo = presence.getTo(); JID jidTo = presence.getTo();
// Manage the cache of remote presence resources. // Manage the cache of remote presence resources.
Set<JID> remotePresenceSet = knownRemotePresences.get(jidTo.toBareJID()); Set<JID> remotePresenceSet = knownRemotePresences.get(jidTo.toBareJID());
...@@ -566,11 +539,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -566,11 +539,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
public void remoteUserUnavailable(Presence presence) { public void remoteUserUnavailable(Presence presence) {
JID jidFrom = presence.getFrom(); JID jidFrom = presence.getFrom();
JID jidTo = presence.getTo(); JID jidTo = presence.getTo();
// Manage the cache of remote presence resources. // Manage the cache of remote presence resources.
Set<JID> remotePresenceSet = knownRemotePresences.get(jidTo.toBareJID()); Set<JID> remotePresenceSet = knownRemotePresences.get(jidTo.toBareJID());
if (remotePresenceSet != null) { if (remotePresenceSet != null) {
remotePresenceSet.remove(jidFrom); remotePresenceSet.remove(jidFrom);
} }
...@@ -587,7 +560,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -587,7 +560,7 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
public void userDeleting(User user, Map<String, Object> params) { public void userDeleting(User user, Map<String, Object> params) {
JID bareJID = XMPPServer.getInstance().createJID(user.getUsername(), null); JID bareJID = XMPPServer.getInstance().createJID(user.getUsername(), null);
PEPService pepService = getPEPService(bareJID.toString()); PEPService pepService = getPEPService(bareJID.toString());
if (pepService == null) { if (pepService == null) {
return; return;
} }
...@@ -600,121 +573,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -600,121 +573,11 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
} }
} }
rootNode.delete(); rootNode.delete();
// Remove the user's PEP service, finally. // Remove the user's PEP service, finally.
pepServices.remove(bareJID.toString()); pepServices.remove(bareJID.toString());
} }
// TODO: This will need to be refactored once XEP-0115 (Entity Capabilities) is implemented.
/*
public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed) throws PacketRejectedException {
if (processed && packet instanceof IQ && ((IQ) packet).getType() == IQ.Type.result) {
// Examine the packet and return if it does not look like a disco#info result containing
// Entity Capabilities for a client. The sooner we return the better, as this method will be called
// quite a lot.
Element element = packet.getElement();
if (element == null) {
return;
}
Element query = element.element("query");
if (query == null) {
return;
}
else {
if (query.attributeValue("node") == null) {
return;
}
String queryNamespace = query.getNamespaceURI();
if (queryNamespace == null || !queryNamespace.equals("http://jabber.org/protocol/disco#info")) {
return;
}
}
if (Log.isDebugEnabled()) {
Log.debug("PEP: Intercepted a caps result packet: " + packet.toString());
}
Iterator featuresIterator = query.elementIterator("feature");
if (featuresIterator == null) {
return;
}
// Get the sender's full JID considering they may be logged in from multiple
// clients with different notification filters.
String jidFrom = packet.getFrom().toString();
// For each feature variable, or in this case node ID, ending in "+notify" -- add
// the node ID to the set of filtered nodes that jidFrom is interested in being
// notified about.
//
// If none of the feature variables contain the node ID ending in "+notify",
// remove it from the set of filtered nodes that jidFrom is interested in being
// notified about.
Set<String> supportedNodesSet = new HashSet<String>();
while (featuresIterator.hasNext()) {
Element featureElement = (Element) featuresIterator.next();
String featureVar = featureElement.attributeValue("var");
if (featureVar == null) {
continue;
}
supportedNodesSet.add(featureVar);
}
for (String nodeID : supportedNodesSet) {
if (nodeID.endsWith("+notify")) {
// Add the nodeID to the sender's filteredNodesSet.
Set<String> filteredNodesSet = filteredNodesMap.get(jidFrom);
if (filteredNodesSet == null) {
filteredNodesSet = new HashSet<String>();
filteredNodesSet.add(nodeID);
filteredNodesMap.put(jidFrom, filteredNodesSet);
if (Log.isDebugEnabled()) {
Log.debug("PEP: Created filteredNodesSet for " + jidFrom);
Log.debug("PEP: Added " + nodeID + " to " + jidFrom + "'s set of filtered nodes.");
}
}
else {
if (filteredNodesSet.add(nodeID)) {
if (Log.isDebugEnabled()) {
Log.debug("PEP: Added " + nodeID + " to " + jidFrom + "'s set of filtered nodes: ");
Iterator tempIter = filteredNodesSet.iterator();
while (tempIter.hasNext()) {
Log.debug("PEP: " + tempIter.next());
}
}
}
}
}
else {
// Remove the nodeID from the sender's filteredNodesSet if nodeIDPlusNotify
// is not in supportedNodesSet.
Set<String> filteredNodesSet = filteredNodesMap.get(jidFrom);
if (filteredNodesSet == null) {
return;
}
String nodeIDPlusNotify = nodeID + "+notify";
if (!supportedNodesSet.contains(nodeIDPlusNotify) && filteredNodesSet.remove(nodeIDPlusNotify)) {
if (Log.isDebugEnabled()) {
Log.debug("PEP: Removed " + nodeIDPlusNotify + " from " + jidFrom + "'s set of filtered nodes: ");
Iterator tempIter = filteredNodesSet.iterator();
while (tempIter.hasNext()) {
Log.debug("PEP: " + tempIter.next());
}
}
}
}
}
}
}
*/
/** /**
* The following functions are unimplemented required interface methods. * The following functions are unimplemented required interface methods.
*/ */
...@@ -755,12 +618,12 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider, ...@@ -755,12 +618,12 @@ public class IQPEPHandler extends IQHandler implements ServerIdentitiesProvider,
public void userCreated(User user, Map<String, Object> params) { public void userCreated(User user, Map<String, Object> params) {
// Do nothing // Do nothing
} }
public void userModified(User user, Map<String, Object> params) { public void userModified(User user, Map<String, Object> params) {
// Do nothing // Do nothing
} }
} }
...@@ -19,7 +19,8 @@ import org.xmpp.packet.IQ; ...@@ -19,7 +19,8 @@ import org.xmpp.packet.IQ;
/** /**
* <p> * <p>
* An IQHandler used to implement XEP-0163: "Personal Eventing via Pubsub." * An {@link IQHandler} used to implement XEP-0163: "Personal Eventing via Pubsub"
* Version 1.0
* </p> * </p>
* *
* <p> * <p>
......
...@@ -18,6 +18,8 @@ import org.jivesoftware.openfire.PacketRouter; ...@@ -18,6 +18,8 @@ import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.commands.AdHocCommandManager; import org.jivesoftware.openfire.commands.AdHocCommandManager;
import org.jivesoftware.openfire.entitycaps.EntityCapabilities;
import org.jivesoftware.openfire.entitycaps.EntityCapabilitiesManager;
import org.jivesoftware.openfire.pubsub.*; import org.jivesoftware.openfire.pubsub.*;
import org.jivesoftware.openfire.pubsub.models.AccessModel; import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.PublisherModel; import org.jivesoftware.openfire.pubsub.models.PublisherModel;
...@@ -38,8 +40,8 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -38,8 +40,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
/** /**
* A PEPService is a PubSubService for use with XEP-0163: "Personal Eventing via * A PEPService is a {@link PubSubService} for use with XEP-0163: "Personal Eventing via
* Pubsub." * Pubsub" Version 1.0
* *
* @author Armando Jagucki * @author Armando Jagucki
* *
...@@ -105,7 +107,12 @@ public class PEPService implements PubSubService { ...@@ -105,7 +107,12 @@ public class PEPService implements PubSubService {
* Manager that keeps the list of ad-hoc commands and processing command * Manager that keeps the list of ad-hoc commands and processing command
* requests. * requests.
*/ */
private AdHocCommandManager manager; private AdHocCommandManager adHocCommandManager;
/**
* Used to handle filtered-notifications.
*/
private EntityCapabilitiesManager entityCapsManager = EntityCapabilitiesManager.getInstance();
/** /**
* The time to elapse between each execution of the maintenance process. * The time to elapse between each execution of the maintenance process.
...@@ -144,8 +151,8 @@ public class PEPService implements PubSubService { ...@@ -144,8 +151,8 @@ public class PEPService implements PubSubService {
router = server.getPacketRouter(); router = server.getPacketRouter();
// Initialize the ad-hoc commands manager to use for this pep service // Initialize the ad-hoc commands manager to use for this pep service
manager = new AdHocCommandManager(); adHocCommandManager = new AdHocCommandManager();
manager.addCommand(new PendingSubscriptionsCommand(this)); adHocCommandManager.addCommand(new PendingSubscriptionsCommand(this));
// Save or delete published items from the database every 2 minutes // Save or delete published items from the database every 2 minutes
// starting in 2 minutes (default values) // starting in 2 minutes (default values)
...@@ -391,10 +398,11 @@ public class PEPService implements PubSubService { ...@@ -391,10 +398,11 @@ public class PEPService implements PubSubService {
// Check if the recipientFullJID is interested in notifications for this node. // Check if the recipientFullJID is interested in notifications for this node.
// If the recipient has not yet requested any notification filtering, continue and send // If the recipient has not yet requested any notification filtering, continue and send
// the notification. // the notification.
Map<String, Set<String>> filteredNodesMap = XMPPServer.getInstance().getIQPEPHandler().getFilteredNodesMap(); EntityCapabilities entityCaps = entityCapsManager.getEntityCapabilities(recipientFullJID);
Set<String> filteredNodesSet = filteredNodesMap.get(recipientFullJID.toString()); if (entityCaps != null) {
if (filteredNodesSet != null && !filteredNodesSet.contains(nodeID + "+notify")) { if (!entityCaps.containsFeature(nodeID + "+notify")) {
return; return;
}
} }
// Get the full JID of the item publisher from the node that was published to. // Get the full JID of the item publisher from the node that was published to.
...@@ -534,7 +542,7 @@ public class PEPService implements PubSubService { ...@@ -534,7 +542,7 @@ public class PEPService implements PubSubService {
} }
public AdHocCommandManager getManager() { public AdHocCommandManager getManager() {
return manager; return adHocCommandManager;
} }
public PublishedItemTask getPublishedItemTask() { public PublishedItemTask getPublishedItemTask() {
......
...@@ -79,6 +79,7 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy { ...@@ -79,6 +79,7 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy {
cacheNames.put("Disco Server Features", "serverFeatures"); cacheNames.put("Disco Server Features", "serverFeatures");
cacheNames.put("Disco Server Items", "serverItems"); cacheNames.put("Disco Server Items", "serverItems");
cacheNames.put("Remote Server Configurations", "serversConfigurations"); cacheNames.put("Remote Server Configurations", "serversConfigurations");
cacheNames.put("Entity Capabilities", "entityCapabilities");
cacheProps.put("cache.fileTransfer.size", 128 * 1024l); cacheProps.put("cache.fileTransfer.size", 128 * 1024l);
cacheProps.put("cache.fileTransfer.maxLifetime", 1000 * 60 * 10l); cacheProps.put("cache.fileTransfer.maxLifetime", 1000 * 60 * 10l);
...@@ -138,6 +139,8 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy { ...@@ -138,6 +139,8 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy {
cacheProps.put("cache.serverItems.maxLifetime", -1l); cacheProps.put("cache.serverItems.maxLifetime", -1l);
cacheProps.put("cache.serversConfigurations.size", 128 * 1024l); cacheProps.put("cache.serversConfigurations.size", 128 * 1024l);
cacheProps.put("cache.serversConfigurations.maxLifetime", JiveConstants.MINUTE * 30); cacheProps.put("cache.serversConfigurations.maxLifetime", JiveConstants.MINUTE * 30);
cacheProps.put("cache.entityCapabilities.size", -1l);
cacheProps.put("cache.entityCapabilities.maxLifetime", JiveConstants.DAY * 2);
} }
......
/**
* Copyright (C) 2004-2007 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.util;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Element;
import org.junit.Test;
import org.xmpp.packet.IQ;
/**
* Test cases for the EntityCapabilitiesManager class.
*
* @author Armando Jagucki
*/
public class EntityCapabilitiesManagerTest {
@Test
public void testGenerateVerHash() {
IQ iq = new IQ(IQ.Type.result);
iq.setFrom("nurse@capulet.lit/chamber");
iq.setTo("juliet@capulet.lit");
iq.setID("disco123");
Element query = iq.setChildElement("query", "http://jabber.org/protocol/disco#info");
Element identity = query.addElement("identity");
identity.addAttribute("category", "client");
identity.addAttribute("type", "pc");
Element feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/disco#info");
feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/disco#items");
feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/muc");
assertEquals("Generating ver Hash #1", "8RovUdtOmiAjzj+xI7SK5BCw3A8=", generateVerHash(iq));
}
@Test
public void testGenerateVerHash2() {
String S = "client/pc<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
assertEquals("Generating ver Hash #2", "8RovUdtOmiAjzj+xI7SK5BCw3A8=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
}
@Test
public void testGenerateVerHash3() {
String S = "client/pda<http://jabber.org/protocol/geoloc<http://jabber.org/protocol/geoloc+notify<http://jabber.org/protocol/tune<http://jabber.org/protocol/tune+notify<";
assertEquals("Generating ver Hash #3", "DqGwXvV/QC6X9QrPOFAwJoDwHkk=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
}
@Test
public void testGenerateVerHash4() {
String S = "client/pc<http://jabber.org/protocol/activity<http://jabber.org/protocol/activity+notify<http://jabber.org/protocol/geoloc<http://jabber.org/protocol/geoloc+notify<http://jabber.org/protocol/muc<http://jabber.org/protocol/tune<http://jabber.org/protocol/tune+notify<";
assertEquals("Generating ver Hash #4", "Hm1UHUVZowSehEBlWo8lO8mPy/M=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
}
/**
* Generates a 'ver' hash attribute.
*
* In order to help prevent poisoning of entity capabilities information,
* the value of the 'ver' attribute is generated according to the method
* outlined in XEP-0115.
*
* @param packet
* @return the generated 'ver' hash
*/
public String generateVerHash(IQ packet) {
// Initialize an empty string S.
String S = "";
/*
* Sort the service discovery identities by category and then by type
* (if it exists), formatted as 'category' '/' 'type'.
*/
List<String> discoIdentities = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator identitiesIterator = query.elementIterator("identity");
if (identitiesIterator != null) {
while (identitiesIterator.hasNext()) {
Element identityElement = (Element) identitiesIterator.next();
String discoIdentity = identityElement.attributeValue("category");
discoIdentity += '/';
discoIdentity += identityElement.attributeValue("type");
discoIdentities.add(discoIdentity);
}
Collections.sort(discoIdentities);
}
/*
* For each identity, append the 'category/type' to S, followed by the
* '<' character.
*/
for (String discoIdentity : discoIdentities) {
S += discoIdentity;
S += '<';
}
// Sort the supported features.
List<String> discoFeatures = new ArrayList<String>();
Iterator featuresIterator = query.elementIterator("feature");
if (featuresIterator != null) {
while (featuresIterator.hasNext()) {
Element featureElement = (Element) featuresIterator.next();
String discoFeature = featureElement.attributeValue("var");
discoFeatures.add(discoFeature);
}
Collections.sort(discoFeatures);
}
/*
* For each feature, append the feature to S, followed by the '<'
* character.
*/
for (String discoFeature : discoFeatures) {
S += discoFeature;
S += '<';
}
/*
* 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).
*/
S = StringUtils.hash(S, "SHA-1");
S = StringUtils.encodeBase64(StringUtils.decodeHex(S));
return S;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment