/* * 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.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.user.UserManager; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.*; import java.util.List; import java.util.StringTokenizer; /** *

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

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(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.isAuthenticated()) { 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 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 error Message dummyMessage = packet.createCopy(); dummyMessage.setFrom(packet.getTo()); dummyMessage.setTo(packet.getFrom()); if (!((LocalClientSession) session).canDeliver(dummyMessage)) { packet.setTo(session.getAddress()); packet.setFrom((JID)null); packet.setError(PacketError.Condition.not_acceptable); session.process(packet); isAcceptable = false; } } if (isAcceptable) { try { // Deliver stanza to requested route routingTable.routePacket(recipientJID, packet); } catch (Exception e) { log.error("Failed to route packet: " + packet.toXML(), e); routingFailed(recipientJID, packet); } // Sent carbon copies to other resources of the sender: if (session != null && Forwarded.isEligibleForCarbonsDelivery(packet)) { List 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 element qualified by the namespace "urn:xmpp:carbons:2", which itself contains a qualified by the namespace "urn:xmpp:forward:0" that contains the original stanza. message.addExtension(new Sent(new Forwarded(packet))); clientSession.process(message); } } } } } } 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.deliverRawText(new StreamError(StreamError.Condition.not_authorized).toXML()); return; } 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.getRejectionError() != null || (e.getRejectionMessage() != null && !e.getRejectionMessage().trim().isEmpty()))) { // 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()); if (e.getRejectionMessage() != null && !e.getRejectionMessage().trim().isEmpty()) { reply.setBody(e.getRejectionMessage()); } if (e.getRejectionError() != null) { reply.setError(e.getRejectionError()); } session.process(reply); } } } /** * Forwards the received message to the list of users defined in the property * xmpp.forward.admins. 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.

* * If the property xmpp.forward.admins 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().isEmpty()) { // 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; 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() >= 0) { // If message was sent to an unavailable full JID of a user then retry using the bare JID. routingTable.routePacket(recipient.asBareJID(), packet); return; } } } } if ( serverName.equals( recipient.getDomain() ) ) { // Delegate to offline message strategy, which will either bounce or ignore the message depending on user settings. log.trace( "Delegating to offline message strategy." ); messageStrategy.storeOffline( (Message) packet ); } else { // Recipient is not a local user. Bounce the message. // Note: this is similar, but not equal, to handling of message handling to local users in OfflineMessageStrategy. // 8.5.2. localpart@domainpart // 8.5.2.2. No Available or Connected Resources if (recipient.getResource() == null) { if (msg.getType() == Message.Type.headline || msg.getType() == Message.Type.error) { // For a message stanza of type "headline" or "error", the server MUST silently ignore the message. log.trace( "Not bouncing a message stanza to a bare JID of non-local user, of type {}", msg.getType() ); return; } } else { // 8.5.3. localpart@domainpart/resourcepart // 8.5.3.2.1. Message // For a message stanza of type "error", the server MUST silently ignore the stanza. if (msg.getType() == Message.Type.error) { log.trace( "Not bouncing a message stanza to a full JID of non-local user, of type {}", msg.getType() ); return; } } bounce( msg ); } } private void bounce(Message message) { // The bouncing behavior as implemented beyond this point was introduced as part // of OF-1852. This kill-switch allows it to be disabled again in case it // introduces unwanted side-effects. if ( !JiveGlobals.getBooleanProperty( "xmpp.message.bounce", true ) ) { log.trace( "Not bouncing a message stanza, as bouncing is disabled by configuration." ); return; } // Do nothing if the packet included an error. This intends to prevent scenarios // where a stanza that is bounced itself gets bounced, causing a loop. if (message.getError() != null) { log.trace( "Not bouncing a stanza that included an error (to prevent never-ending loops of bounces-of-bounces)." ); return; } // Do nothing if the sender was the server itself if (message.getFrom() == null || message.getFrom().toString().equals( serverName )) { log.trace( "Not bouncing a stanza that was sent by the server itself." ); return; } try { log.trace( "Bouncing a message stanza: {}", message); // Generate a rejection response to the sender final Message errorResponse = message.createCopy(); // return an error stanza to the sender, which SHOULD be errorResponse.setError(PacketError.Condition.service_unavailable); errorResponse.setFrom(message.getTo()); errorResponse.setTo(message.getFrom()); // Send the response route(errorResponse); } catch (Exception e) { log.error("An exception occurred while trying to bounce a message.", e); } } }