org.jivesoftware.openfire.IQRouter Maven / Gradle / Ivy
/*
* 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.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.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 iqHandlers = new ArrayList<>();
private Map namespace2Handlers = new ConcurrentHashMap<>();
private Map resultListeners = new ConcurrentHashMap<>();
private Map resultTimeout = new ConcurrentHashMap<>();
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 && 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);
}
}
}
}
/**
* 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);
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(new DomainPair(packet.getFrom().getDomain(), recipientJID.getDomain())))) {
// 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
sendErrorPacket(packet, PacketError.Condition.feature_not_implemented);
}
else {
// JID is of the form
// 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 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 (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 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 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 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, [email protected]
*/
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();
}
}
}
}