Commit 128395c9 authored by csh's avatar csh

Add support for XEP-0280 Message Carbons

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@14012 b35dd754-fafc-0310-a699-88a17e54d16e
parent cf345717
......@@ -20,10 +20,14 @@
package org.jivesoftware.openfire;
import java.util.List;
import java.util.StringTokenizer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.dom4j.QName;
import org.jivesoftware.openfire.carbons.Sent;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
......@@ -85,6 +89,7 @@ public class MessageRouter extends BasicModule {
throw new NullPointerException();
}
ClientSession session = sessionManager.getSession(packet.getFrom());
try {
// Invoke the interceptors before we process the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true, false);
......@@ -97,7 +102,7 @@ public class MessageRouter extends BasicModule {
}
// Check if the message was sent to the server hostname
if (recipientJID != null && recipientJID.getNode() == null && recipientJID.getResource() == null &&
if (recipientJID.getNode() == null && recipientJID.getResource() == null &&
serverName.equals(recipientJID.getDomain())) {
if (packet.getElement().element("addresses") != null) {
// Message includes multicast processing instructions. Ask the multicastRouter
......@@ -129,6 +134,7 @@ public class MessageRouter extends BasicModule {
}
}
if (isAcceptable) {
boolean isPrivate = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2")) != null;
try {
// Deliver stanza to requested route
routingTable.routePacket(recipientJID, packet, false);
......@@ -136,6 +142,30 @@ public class MessageRouter extends BasicModule {
log.error("Failed to route packet: " + packet.toXML(), e);
routingFailed(recipientJID, packet);
}
// Sent carbon copies to other resources of the sender:
// When a client sends a <message/> of type "chat"
if (packet.getType() == Message.Type.chat && !isPrivate) { // && session.isMessageCarbonsEnabled() ??? // must the own session also be carbon enabled?
List<JID> routes = routingTable.getRoutes(packet.getFrom().asBareJID(), null);
for (JID route : routes) {
// The sending server SHOULD NOT send a forwarded copy to the sending full JID if it is a Carbons-enabled resource.
if (!route.equals(session.getAddress())) {
ClientSession clientSession = sessionManager.getSession(route);
if (clientSession != null && clientSession.isMessageCarbonsEnabled()) {
Message message = new Message();
// The wrapping message SHOULD maintain the same 'type' attribute value
message.setType(packet.getType());
// the 'from' attribute MUST be the Carbons-enabled user's bare JID
message.setFrom(packet.getFrom().asBareJID());
// and the 'to' attribute SHOULD be the full JID of the resource receiving the copy
message.setTo(route);
// The content of the wrapping message MUST contain a <sent/> element qualified by the namespace "urn:xmpp:carbons:2", which itself contains a <forwarded/> qualified by the namespace "urn:xmpp:forward:0" that contains the original <message/> stanza.
message.addExtension(new Sent(new Forwarded(packet)));
clientSession.process(message);
}
}
}
}
}
}
else {
......
......@@ -561,6 +561,8 @@ public class XMPPServer {
loadModule(InternalComponentManager.class.getName());
loadModule(MultiUserChatManager.class.getName());
loadModule(ClearspaceManager.class.getName());
loadModule(IQMessageCarbonsHandler.class.getName());
// Load this module always last since we don't want to start listening for clients
// before the rest of the modules have been started
loadModule(ConnectionManagerImpl.class.getName());
......
package org.jivesoftware.openfire.carbons;
import org.jivesoftware.openfire.forward.Forwarded;
import org.xmpp.packet.PacketExtension;
/**
* @author Christian Schudt
*/
public class Received extends PacketExtension {
public Received(Forwarded forwarded) {
super("received", "urn:xmpp:carbons:2");
element.add(forwarded.getElement());
}
}
package org.jivesoftware.openfire.carbons;
import org.jivesoftware.openfire.forward.Forwarded;
import org.xmpp.packet.PacketExtension;
/**
* @author Christian Schudt
*/
public class Sent extends PacketExtension {
public Sent(Forwarded forwarded) {
super("sent", "urn:xmpp:carbons:2");
element.add(forwarded.getElement());
}
}
package org.jivesoftware.openfire.forward;
import org.dom4j.Element;
import org.dom4j.QName;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketExtension;
/**
* @author Christian Schudt
*/
public class Forwarded extends PacketExtension {
public Forwarded(Message message) {
super("forwarded", "urn:xmpp:forward:0");
message.getElement().setQName(QName.get("message", "jabber:client"));
for (Object element : message.getElement().elements()) {
if (element instanceof Element) {
Element el = (Element) element;
el.setQName(QName.get(el.getName(), "jabber:client"));
}
}
element.add(message.getElement());
}
}
/**
* $RCSfile$
* $Revision: 1747 $
* $Date: 2005-08-04 18:36:36 -0300 (Thu, 04 Aug 2005) $
*
* Copyright (C) 2004-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.handler;
import org.dom4j.Element;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.session.ClientSession;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import java.util.Collections;
import java.util.Iterator;
/**
* This handler manages XEP-0280 Message Carbons.
*
* @author Christian Schudt
*/
public class IQMessageCarbonsHandler extends IQHandler implements ServerFeaturesProvider {
private static final String NAMESPACE = "urn:xmpp:carbons:2";
private IQHandlerInfo info;
public IQMessageCarbonsHandler() {
super("Message Carbons Handler");
info = new IQHandlerInfo("", NAMESPACE);
}
@Override
public IQ handleIQ(IQ packet) {
Element enable = packet.getChildElement();
if (XMPPServer.getInstance().isLocal(packet.getFrom())) {
if (enable.getName().equals("enable")) {
ClientSession clientSession = sessionManager.getSession(packet.getFrom());
clientSession.setMessageCarbonsEnabled(true);
return IQ.createResultIQ(packet);
} else if (enable.getName().equals("disable")) {
ClientSession clientSession = sessionManager.getSession(packet.getFrom());
clientSession.setMessageCarbonsEnabled(false);
return IQ.createResultIQ(packet);
} else {
IQ error = IQ.createResultIQ(packet);
error.setError(PacketError.Condition.bad_request);
return error;
}
} else {
// <forbidden/> if the server's policy forbids the client from enabling Carbons.
IQ error = IQ.createResultIQ(packet);
error.setError(PacketError.Condition.forbidden);
return error;
}
}
@Override
public IQHandlerInfo getInfo() {
return info;
}
public Iterator<String> getFeatures() {
return Collections.singleton(NAMESPACE).iterator();
}
}
\ No newline at end of file
......@@ -146,4 +146,19 @@ public interface ClientSession extends Session {
* @return the new number of conflicts detected on this session.
*/
public int incrementConflictCount();
/**
* Indicates, whether message carbons are enabled.
*
* @return True, if message carbons are enabled.
*/
boolean isMessageCarbonsEnabled();
/**
* Enables or disables <a href="http://xmpp.org/extensions/xep-0280.html">XEP-0280: Message Carbons</a> for this session.
*
* @param enabled True, if message carbons are enabled.
* @see <a href="hhttp://xmpp.org/extensions/xep-0280.html">XEP-0280: Message Carbons</a>
*/
void setMessageCarbonsEnabled(boolean enabled);
}
......@@ -74,6 +74,8 @@ public class LocalClientSession extends LocalSession implements ClientSession {
private static Map<String,String> allowedIPs = new HashMap<String,String>();
private static Map<String,String> allowedAnonymIPs = new HashMap<String,String>();
private boolean messageCarbonsEnabled;
/**
* The authentication token for this session.
*/
......@@ -813,6 +815,16 @@ public class LocalClientSession extends LocalSession implements ClientSession {
return conflictCount;
}
@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}
@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = true;
}
/**
* Returns true if the specified packet must not be blocked based on the active or default
* privacy list rules. The active list will be tried first. If none was found then the
......
......@@ -20,13 +20,17 @@
package org.jivesoftware.openfire.spi;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.carbons.Received;
import org.jivesoftware.openfire.cluster.ClusterEventListener;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.component.ExternalComponentManager;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.handler.PresenceUpdateHandler;
import org.jivesoftware.openfire.server.OutgoingSessionPromise;
import org.jivesoftware.openfire.session.*;
......@@ -277,11 +281,16 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
private boolean routeToLocalDomain(JID jid, Packet packet,
boolean fromServer) {
boolean routed = false;
Element privateElement = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2"));
boolean isPrivate = privateElement != null;
// The receiving server and SHOULD remove the <private/> element before delivering to the recipient.
packet.getElement().remove(privateElement);
if (jid.getResource() == null) {
// Packet sent to a bare JID of a user
if (packet instanceof Message) {
// Find best route of local user
routed = routeToBareJID(jid, (Message) packet);
routed = routeToBareJID(jid, (Message) packet, isPrivate);
}
else {
throw new PacketException("Cannot route packet of type IQ or Presence to bare JID: " + packet.toXML());
......@@ -298,11 +307,40 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
!presenceUpdateHandler.hasDirectPresence(packet.getTo(), packet.getFrom())) {
Log.debug("Unable to route packet. Packet should only be sent to available sessions and the route is not available. {} ", packet.toXML());
routed = false;
}
else {
if (localRoutingTable.isLocalRoute(jid)) {
// This is a route to a local user hosted in this node
try {
} else {
if (localRoutingTable.isLocalRoute(jid)) {
if (packet instanceof Message) {
Message message = (Message) packet;
if (message.getType() == Message.Type.chat && !isPrivate) {
List<JID> routes = getRoutes(jid.asBareJID(), null);
for (JID route : routes) {
// The receiving server MUST NOT send a forwarded copy to the full JID the original <message/> stanza was addressed to, as that recipient receives the original <message/> stanza.
if (!route.equals(jid)) {
ClientSession clientSession = getClientRoute(route);
if (clientSession.isMessageCarbonsEnabled()) {
Message carbon = new Message();
// The wrapping message SHOULD maintain the same 'type' attribute value;
carbon.setType(message.getType());
// the 'from' attribute MUST be the Carbons-enabled user's bare JID
carbon.setFrom(route.asBareJID());
// and the 'to' attribute MUST be the full JID of the resource receiving the copy
carbon.setTo(route);
// The content of the wrapping message MUST contain a <received/> element qualified by the namespace "urn:xmpp:carbons:2", which itself contains a <forwarded/> element qualified by the namespace "urn:xmpp:forward:0" that contains the original <message/>.
carbon.addExtension(new Received(new Forwarded(message)));
try {
localRoutingTable.getRoute(route.toString()).process(carbon);
} catch (UnauthorizedException e) {
Log.error("Unable to route packet " + packet.toXML(), e);
}
}
}
}
}
}
// This is a route to a local user hosted in this node
try {
localRoutingTable.getRoute(jid.toString()).process(packet);
routed = true;
} catch (UnauthorizedException e) {
......@@ -333,9 +371,6 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
* the recipient of the packet to route.
* @param packet
* the packet to route.
* @param fromServer
* true if the packet was created by the server. This packets
* should always be delivered
* @throws PacketException
* thrown if the packet is malformed (results in the sender's
* session being shutdown).
......@@ -398,9 +433,6 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
* the recipient of the packet to route.
* @param packet
* the packet to route.
* @param fromServer
* true if the packet was created by the server. This packets
* should always be delivered
* @throws PacketException
* thrown if the packet is malformed (results in the sender's
* session being shutdown).
......@@ -483,7 +515,7 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
* @param packet the message to send.
* @return true if at least one target session was found
*/
private boolean routeToBareJID(JID recipientJID, Message packet) {
private boolean routeToBareJID(JID recipientJID, Message packet, boolean isPrivate) {
List<ClientSession> sessions = new ArrayList<ClientSession>();
// Get existing AVAILABLE sessions of this user or AVAILABLE to the sender of the packet
for (JID address : getRoutes(recipientJID, packet.getFrom())) {
......@@ -492,7 +524,7 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
sessions.add(session);
}
}
sessions = getHighestPrioritySessions(sessions);
if (sessions.isEmpty()) {
// No session is available so store offline
Log.debug("Unable to route packet. No session is available so store offline. {} ", packet.toXML());
......@@ -503,6 +535,15 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
sessions.get(0).process(packet);
}
else {
// Check for message carbons enabled sessions and sent message to them.
for (ClientSession session : sessions) {
// Deliver to each session.
if (shouldSentToResource(session, packet, isPrivate)) {
session.process(packet);
}
}
// Many sessions have the highest priority (be smart now) :)
if (!JiveGlobals.getBooleanProperty("route.all-resources", false)) {
// Sort sessions by show value (e.g. away, xa)
......@@ -555,19 +596,32 @@ public class RoutingTableImpl extends BasicModule implements RoutingTable, Clust
return o2.getLastActiveDate().compareTo(o1.getLastActiveDate());
}
});
// Deliver stanza to session with highest priority, highest show value and most recent activity
targets.get(0).process(packet);
// Make sure, we don't send the packet again, if it has already been sent by message carbons.
ClientSession session = targets.get(0);
if (!shouldSentToResource(session, packet, isPrivate)) {
// Deliver stanza to session with highest priority, highest show value and most recent activity
session.process(packet);
}
}
else {
// Deliver stanza to all connected resources with highest priority
sessions = getHighestPrioritySessions(sessions);
for (ClientSession session : sessions) {
session.process(packet);
// Make sure, we don't send the packet again, if it has already been sent by message carbons.
if (!shouldSentToResource(session, packet, isPrivate)) {
session.process(packet);
}
}
}
}
return true;
}
private boolean shouldSentToResource(ClientSession session, Message message, boolean isPrivate) {
return !isPrivate && session.isMessageCarbonsEnabled() && message.getType() == Message.Type.chat;
}
/**
* Returns the sessions that had the highest presence priority that is non-negative.
*
......
......@@ -47,6 +47,8 @@ public class RemoteClientSession extends RemoteSession implements ClientSession
private long initialized = -1;
private boolean messageCarbonsEnabled;
public RemoteClientSession(byte[] nodeID, JID address) {
super(nodeID, address);
}
......@@ -153,6 +155,16 @@ public class RemoteClientSession extends RemoteSession implements ClientSession
return (Integer) doSynchronousClusterTask(task);
}
@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}
@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = true;
}
RemoteSessionTask getRemoteSessionTask(RemoteSessionTask.Operation operation) {
return new ClientSessionTask(address, operation);
}
......
package org.jivesoftware.openfire.carbons;
import junit.framework.Assert;
import org.jivesoftware.openfire.forward.Forwarded;
import org.junit.Test;
import org.xmpp.packet.Message;
/**
* @author Christian Schudt
*/
public class MessageCarbonsTest {
@Test
public void testSent() {
Message message = new Message();
message.setType(Message.Type.chat);
message.setBody("Tests");
Forwarded forwarded = new Forwarded(message);
Sent sent = new Sent(forwarded);
String xml = sent.getElement().asXML();
Assert.assertEquals("<sent xmlns=\"urn:xmpp:carbons:2\"><forwarded xmlns=\"urn:xmpp:forward:0\"><message xmlns=\"jabber:client\" type=\"chat\"><body>Tests</body></message></forwarded></sent>", xml);
}
@Test
public void testReceived() {
Message message = new Message();
message.setType(Message.Type.chat);
message.setBody("Tests");
Forwarded forwarded = new Forwarded(message);
Received received = new Received(forwarded);
String xml = received.getElement().asXML();
Assert.assertEquals("<received xmlns=\"urn:xmpp:carbons:2\"><forwarded xmlns=\"urn:xmpp:forward:0\"><message xmlns=\"jabber:client\" type=\"chat\"><body>Tests</body></message></forwarded></received>", xml);
}
}
package org.jivesoftware.openfire.forward;
import junit.framework.Assert;
import org.junit.Test;
import org.xmpp.packet.Message;
/**
* @author Christian Schudt
*/
public class ForwardTest {
@Test
public void testForwarded() {
Message message = new Message();
message.setType(Message.Type.chat);
message.setBody("Tests");
Forwarded forwarded = new Forwarded(message);
String xml = forwarded.getElement().asXML();
Assert.assertEquals("<forwarded xmlns=\"urn:xmpp:forward:0\"><message xmlns=\"jabber:client\" type=\"chat\"><body>Tests</body></message></forwarded>", xml);
}
}
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