/* * 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; 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.session.ClientSession; import org.jivesoftware.openfire.session.LocalClientSession; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import org.xmpp.packet.PacketError; import java.util.List; import java.util.StringTokenizer; /** * <p>Route message packets throughout the server.</p> * <p>Routing is based on the recipient and sender addresses. The typical * packet will often be routed twice, once from the sender to some internal * server component for handling or processing, and then back to the router * to be delivered to it's final destination.</p> * * @author Iain Shigeoka */ public class MessageRouter extends BasicModule { private static Logger log = LoggerFactory.getLogger(MessageRouter.class); private OfflineMessageStrategy messageStrategy; private RoutingTable routingTable; private SessionManager sessionManager; private MulticastRouter multicastRouter; private UserManager userManager; private String serverName; /** * Constructs a message router. */ public MessageRouter() { super("XMPP Message Router"); } /** * <p>Performs the actual packet routing.</p> * <p>You routing is considered 'quick' and implementations may not take * excessive amounts of time to complete the routing. If routing will take * a long amount of time, the actual routing should be done in another thread * so this method returns quickly.</p> * <h2>Warning</h2> * <p>Be careful to enforce concurrency DbC of concurrent by synchronizing * any accesses to class resources.</p> * * @param packet The packet to route * @throws NullPointerException If the packet is null */ public void route(Message packet) { if (packet == null) { 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); if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED) { JID recipientJID = packet.getTo(); // If the server receives a message stanza with no 'to' attribute, it MUST treat the message as if the 'to' address were the bare JID <localpart@domainpart> of the sending entity. if (recipientJID == null) { recipientJID = packet.getFrom().asBareJID(); } // Check if the message was sent to the server hostname 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 // to route this packet multicastRouter.route(packet); } else { // Message was sent to the server hostname so forward it to a configurable // set of JID's (probably admin users) sendMessageToAdmins(packet); } return; } boolean isAcceptable = true; if (session instanceof LocalClientSession) { // Check if we could process messages from the recipient. // If not, return a not-acceptable error as per XEP-0016: // If the user attempts to send an outbound stanza to a contact and that stanza type is blocked, the user's server MUST NOT route the stanza to the contact but instead MUST return a <not-acceptable/> error Message dummyMessage = packet.createCopy(); dummyMessage.setFrom(packet.getTo()); dummyMessage.setTo(packet.getFrom()); if (!((LocalClientSession) session).canProcess(dummyMessage)) { packet.setTo(session.getAddress()); packet.setFrom((JID)null); packet.setError(PacketError.Condition.not_acceptable); session.process(packet); isAcceptable = false; } } 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); } catch (Exception e) { 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 != null) { // && 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 { packet.setTo(session.getAddress()); packet.setFrom((JID)null); packet.setError(PacketError.Condition.not_authorized); session.process(packet); } // Invoke the interceptors after we have processed the read packet InterceptorManager.getInstance().invokeInterceptors(packet, session, true, true); } catch (PacketRejectedException e) { // An interceptor rejected this packet if (session != null && e.getRejectionMessage() != null && e.getRejectionMessage().trim().length() > 0) { // A message for the rejection will be sent to the sender of the rejected packet Message reply = new Message(); reply.setID(packet.getID()); reply.setTo(session.getAddress()); reply.setFrom(packet.getTo()); reply.setType(packet.getType()); reply.setThread(packet.getThread()); reply.setBody(e.getRejectionMessage()); session.process(reply); } } } /** * Forwards the received message to the list of users defined in the property * <b>xmpp.forward.admins</b>. The property may include bare JIDs or just usernames separated * by commas or white spaces. When using bare JIDs the target user may belong to a remote * server.<p> * * If the property <b>xmpp.forward.admins</b> was not defined then the message will be sent * to all the users allowed to enter the admin console. * * @param packet the message to forward. */ private void sendMessageToAdmins(Message packet) { String jids = JiveGlobals.getProperty("xmpp.forward.admins"); if (jids != null && jids.trim().length() > 0) { // Forward the message to the users specified in the "xmpp.forward.admins" property StringTokenizer tokenizer = new StringTokenizer(jids, ", "); while (tokenizer.hasMoreTokens()) { String username = tokenizer.nextToken(); Message forward = packet.createCopy(); if (username.contains("@")) { // Use the specified bare JID address as the target address forward.setTo(username); } else { forward.setTo(username + "@" + serverName); } route(forward); } } else { // Forward the message to the users allowed to log into the admin console for (JID jid : XMPPServer.getInstance().getAdmins()) { Message forward = packet.createCopy(); forward.setTo(jid); route(forward); } } } @Override public void initialize(XMPPServer server) { super.initialize(server); messageStrategy = server.getOfflineMessageStrategy(); routingTable = server.getRoutingTable(); sessionManager = server.getSessionManager(); multicastRouter = server.getMulticastRouter(); userManager = server.getUserManager(); serverName = server.getServerInfo().getXMPPDomain(); } /** * Notification message indicating that a packet has failed to be routed to the recipient. * * @param recipient address of the entity that failed to receive the packet. * @param packet Message packet that failed to be sent to the recipient. */ public void routingFailed( JID recipient, Packet packet ) { log.debug( "Message sent to unreachable address: " + packet.toXML() ); final Message msg = (Message) packet; boolean storeOffline = true; if ( msg.getType().equals( Message.Type.chat ) && serverName.equals( recipient.getDomain() ) && recipient.getResource() != null ) { // Find an existing AVAILABLE session with non-negative priority. for (JID address : routingTable.getRoutes(recipient.asBareJID(), packet.getFrom())) { ClientSession session = routingTable.getClientRoute(address); if (session != null && session.isInitialized()) { if (session.getPresence().getPriority() >= 1) { storeOffline = false; } } } } if ( !storeOffline ) { // If message was sent to an unavailable full JID of a user then retry using the bare JID. routingTable.routePacket( recipient.asBareJID(), packet, false ); } else { // Delegate to offline message strategy, which will either bounce or ignore the message depending on user settings. messageStrategy.storeOffline( (Message) packet ); } } }