/* * 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.Element; import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.handler.IQHandler; import org.jivesoftware.openfire.interceptor.InterceptorManager; import org.jivesoftware.openfire.interceptor.PacketRejectedException; import org.jivesoftware.openfire.privacy.PrivacyList; import org.jivesoftware.openfire.privacy.PrivacyListManager; 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.LocaleUtils; import org.jivesoftware.util.TaskEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.component.IQResultListener; import org.xmpp.packet.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * Routes iq packets throughout the server. 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. * * @author Iain Shigeoka */ public class IQRouter extends BasicModule { private static final Logger Log = LoggerFactory.getLogger(IQRouter.class); private RoutingTable routingTable; private MulticastRouter multicastRouter; private String serverName; private List<IQHandler> iqHandlers = new ArrayList<>(); private Map<String, IQHandler> namespace2Handlers = new ConcurrentHashMap<>(); private Map<String, IQResultListener> resultListeners = new ConcurrentHashMap<>(); private Map<String, Long> resultTimeout = new ConcurrentHashMap<>(); private SessionManager sessionManager; private UserManager userManager; /** * Creates a packet router. */ public IQRouter() { super("XMPP IQ 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(IQ packet) { if (packet == null) { throw new NullPointerException(); } JID sender = packet.getFrom(); ClientSession session = sessionManager.getSession(sender); Element childElement = packet.getChildElement(); // may be null try { // Invoke the interceptors before we process the read packet InterceptorManager.getInstance().invokeInterceptors(packet, session, true, false); JID to = packet.getTo(); if (session != null && to != null && session.getStatus() == Session.STATUS_CONNECTED && !serverName.equals(to.toString())) { // User is requesting this server to authenticate for another server. Return // a bad-request error IQ reply = IQ.createResultIQ(packet); if (childElement != null) { reply.setChildElement(childElement.createCopy()); } reply.setError(PacketError.Condition.bad_request); session.process(reply); Log.warn("User tried to authenticate with this server using an unknown receipient: " + packet.toXML()); } else if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED || ( childElement != null && isLocalServer(to) && ( "jabber:iq:auth".equals(childElement.getNamespaceURI()) || "jabber:iq:register".equals(childElement.getNamespaceURI()) || "urn:ietf:params:xml:ns:xmpp-bind".equals(childElement.getNamespaceURI())))) { handle(packet); } else if (packet.getType() == IQ.Type.get || packet.getType() == IQ.Type.set) { IQ reply = IQ.createResultIQ(packet); if (childElement != null) { reply.setChildElement(childElement.createCopy()); } reply.setError(PacketError.Condition.not_authorized); session.process(reply); } // Invoke the interceptors after we have processed the read packet InterceptorManager.getInstance().invokeInterceptors(packet, session, true, true); } catch (PacketRejectedException e) { if (session != null) { // An interceptor rejected this packet so answer a not_allowed error IQ reply = new IQ(); if (childElement != null) { reply.setChildElement(childElement.createCopy()); } reply.setID(packet.getID()); reply.setTo(session.getAddress()); reply.setFrom(packet.getTo()); reply.setError(PacketError.Condition.not_allowed); session.process(reply); // Check if a message notifying the rejection should be sent if (e.getRejectionMessage() != null && e.getRejectionMessage().trim().length() > 0) { // A message for the rejection will be sent to the sender of the rejected packet Message notification = new Message(); notification.setTo(session.getAddress()); notification.setFrom(packet.getTo()); notification.setBody(e.getRejectionMessage()); session.process(notification); } } } } /** * <p>Adds a new IQHandler to the list of registered handler. The new IQHandler will be * responsible for handling IQ packet whose namespace matches the namespace of the * IQHandler.</p> * * An IllegalArgumentException may be thrown if the IQHandler to register was already provided * by the server. The server provides a certain list of IQHandlers when the server is * started up. * * @param handler the IQHandler to add to the list of registered handler. */ public void addHandler(IQHandler handler) { if (iqHandlers.contains(handler)) { throw new IllegalArgumentException("IQHandler already provided by the server"); } // Ask the handler to be initialized handler.initialize(XMPPServer.getInstance()); // Register the handler as the handler of the namespace namespace2Handlers.put(handler.getInfo().getNamespace(), handler); } /** * <p>Removes an IQHandler from the list of registered handler. The IQHandler to remove was * responsible for handling IQ packet whose namespace matches the namespace of the * IQHandler.</p> * * An IllegalArgumentException may be thrown if the IQHandler to remove was already provided * by the server. The server provides a certain list of IQHandlers when the server is * started up. * * @param handler the IQHandler to remove from the list of registered handler. */ public void removeHandler(IQHandler handler) { if (iqHandlers.contains(handler)) { throw new IllegalArgumentException("Cannot remove an IQHandler provided by the server"); } // Unregister the handler as the handler of the namespace namespace2Handlers.remove(handler.getInfo().getNamespace()); } /** * Adds an {@link IQResultListener} that will be invoked when an IQ result * is sent to the server itself and is of type result or error. This is a * nice way for the server to send IQ packets to other XMPP entities and be * waked up when a response is received back.<p> * * Once an IQ result was received, the listener will be invoked and removed * from the list of listeners.<p> * * If no result was received within one minute, the timeout method of the * listener will be invoked and the listener will be removed from the list * of listeners. * * @param id * the id of the IQ packet being sent from the server to an XMPP * entity. * @param listener * the IQResultListener that will be invoked when an answer is * received */ public void addIQResultListener(String id, IQResultListener listener) { addIQResultListener(id, listener, 60 * 1000); } /** * Adds an {@link IQResultListener} that will be invoked when an IQ result * is sent to the server itself and is of type result or error. This is a * nice way for the server to send IQ packets to other XMPP entities and be * waked up when a response is received back.<p> * * Once an IQ result was received, the listener will be invoked and removed * from the list of listeners.<p> * * If no result was received within the specified amount of milliseconds, * the timeout method of the listener will be invoked and the listener will * be removed from the list of listeners.<p> * * Note that the listener will remain active for <em>at least</em> the * specified timeout value. The listener will not be removed at the exact * moment it times out. Instead, purging of timed out listeners is a * periodic scheduled job. * * @param id * the id of the IQ packet being sent from the server to an XMPP * entity. * @param listener * the IQResultListener that will be invoked when an answer is * received. * @param timeoutmillis * The amount of milliseconds after which waiting for a response * should be stopped. */ public void addIQResultListener(String id, IQResultListener listener, long timeoutmillis) { resultListeners.put(id, listener); resultTimeout.put(id, System.currentTimeMillis() + timeoutmillis); } @Override public void initialize(XMPPServer server) { super.initialize(server); TaskEngine.getInstance().scheduleAtFixedRate(new TimeoutTask(), 5000, 5000); serverName = server.getServerInfo().getXMPPDomain(); routingTable = server.getRoutingTable(); multicastRouter = server.getMulticastRouter(); iqHandlers.addAll(server.getIQHandlers()); sessionManager = server.getSessionManager(); userManager = server.getUserManager(); } /** * A JID is considered local if: * 1) is null or * 2) has no domain or domain is empty * or * if it's not a full JID and it was sent to the server itself. * * @param recipientJID address to check. * @return true if the specified address belongs to the local server. */ private boolean isLocalServer(JID recipientJID) { // Check if no address was specified in the IQ packet boolean implicitServer = recipientJID == null || recipientJID.getDomain() == null || "".equals(recipientJID.getDomain()); if (!implicitServer) { // We found an address. Now check if it's a bare or full JID if (recipientJID.getNode() == null || recipientJID.getResource() == null) { // Address is a bare JID so check if it was sent to the server itself return serverName.equals(recipientJID.getDomain()); } // Address is a full JID. IQ packets sent to full JIDs are not handle by the server return false; } return true; } private void handle(IQ packet) { JID recipientJID = packet.getTo(); // Check if the packet was sent to the server hostname if (recipientJID != null && recipientJID.getNode() == null && recipientJID.getResource() == null && serverName.equals(recipientJID.getDomain())) { Element childElement = packet.getChildElement(); if (childElement != null && childElement.element("addresses") != null) { // Packet includes multicast processing instructions. Ask the multicastRouter // to route this packet multicastRouter.route(packet); return; } } if (packet.getID() != null && (IQ.Type.result == packet.getType() || IQ.Type.error == packet.getType())) { // The server got an answer to an IQ packet that was sent from the server IQResultListener iqResultListener = resultListeners.remove(packet.getID()); if (iqResultListener != null) { resultTimeout.remove(packet.getID()); if (iqResultListener != null) { try { iqResultListener.receivedAnswer(packet); } catch (Exception e) { Log.error( "Error processing answer of remote entity. Answer: " + packet.toXML(), e); } return; } } } try { // Check for registered components, services or remote servers if (recipientJID != null && (routingTable.hasComponentRoute(recipientJID) || routingTable.hasServerRoute(recipientJID))) { // A component/service/remote server was found that can handle the Packet routingTable.routePacket(recipientJID, packet, false); return; } if (isLocalServer(recipientJID)) { // Let the server handle the Packet Element childElement = packet.getChildElement(); String namespace = null; if (childElement != null) { namespace = childElement.getNamespaceURI(); } if (namespace == null) { if (packet.getType() != IQ.Type.result && packet.getType() != IQ.Type.error) { // Do nothing. We can't handle queries outside of a valid namespace Log.warn("Unknown packet " + packet.toXML()); } } else { // Check if communication to local users is allowed if (recipientJID != null && userManager.isRegisteredUser(recipientJID.getNode())) { PrivacyList list = PrivacyListManager.getInstance().getDefaultPrivacyList(recipientJID.getNode()); if (list != null && list.shouldBlockPacket(packet)) { // Communication is blocked if (IQ.Type.set == packet.getType() || IQ.Type.get == packet.getType()) { // Answer that the service is unavailable sendErrorPacket(packet, PacketError.Condition.service_unavailable); } return; } } IQHandler handler = getHandler(namespace); if (handler == null) { if (recipientJID == null) { // Answer an error since the server can't handle the requested namespace sendErrorPacket(packet, PacketError.Condition.service_unavailable); } else if (recipientJID.getNode() == null || "".equals(recipientJID.getNode())) { // Answer an error if JID is of the form <domain> sendErrorPacket(packet, PacketError.Condition.feature_not_implemented); } else { // JID is of the form <node@domain> // Answer an error since the server can't handle packets sent to a node sendErrorPacket(packet, PacketError.Condition.service_unavailable); } } else { handler.process(packet); } } } else { // RFC 6121 8.5.1. No Such User http://xmpp.org/rfcs/rfc6121.html#rules-localpart-nosuchuser // If the 'to' address specifies a bare JID <localpart@domainpart> or full JID <localpart@domainpart/resourcepart> where the domainpart of the JID matches a configured domain that is serviced by the server itself, the server MUST proceed as follows. // If the user account identified by the 'to' attribute does not exist, how the stanza is processed depends on the stanza type. if (recipientJID != null && recipientJID.getNode() != null && serverName.equals(recipientJID.getDomain()) && !userManager.isRegisteredUser(recipientJID.getNode()) && sessionManager.getSession(recipientJID) == null && (IQ.Type.set == packet.getType() || IQ.Type.get == packet.getType())) { // For an IQ stanza, the server MUST return a <service-unavailable/> stanza error to the sender. sendErrorPacket(packet, PacketError.Condition.service_unavailable); return; } ClientSession session = sessionManager.getSession(packet.getFrom()); boolean isAcceptable = true; if (session instanceof LocalClientSession) { // Check if we could process IQ stanzas 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 IQ dummyIQ = packet.createCopy(); dummyIQ.setFrom(packet.getTo()); dummyIQ.setTo(packet.getFrom()); if (!((LocalClientSession) session).canProcess(dummyIQ)) { packet.setTo(session.getAddress()); packet.setFrom((JID) null); packet.setError(PacketError.Condition.not_acceptable); session.process(packet); isAcceptable = false; } } if (isAcceptable) { // JID is of the form <node@domain/resource> or belongs to a remote server // or to an uninstalled component routingTable.routePacket(recipientJID, packet, false); } } } catch (Exception e) { Log.error(LocaleUtils.getLocalizedString("admin.error.routing"), e); Session session = sessionManager.getSession(packet.getFrom()); if (session != null) { IQ reply = IQ.createResultIQ(packet); reply.setError(PacketError.Condition.internal_server_error); session.process(reply); } } } private void sendErrorPacket(IQ originalPacket, PacketError.Condition condition) { if (IQ.Type.error == originalPacket.getType()) { Log.error("Cannot reply an IQ error to another IQ error: " + originalPacket.toXML()); return; } if (originalPacket.getFrom() == null) { if (Log.isDebugEnabled()) { Log.debug("Original IQ has no sender for reply; dropped: " + originalPacket.toXML()); } return; } IQ reply = IQ.createResultIQ(originalPacket); reply.setChildElement(originalPacket.getChildElement().createCopy()); reply.setError(condition); // Check if the server was the sender of the IQ if (serverName.equals(originalPacket.getFrom().toString())) { // Just let the IQ router process the IQ error reply handle(reply); return; } // Route the error packet to the original sender of the IQ. routingTable.routePacket(reply.getTo(), reply, true); } /** * Determines if this instance has support (formally: has a IQ Handler) for the provided namespace. * * @param namespace Identifier of functionality (cannot be null) * @return true if the functionality identified by the namespace is supported, otherwise false. */ public boolean supports( String namespace ) { return getHandler( namespace ) != null; } private IQHandler getHandler(String namespace) { IQHandler handler = namespace2Handlers.get(namespace); if (handler == null) { for (IQHandler handlerCandidate : iqHandlers) { IQHandlerInfo handlerInfo = handlerCandidate.getInfo(); if (handlerInfo != null && namespace.equalsIgnoreCase(handlerInfo.getNamespace())) { handler = handlerCandidate; namespace2Handlers.put(namespace, handler); break; } } } return handler; } /** * 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 IQ packet that failed to be sent to the recipient. */ public void routingFailed( JID recipient, Packet packet ) { Log.debug( "IQ sent to unreachable address: " + packet.toXML() ); final IQ iq = (IQ) packet; // If a route to the target address was not found then try to answer a service_unavailable error code to the sender of the IQ packet if ( iq.isRequest() ) { sendErrorPacket( iq, PacketError.Condition.service_unavailable ); } } /** * Timer task that will remove Listeners that wait for results to IQ stanzas * that have timed out. Time out values can be set to each listener * individually by adjusting the timeout value in the third parameter of * {@link IQRouter#addIQResultListener(String, IQResultListener, long)}. * * @author Guus der Kinderen, guus@nimbuzz.com */ private class TimeoutTask extends TimerTask { /** * Iterates over and removes all timed out results.<p> * * The map that keeps track of timeout values is ordered by timeout * date. This way, iteration can be stopped as soon as the first value * has been found that didn't timeout yet. */ @Override public void run() { // Use an Iterator to allow changes to the Map that is backing // the Iterator. final Iterator<Map.Entry<String, Long>> it = resultTimeout.entrySet().iterator(); while (it.hasNext()) { final Map.Entry<String, Long> pointer = it.next(); if (System.currentTimeMillis() < pointer.getValue()) { // This entry has not expired yet. Ignore it. continue; } final String packetId = pointer.getKey(); // remove this listener from the list final IQResultListener listener = resultListeners.remove(packetId); if (listener != null) { // notify listener of the timeout. listener.answerTimeout(packetId); } // remove the packet from the list that's used to track // timeouts it.remove(); } } } }