/* * Copyright (C) 2005-2008 Jive Software, 2017-2025 Ignite Realtime Foundation. 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.cluster.ClusterManager; import org.jivesoftware.openfire.cluster.IQResultListenerTask; import org.jivesoftware.openfire.cluster.NodeID; 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.DomainPair; 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.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.CacheFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.component.IQResultListener; import org.xmpp.packet.*; import java.time.Duration; 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 final List iqHandlers = new ArrayList<>(); private final Map namespace2Handlers = new ConcurrentHashMap<>(); private final Map resultListeners = new ConcurrentHashMap<>(); private final Map resultTimeout = new ConcurrentHashMap<>(); private final Cache resultPending = CacheFactory.createCache("Routing Result Listeners"); private SessionManager sessionManager; private UserManager userManager; /** * Creates a packet router. */ public IQRouter() { super("XMPP IQ Router"); } /** *

Performs the actual packet routing.

*

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.

*

Warning

*

Be careful to enforce concurrency DbC of concurrent by synchronizing * any accesses to class resources.

* * @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 || session.isAuthenticated() || ( 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 (SessionPacketRouter.isInvalidStanzaSentPriorToResourceBinding(packet, session)) { Log.debug("Closing session that attempts to send stanza to an entity other than the server itself or the client's account, before completing resource binding. Session closed: {}", session); session.close(new StreamError(StreamError.Condition.not_authorized, "Do not send invalid stanza prior to authentication/resource binding.")); return; } else { Log.debug("Rejecting stanza from client that has not (yet?) established an authenticated session: {}", packet.toXML()); 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 with an error IQ reply = new IQ(); if (childElement != null) { reply.setChildElement(childElement.createCopy()); } reply.setID(packet.getID()); reply.setTo(session.getAddress()); reply.setFrom(packet.getTo()); if (e.getRejectionError() != null) { reply.setError(e.getRejectionError()); } else { 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().isEmpty()) { // 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); } } } } /** *

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.

* * 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); } /** *

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.

* * 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.

* * Once an IQ result was received, the listener will be invoked and removed * from the list of listeners.

* * 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.

* * Once an IQ result was received, the listener will be invoked and removed * from the list of listeners.

* * 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.

* * Note that the listener will remain active for at least 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); resultPending.put(id, XMPPServer.getInstance().getNodeID()); resultTimeout.put(id, System.currentTimeMillis() + timeoutmillis); } @Override public void initialize(XMPPServer server) { super.initialize(server); TaskEngine.getInstance().scheduleAtFixedRate(new TimeoutTask(), Duration.ofSeconds(5), Duration.ofSeconds(5)); 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 // If there's a listener for this result at all, then it's likely that that listener had been registered // on this cluster node. For efficiency, try the local cluster node before triggering tasks in the rest // of the cluster. IQResultListener iqResultListener = resultListeners.remove(packet.getID()); if (iqResultListener != null) { resultTimeout.remove(packet.getID()); resultPending.remove(packet.getID()); try { iqResultListener.receivedAnswer(packet); } catch (Exception e) { Log.error("Error processing answer of remote entity. Answer: " + packet.toXML(), e); } return; } else if (ClusterManager.isClusteringStarted() ) { // Only do lookups in the cluster, after it's determined that the local node cannot process the result. final NodeID nodeID = resultPending.remove(packet.getID()); // remove it, to reduce the risk of this packet being sent back and forth. if ( nodeID != null && !XMPPServer.getInstance().getNodeID().equals( nodeID ) ) { CacheFactory.doClusterTask( new IQResultListenerTask( packet ), nodeID.toByteArray() ); return; } } } try { // Check for registered components, services or remote servers // OF-2112: It is generally permissible to route stanzas that have no 'from' attribute. However, // they are not allowed in s2s traffic. if (packet.getFrom() == null && !XMPPServer.getInstance().isLocal(recipientJID)) { // Stanzas that originate from clients _always_ have a 'from' attribute (as that attribute value is set/ // overwritten by Openfire upon receiving the stanza, to prevent abuse where a user tries to impersonate // someone else). That means that, if we're processing a stanza without a 'from' attribute, that the // stanza is very likely to originate from Openfire's code. If we have code that generates a stanza // without a 'from' address but addressed to a remote domain, this simply is a bug that we should very // verbosely warn about. Log.error("Unable to process a stanza that has no 'from' attribute, addressed to a remote entity. Stanza is being dropped: {}", packet.toXML()); return; } if (recipientJID != null && (routingTable.hasComponentRoute(recipientJID) || (packet.getFrom() != null && routingTable.hasServerRoute(new DomainPair(packet.getFrom().getDomain(), recipientJID.getDomain()))))) { // A component/service/remote server was found that can handle the Packet routingTable.routePacket(recipientJID, packet); 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, false) || UserManager.isPotentialFutureLocalUser(recipientJID))) { 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 Log.trace("Responding with 'service-unavailable' as IQ request blocked by privacy list, to: {}", packet); 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 Log.trace("Responding with 'service-unavailable' since the server can't handle the requested namespace, to: {}", packet); sendErrorPacket(packet, PacketError.Condition.service_unavailable); } else if (recipientJID.getNode() == null || "".equals(recipientJID.getNode())) { // Answer an error if JID is of the form sendErrorPacket(packet, PacketError.Condition.feature_not_implemented); } else { // JID is of the form Log.trace("Responding with 'service-unavailable' since the server can't handle packets sent to a node, to: {}", packet); 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 or full JID 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 (packet.isRequest() && recipientJID != null && recipientJID.getNode() != null && !XMPPServer.getInstance().isRemote(recipientJID) && !userManager.isRegisteredUser(recipientJID, false) && !UserManager.isPotentialFutureLocalUser(recipientJID) && !sessionManager.isAnonymousClientSession(recipientJID) && sessionManager.getSession(recipientJID) == null && !(recipientJID.asBareJID().equals(packet.getFrom().asBareJID()) && sessionManager.isPreAuthenticatedSession(packet.getFrom())) // A pre-authenticated session queries the server about itself. ) { // For an IQ stanza, the server MUST return a stanza error to the sender. Log.trace("Responding with 'service-unavailable' since there's no such local user that matches the addressee, to: {}", packet); 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 error IQ dummyIQ = packet.createCopy(); dummyIQ.setFrom(packet.getTo()); dummyIQ.setTo(packet.getFrom()); if (!((LocalClientSession) session).canDeliver(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 or belongs to a remote server // or to an uninstalled component routingTable.routePacket(recipientJID, packet); } } } 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. XMPPServer.getInstance().getPacketRouter().route(reply); } /** * 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 '{}': {}", recipient, 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.

* * 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> it = resultTimeout.entrySet().iterator(); while (it.hasNext()) { final Map.Entry 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(); } } } }